rails 单表继承 观察者

来源:互联网 发布:高校法学教学软件 编辑:程序博客网 时间:2024/06/05 07:27


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 :subjectendclass GuestPost < Postendclass MemberPost < Postend

讓我們進入rails console實驗看看,Rails會根據你使用的類別,自動去設定type欄位:

post = GuestPost.create( :subject => "guest")post.type # "GuestPost"post.id # 1post = MemberPost.create( :subject => "member" )post.id # 2post.type # "MemberPost"GuestPost.last # 1

很遺憾,也因為這個慣例的關係,你不能將type這麼名字挪做它用。

STI最大的問題在於欄位的浪費,如果繼承體系中交集的欄位不多,那麼使用STI就會非常的浪費空間。如果有較多的不共用的欄位,筆者會建議不要使用這個功能,讓個別的類別有自己的資料表。要關閉STI,請父類別加上self.abstract_class = true

class Post < ActiveRecord::Base    self.abstract_class = true    validates_presence_of :subjectendclass GuestPost < Postendclass MemberPost < Postend

這裡的GuestPostMemberPost就需要有自己的Migrations建立guest_postsmember_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  endend

一個Observer中也可同時觀察多個不同的Model:

class ContentObserver < ActiveRecord::Observer    observe :article, :comment    def update_update(record)      # do something    endend

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

單一Modelsavedestroy方法已經幫你使用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.saveperson.changed?       # => falseperson.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上,假設我們已經有了ArticlePhoto這兩個Model,然後我們希望這兩個Model都可以被留言。不用多型關連的話,你得分別建立ArticleCommentPhotoCommentmodel。用多型關連的話,無論有多少種需要被留言的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  endend

這個Migration檔案中,我們用content這個欄位來儲存留言的內容,commentable_id用來儲存被留言的物件的idcommentable_type則用來儲存被留言物件的種類,以這個例子來說被留言的對象就是ArticlePhoto這兩種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  endend

回到我們的Model,我們必須指定他們的關聯關係:

class Comment < ActiveRecord::Base  belongs_to :commentable, :polymorphic => trueendclass Article < ActiveRecord::Base  has_many :comments, :as => :commentableendclass Photo < ActiveRecord::Base  has_many :comments, :as => :commentableend

這樣會告訴Rails如何去設定你的多型關係,現在讓我們進console實驗看看:

article = Article.first# 透過關連新增留言comment = article.comments.create(:content => "First Comment")# 你可以發現 Rails 很聰明的幫我們指定了被留言物件的種類和idcomment.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 :settingsend> user = User.create(:settings => { "sex" => "male", "url" => "foo" })> User.find(user.id).settings # => { "sex" => "male", "url" => "foo" }

雖然序列化很方便可以讓你儲存任意的物件,但是缺點是序列化資料就失去了透過資料庫查詢索引的功效,你無法在SQLwhere條件中指定序列化後的資料。

Store

Store又在包裹了上一節的序列化功能,是個簡單又實用的功能,讓你可以將某個欄位指定儲存為Hash值。舉例來說,上一節的settings也可以改用store來設定:

class User < ActiveRecord::Base  store :settings, accessors => [:sex, :url]end

特別的是其中accessors用來設定可以直接存取的屬性,這樣就可以像平常一樣那樣操作sexurl這兩個屬性,讓我們進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
0 0