Rails Gossip: ActiveRecord 基礎
ActiveRecord是Rails的物件/關聯映射(Object/Relational Mapping, ORM)解決方案,可以物件角度來進行資料庫操作,將物件與物件關係,映射至關聯資料庫表格與表格的關係,Rails的ActiveRecord方案實現了ActiveRecord 模式,物件本身代表資料表格中的一筆資料,物件本身攜帶資料庫存取的相關操作方法。
先前在 MVC 與 Rails 中曾略為看過ActiveRecord的一些使用,當你使用rails generate model產生指定的資料庫模型物件時,會產生幾個檔案:
$ rails g model message name:string content:text is_read:boolean invoke active_record create db/migrate/20111214072231_create_messages.rb create app/models/message.rb invoke test_unit create test/unit/message_test.rb create test/fixtures/messages.yml |
以上例而言,*_create_messages.rb記錄了稍後執行db:migrate時要作的動作,其中*是時間標記,用以避免名稱衝突,就之前的指令主要是建立新表格:
- *_create_messages.rb
class CreateMessages < ActiveRecord::Migration def change create_table :messages do |t| t.string :name t.text :content t.boolean :is_read t.timestamps end end end
create_table方法指定建立messages表格,在程式區塊中分別指定t.string、t.text、t.boolean等建立了name、content、is_read欄位,t.timestamps則會自動建立created_at與updated_at兩個欄位,每個表格也會有個自動增生的id欄位作為主鍵。為了執行*_create_messages.rb的內容以建立表格,需要執行db:migrate任務:
~/hello$ bundle exec rake db:migrate |
使用的SQL可在log/development.log中查到:
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
"name" varchar(255),
"content" text,
"is_read" boolean,
"created_at" datetime,
"updated_at" datetime
)
message.rb中定義了對應資料庫表格的模型:
- message.rb
class Message < ActiveRecord::Base end
ActiveRecord是Rails中提供ORM(Object-Relaction Mapping)的元件,預設的message.rb只繼承ActiveRecord:Base定義的基本功能,基本上,類別方法是對整體資料庫的操作,實例方法是對表格中各筆資料的操作。
ActiveRecord物件並不支援在類別上定義屬性,而是直接參考所連結的資料表格,要新增、刪除或改變屬性,都是直接對資料庫表格相關欄位加以修改,資料表格欄位的變化,會反應在ActiveRecord的屬性。
要準備一筆新的資料,可以使用new方法,例如:
message = Message.new
message.name = "Justin"
message.content = "Justin's message"
欄位名稱對應會實例方法名稱,另一個建立實例並指定資料的方式是:
message = Message.new { |msg|
msg.name = "Justin"
msg.content = "Justin's message"
}
最簡單建立實例並準備資料的方式,是在new時給予一個Hash實例,這對於接受params請求參數時相當方便:
message = Message.new(:name => "Justin", :content => "Hi! Justin!")
此時建立的實例,只是記憶體中的一筆資料,還沒有真正新增至資料庫,如果要儲存資料,可以使用save或save!方法。例如:
message.save
save儲存成功與否,會傳回布林值true或false表示,save!如果成功會傳回true,如果失敗會丟出例外,如果是新建物件執行save或save!,會執行INSERT。例如:
INSERT INTO "messages" ("content", "created_at", "is_read", "name", "updated_at") VALUES (?, ?, ?, ?, ?)
如果緊接著修改了物件的屬性,再次執行儲存,則會進行UPDATE。例如若僅message.content = "XD",再次執行save或save!會如下:
UPDATE "messages" SET "content" = 'XD', "updated_at" = '2012-01-13 03:43:38.294211' WHERE "messages"."id" = 1
如果想直接建立實例並儲存至資料庫,可以使用create或create!。例如:
Message.create(:name => "Justin", :content => "Hi! Justin!")
物件建立並儲存為資料表中的一筆資料後,物件的id就會被指定,此時物件就對應於資料庫的一筆資料,可以用new_record?來得知資料是否已經在資料庫。如果要查找資料,可以使用find指定id查找。例如:
Message.find(1)
如果有兩筆以上要查找的資料,可以使用find指定多個id,此時會以陣列方式傳回實例。例如:
Message.find(1, 2) # 或Message.find([1, 2])
直接更新物件上的屬性,並不會直接對資料庫中的資料造成修改,而必須在執行save或save!之後,才會對資料庫造成更新。除了根據id查找的find方法之外,可以使用find_by_*或find_all_by_*方法,前者只找回第一個符合資料,後者則找回所有符合資料,*部份表示欄位名稱,可以使用and連接。例如:
Message.find_by_name("Justin")
Message.find_all_by_name("Justin")
Message.find_by_name_and_content("Justin", "Hi, Justin!")
all可以取得所有資料,first可以取得第一筆資料,last可以取得最後一筆資料,也可以使用where指定條件。例如:
Message.where(:name => "Justin", :is_read => nil) # 結果相當於 Message.find_all_by_name_and_is_read("Justin", nil)
Message.where("name = ? or is_read = ?", "Justin", nil)
可以注意一開始就使用find_all_by、all等方法與where的不同,find_all_by、all等方法會一次取出所有符合資料至記憶體,如果之後有緊接著其它限定方法,則是從記憶體中符合的資料中再取得,where之後若緊接著有一些限定方法,將會影響產生的SQL。例如Message.find_all_by_name("Justin").last與Message.where(:name => "Justin").last,最後取得的物件雖然相同的,但自動產生的SQL卻是不同。例如Message.find_all_by_name("Justin").last會產生以下的SQL:
SELECT "messages".* FROM "messages" WHERE "messages"."name" = 'Justin'
Message.where(:name => "Justin").last則會產生以下的SQL:
SELECT "messages".* FROM "messages" WHERE "messages"."name" = 'Justin' ORDER BY "messages"."id" DESC LIMIT 1
以上兩個例子,在資料筆數多時,使用where會比較有效率,像是搭配count、sum、maximum、average等統計方法:
Message.where(:name => "Justin").count
以上會比Message.find_all_by_name("Justin").count來得有效率。
可以使用limit限制查詢筆數。例如:
messages = Message.limit(5).find_all_by_name("Justin")
可以使用offset設定從第幾筆查到的資料之後開始取得,通常用於分頁查詢:
messages = Message.offset(2).find_by_name("Justin")
可以使用order作排序,用reorder取消排序。例如:
Message.order("name").find_by_name("Justin")
Message.order("name DESC").find_by_name("Justin")
Message.order("name DESC, updated_at ASC").find_by_name("Justin")
可以使用select指定只取出哪些欄位。例如:
Message.select("content").find_all_by_name("Justin")
Message.select("content, is_read").find_all_by_name("Justin")
以上介紹到的where、limit、offset、select、order等方法可以自由組合,以取得想要的查詢,方法呼叫組合的結果會影響產生的SQL語句,不確定哪種組合有效率的話,可以在Rails console中試試,觀察產生的SQL。如果真要自己下SQL查找,可以使用find_by_sql。例如:
messages = Message.find_by_sql("SELECT * FROM messages")
如果查詢出來的資料不希望被修改,可以使用readonly。例如:
message = Message.readonly.find(1)
如上取得的資料,如果message的屬性被更改而後嘗試更新資料庫,或者嘗試刪除資料,就會引發例外。
如果要更新資料,除了對查找到的資料使用save或save!之外,還可以使用update_attributes或update_attributes!,單一屬性更新則使用update_attribute或update_attribute!。例如:
message.update_attributes(:content => "Justin! Welcome to Rails!", :is_read => true)
message.update_attribute("name", "caterpillar")
如果資料庫中某筆資料有變更,物件可以使用reload來重新載入資料。例如:
message.reload
如果已取得資料,可以使用destroy刪除資料。例如:
message.destroy
或者也可以透過類別方法delete、delete_all、destroy_all來刪除資料。例如:
Message.delete(1) # 刪除id為1的資料
Message.delete_all(["updated_at < ?", 10.minute.ago]
以上先介紹基本的增刪查找,物件與資料表關聯在之後會介紹,更多資料則可參考 ActiveRecord::BASE 文件,或是 Active Record Query Interface 文件,交易(Transaction)則可參考 Active Record Transactions。