ActiveRecord 進階功能
Most of you are familiar with the virtues of a programmer. There are three, of course: laziness, impatience, and hubris. - Larry Wall
本章介紹其他ActiveRecord的常用進階功能。
單一表格繼承STI(Single-table inheritance)
如何將物件導向中的繼承概念,對應到關聯式資料庫的設計,是個大哉問。Rails內建了其中最簡單的一個解法,只用一個資料表儲存繼承體系中的物件,搭配一個type
欄位用來指名這筆資料的類別名稱。
要開啟STI功能,依照慣例只要有一個欄位叫做type
,型態字串即可。假設以下的posts
資料表有欄位叫做type
,那麼這三個Models實際上就會共用posts
一個資料表,當然,還有這兩個子類別也都繼承到父類別的validates_presence_of :subject
:
class Post < ActiveRecord::Base
validates_presence_of :subject
end
class GuestPost < Post
end
class MemberPost < Post
end
讓我們進入rails console實驗看看,Rails會根據你使用的類別,自動去設定type
欄位:
post = GuestPost.create( :subject => "guest")
post.type # "GuestPost"
post.id # 1
post = MemberPost.create( :subject => "member" )
post.id # 2
post.type # "MemberPost"
GuestPost.last # 1
很遺憾,也因為這個慣例的關係,你不能將
type
這麼名字挪做它用。
STI最大的問題在於欄位的浪費,如果繼承體系中交集的欄位不多,那麼使用STI就會非常的浪費空間。如果有較多的不共用的欄位,筆者會建議不要使用這個功能,讓個別的類別有自己的資料表。要關閉STI,請父類別加上self.abstract_class = true
class Post < ActiveRecord::Base
self.abstract_class = true
validates_presence_of :subject
end
class GuestPost < Post
end
class MemberPost < Post
end
這裡的GuestPost和MemberPost就需要有自己的Migrations建立guest_posts和member_posts資料表。
觀察者Observers
Observer(觀察者)是一種常見的設計模式,可以讓你可以針對Model 的生命週期中的某些階段做出對應的行為,例如你想在使用者註冊完帳號的時寄送一封確認信給他,這時候你就可以註冊一個觀察者來觀察User Model,當User建立成功的時候便寄送Email。
你可以透過Rails提供的generator來建立Observer:
$ rails generate observer user
invoke active_record
create app/models/user_observer.rb
invoke rspec
create spec/models/user_observer_spec.rb
建立好後需要在config/application.rb
中註冊這個觀察者:
config.active_record.observers = :user_observer
在所建立的觀察者類別中,撰寫需要觸發的行為,例如:
class PageObserver < ActiveRecord::Observer
observe :user
def after_save(user)
UserMailer.registration_confirmation(user).deliver # 寄送註冊確認信給 user
end
end
一個Observer中也可同時觀察多個不同的Model:
class ContentObserver < ActiveRecord::Observer
observe :article, :comment
def update_update(record)
# do something
end
end
Observer支援的觸發條件,和Callback所定義的七種觸發條件是相同的,詳見ActiveRecord 資料驗證及回呼 中的回呼一節
與Callback不同的地方是,Observer通常是設計來做所觀察的Model以外的行為,例如在我們的例子中我們觀察了User,而在觸發的行為我們呼叫了其他類別的方法(UserMailer),這不屬於User Model的責任範圍,因此在應用上我們會將自己Model責任範圍內的行為使用Callback,像是資料驗證或是更新。本身責任範圍外的行為則使用Observer,像是寄送Email或是呼叫背景處理等。
交易Transactions
Transaction(交易)保證所有資料的操作都只有在成功的情況下才會寫入到資料庫,最著名的例子也就是銀行的帳戶交易,只有在帳戶提領金額及存入帳戶這兩個動作都成功的情況下才會將這筆操作寫入資料庫,否則在其中一個動作因為某些原因失敗的話就會放棄所有已做的操作將資料回復到交易前的狀態。在Rails中使用交易的方式像這樣:
ActiveRecord::Base.transaction do
david.withdrawal(100)
mary.deposit(100)
end
你可以在一個交易中包含不同Active Record的類別或物件,這是因為交易是以資料庫連線為範圍,而不是個別Model:
User.transaction do
User.create!(:name => 'ihower')
Feed.create!
end
注意到這裡我們要使用create!
而不是create
,這是因為前者驗證失敗才會丟出例外,好讓整個交易失敗。同理,在交易裡做更新應該使用update_attributes!
而不是update_attributes
。
單一Model的save
及destroy
方法已經幫你使用transaction包起來了,當資料驗證失敗或其中的回呼發生例外時,Rails就會觸發rollback。所以下述的交易區塊是多餘的不需要寫:
User.transaction do # 這是多餘的
User.create!(:name => 'ihower')
end
另外,由於資料的更新要在交易完成後才能被讀取到,所以如果你在after_save
回呼裡讓外部服務存取(例如呼叫全文搜尋引擎做索引),很可能因為交易尚未完成,會讀取不到更新。這時候必須改用after_commit
這個回呼,才能確保讀取到交易完成後的資料。
Dirty objects
Dirty Objects功能可以追蹤Model的屬性是否有改變:
person = Person.find_by_name('Uncle Bob')
person.changed? # => false 沒有改變任何值
# 讓我們來改一些值
person.name = 'Bob'
person.changed? # => true 有改變
person.name_changed? # => true 這個屬性有改變
person.name_was # => 'Uncle Bob' 改變之前的值
person.name_change # => ['Uncle Bob', 'Bob']
person.name = 'Bill'
person.name_change # => ['Uncle Bob', 'Bill']
# 儲存進資料庫
person.save
person.changed? # => false
person.name_changed? # => false
# 看看哪些屬性改變了
person.name = 'Bob'
person.changed # => ['name']
person.changes # => { 'name' => ['Bill', 'Bob'] }
注意到Model資料一旦儲存進資料庫,追蹤記錄就重算消失了。
什麼時候會用到這個功能呢?通常是在儲存進資料庫前的回呼、驗證或Observer中,你想根據修改了什麼來做些動作,這時候Dirty Objects功能就派上用場了。
Polymorphic Associations
多型關連(Polymorphic Associations)可以讓一個 Model 不一定關連到某一個特定的 Model,秘訣在於除了整數的_id
外部鍵之外,再加一個字串的_type
欄位說明是哪一種Model。
例如一個Comment model
,我們可以透過多型關連讓它belongs_to
到各種不同的 Model上,假設我們已經有了Article與Photo這兩個Model,然後我們希望這兩個Model都可以被留言。不用多型關連的話,你得分別建立ArticleComment和PhotoComment的model。用多型關連的話,無論有多少種需要被留言的Model,只需要一個Comment model即可:
rails g model comment content:text commentable_id:integer commentable_type
這樣會產生下面的 Migration 檔案:
class CreateComments < ActiveRecord::Migration
def change
create_table :comments do |t|
t.text :content
t.integer :commentable_id
t.string :commentable_type
t.timestamps
end
end
end
這個Migration檔案中,我們用content這個欄位來儲存留言的內容,commentable_id用來儲存被留言的物件的id而commentable_type則用來儲存被留言物件的種類,以這個例子來說被留言的對象就是Article與Photo這兩種Model,這個Migration檔案也可以改寫成下面這樣:
class CreateComments < ActiveRecord::Migration
def change
create_table :comments do |t|
t.text :content
t.belongs_to :commentable, :polymorphic => true
t.timestamps
end
end
end
回到我們的Model,我們必須指定他們的關聯關係:
class Comment < ActiveRecord::Base
belongs_to :commentable, :polymorphic => true
end
class Article < ActiveRecord::Base
has_many :comments, :as => :commentable
end
class Photo < ActiveRecord::Base
has_many :comments, :as => :commentable
end
這樣會告訴Rails如何去設定你的多型關係,現在讓我們進console實驗看看:
article = Article.first
# 透過關連新增留言
comment = article.comments.create(:content => "First Comment")
# 你可以發現 Rails 很聰明的幫我們指定了被留言物件的種類和id
comment.commentable_type => "Article"
comment.commentable_id => 1
# 也可以透過 commentable 反向回查關連的物件
comment.commentable => #<Article id: 1, ....>
DBA背景的同學可能會注意到PolymorphicAassociations無法做到保證Referential integrity特性。原因很簡單,既然不知道
_id
會指到哪個table
,自然也就沒辦法在資料庫層級加上Foreign key constraint。
序列化Serialize
序列化(Serialize)通常指的是將一個物件轉換成一個可被資料庫儲存及傳輸的純文字形態,反之將這筆資料從資料庫讀出後轉回物件的動作我們就稱之為反序列(Deserialize),Rails提供了serialize
讓你指定需要序列化資料的欄位,任何物件在存入資料庫時就會自動序列化成YAML格式,而當從資料庫取出時就會自動幫你反序列成原先的物件。下面的範例中,settings通常是text型態讓我們有更大的空間可以儲存資料,然後我們將一個Hash物件序列化之後儲存到settings裡:
class User < ActiveRecord::Base
serialize :settings
end
> user = User.create(:settings => { "sex" => "male", "url" => "foo" })
> User.find(user.id).settings # => { "sex" => "male", "url" => "foo" }
雖然序列化很方便可以讓你儲存任意的物件,但是缺點是序列化資料就失去了透過資料庫查詢索引的功效,你無法在SQL的where條件中指定序列化後的資料。
Store
Store又在包裹了上一節的序列化功能,是個簡單又實用的功能,讓你可以將某個欄位指定儲存為Hash值。舉例來說,上一節的settings也可以改用store來設定:
class User < ActiveRecord::Base
store :settings, accessors => [:sex, :url]
end
特別的是其中accessors
用來設定可以直接存取的屬性,這樣就可以像平常一樣那樣操作sex和url這兩個屬性,讓我們進console實驗看看:
> user = User.new(:sex => "male", :url => "http://example.com")
> user.sex
=> "male"
> user.url
=> "http://example.com"
> user.settings
=> {:sex => "male", :url => "http://example.com"}
因為store
就像使用hash一樣,你也可以直接操作它,加入新的資料:
> user.settings[:food] = "pizza"
> user.settings
=> {:sex => "male", :url => "http://example.com", :food => "pizza"}
更多線上資源
- Active Record Query Interface http://guides.rubyonrails.org/active_record_querying.html
转自:http://ihower.tw/rails3/activerecord-others.html