本課時將講解 ActiveRecord 中常用的回調(diào)方法。
ActiveModel 提供了多個實用的功能,它可以讓一個普通的類,具備如屬性校驗,回調(diào),顯示字段 I18n 值等眾多功能。
比如,我們可以為 Person 類增加了一個回調(diào)方法:
class Person
extend ActiveModel::Callbacks
define_model_callbacks :create
end
所謂回調(diào),是指在某個方法前(before)、后(after)、前后(around),執(zhí)行某個方法。上面的例子里,Person 擁有了三個標(biāo)準(zhǔn)的回調(diào)方法:before_create、after_create、around_create。
我們還需要為這個回調(diào)方法增加邏輯代碼:
class Person
extend ActiveModel::Callbacks
define_model_callbacks :create
# 定義 create 方法代碼
def create
run_callbacks :create do
puts "I am in create method."
end
end
# 開始定義回調(diào)
before_create :action_before_create
def action_before_create
puts "I am in before action of create."
end
after_create :action_after_create
def action_after_create
puts "I am in after action of create."
end
around_create :action_around_create
def action_around_create
puts "I am in around action of create."
yield
puts "I am in around action of create."
end
end
進(jìn)入到 Rails 的終端里,我們測試下這個類:
% rails c
> person = Person.new
> person.create
I am in before action of create.
I am in around action of create.
I am in create method.
I am in around action of create.
I am in after action of create.
在 ActionModel 中有許多的 Ruby 元編程知識,如果你感興趣,可以讀一讀《Ruby 元編程(第二版)》這本書。
ActiveRecord 中的 回調(diào) 將常用的 find
,create
,update
,destroy
等方法進(jìn)行包裝。
Rails 在 controller 也有回調(diào),我們下一章會介紹。
我們在 Rails 中使用的 Model 回調(diào),是通過調(diào)用 ActiveRecord 中定義的 實例方法
來實現(xiàn)的,比如 before_validation
方法,實現(xiàn)了在 validate
方法前的回調(diào)。
所謂 回調(diào)
,就是在目標(biāo)方法上,再執(zhí)行其他的方法代碼。
ActiveRecord 提供了眾多回調(diào)方法,包含了一個 model 實例在數(shù)據(jù)庫操作中的各個時期。按照數(shù)據(jù)庫操作的不同,可以將它們劃分為五種情形的回調(diào)方法。
after_initialize 會在一個實例使用 new 創(chuàng)建,或從數(shù)據(jù)庫讀取時觸發(fā)。這樣避免直接覆寫實例的 initialize 方法。
當(dāng)從數(shù)據(jù)庫讀取數(shù)據(jù)時,會觸發(fā) after_find 回調(diào):
after_find 執(zhí)行優(yōu)先于 after_initialize。
執(zhí)行實例的 touch
方法觸發(fā)該回調(diào)。
我們觀察一下以上每個回調(diào)的執(zhí)行的順序,這里做一個簡單的例子:
class Product < ActiveRecord::Base
before_validation do
puts "before_validation"
end
after_validation do
puts "after_validation"
end
before_save do
puts "before_save"
end
around_save :test_around_save
def test_around_save
puts "begin around_save"
yield
puts "end around_save"
end
before_create do
puts "before_create"
end
around_create :test_around_create
def test_around_create
puts "begin around_create"
yield
puts "end around_create"
end
after_create do
puts "after_create"
end
after_save do
puts "after_save"
end
after_commit do
puts "after_commit"
end
after_rollback do
puts "after_rollback"
end
end
進(jìn)入終端試驗下:
product = Product.new(name: "TTT")
product.save
(0.1ms) begin transaction
before_validation
after_validation
before_save
begin around_save
before_create
begin around_create
SQL (0.6ms) INSERT INTO "products" ("name", "created_at", "updated_at") VALUES (?, ?, ?) [["name", "TTT"], ["created_at", "2015-06-16 02:49:20.871384"], ["updated_at", "2015-06-16 02:49:20.871384"]]
end around_create
after_create
end around_save
after_save
(0.7ms) commit transaction
after_commit
=> true
可以看到,create 回調(diào)是最接近 sql 執(zhí)行的,并且 validation、save、create 回調(diào)被包含在一個 transaction 事務(wù)中,最后,是 after_commit 回調(diào)。
我們在設(shè)計邏輯的過程中,需要了解它執(zhí)行的順序。當(dāng)需要在回調(diào)中操作保存到數(shù)據(jù)庫后的實例,需要把代碼放到 在 after_commit
中。
上面列出的,是回調(diào)的方法名,我們還需要編寫具體的回調(diào)代碼。
class Topic < ActiveRecord::Base
before_destroy :delete_parents [1]
private [2]
def delete_parents [3]
self.class.delete_all "parent_id = #{id}"
end
end
[1] 用符號定義回調(diào)執(zhí)行的方法名稱 [2] private 或 protected 方法均可作為回調(diào)執(zhí)行方法 [3] 執(zhí)行的方法名,和定義的符號一致
對于 round_
回調(diào),我們需要在方法中使用 yield
,上面的例子已經(jīng)看到:
around_create :test_around_create
def test_around_create
puts "begin around_create"
yield
puts "end around_create"
end
before_create do
self.name = login.capitalize if name.blank?
end
回調(diào)執(zhí)行時,self 指的是它本身。在注冊的時候,我們可能不需要填寫 name,而要填寫 login,所以默認(rèn)把 name 改成 login 的首字母大寫形式。
上面例子也可以改寫成:
before_create { |record|
record.name = record.login.capitalize if record.name.blank?
}
在一些注冊和修改的邏輯中,注冊時默認(rèn)填寫的數(shù)據(jù),在修改時不做處理,所以回調(diào)方法只在 create 上生效,下面的例子就是這種情形:
before_validation(on: :create) do
self.number = number.gsub(/[^0-9]/, "")
end
或者:
before_validation :normalize_name, on: :create
和校驗一樣,回調(diào)也可以增加 if 或 unless 判斷:
before_save :normalize_card_number, if: :paid_with_card?
class Topic < ActiveRecord::Base
before_destroy 'self.class.delete_all "parent_id = #{id}"'
end
before_destroy
既可以接受符號定義的方法名,也可以接受字符串。這種方式要被廢棄掉了。
一個類集成自另一個類,也會繼承它的回調(diào),比如:
class Topic < ActiveRecord::Base
before_destroy :destroy_author
end
class Reply < Topic
before_destroy :destroy_readers
end
在執(zhí)行 Reply#destroy
的時候,兩個回調(diào)都會被執(zhí)行,為了避免這種情況,可以覆寫 before_destroy
:
class Reply < Topic
def before_destroy() destroy_readers end
end
但是,這是非常不好的解決方案!這個代碼只是一個例子,來自 這里。
回調(diào)雖然可以解決問題,但是它功能太過強大,當(dāng)項目代碼變得復(fù)雜,回調(diào)的維護(hù)會造成很大的技術(shù)難度。建議使用回調(diào)解決小問題,過多的業(yè)務(wù)邏輯應(yīng)該單獨處理,或者使用單獨的回調(diào)類。
我們可以用一個類作為 回調(diào)類
,使用它的的實例方法實現(xiàn)回調(diào)邏輯:
class BankAccount < ActiveRecord::Base
before_save EncryptionWrapper.new
end
class EncryptionWrapper
def before_save(record) [1]
record.credit_card_number = encrypt(record.credit_card_number)
end
end
[1] 該方法僅能接受一個參數(shù),為該 model 實例。
還可以使用 回調(diào)類
的類方法,來定義回調(diào)邏輯:
class PictureFileCallbacks
def self.after_destroy(picture_file)
...
end
end
在使用上:
class PictureFile < ActiveRecord::Base
after_destroy PictureFileCallbacks
end
使用單獨的回調(diào)類,可以方便我們維護(hù)回調(diào)代碼,但是使用起來也需慎重考慮,不要增加后期的維護(hù)難度。
在我們前面講解中,更新一個記錄時,destroy 方法會觸發(fā)校驗和回調(diào),而 delete 方法不會。在這里詳細(xì)的列出,ActiveRecord 方法中,哪些會觸發(fā)回調(diào),哪些不會。
觸發(fā)回調(diào):
不觸發(fā)回調(diào):
所有的回調(diào),在動作執(zhí)行的過程中,是順序觸發(fā)的。在 before_xxx
回調(diào)中,如果返回 false
, 這個回調(diào)過程會被終止,并且觸發(fā)數(shù)據(jù)庫事務(wù)的 rollback
,以及 after_rollback
回調(diào)。
但是,對于 after_xxx
回調(diào),就只能用 raise
拋出異常的方式,來終止它。這里拋出的異常必須是 ActiveRecord::Rollback
。我們修改下 after_create
回調(diào):
after_create do
puts "after_create"
raise ActiveRecord::Rollback
end
在終端里:
> Product.create
(0.1ms) begin transaction
before_validation
after_validation
before_save
begin around_save
before_create
begin around_create
SQL (0.4ms) INSERT INTO "products" ("created_at", "updated_at") VALUES (?, ?) [["created_at", "2015-08-03 15:30:20.552783"], ["updated_at", "2015-08-03 15:30:20.552783"]]
end around_create
after_create
(8.5ms) rollback transaction
after_rollback
=> #<Product id: nil, name: nil, price: nil, description: nil, created_at: "2015-08-03 15:30:20", updated_at: "2015-08-03 15:30:20", top: nil, hot: nil>
ActiveRecord::Rollback
終止了數(shù)據(jù)庫事務(wù),返回了一個沒有保存到數(shù)據(jù)庫中的實例。如果我們不拋出這個異常,比如拋出一個標(biāo)準(zhǔn)的異常類:
after_create do
puts "after_create"
raise StandardError
end
雖然它也會終止事務(wù),沒有把保存數(shù)據(jù),但是它再次拋出這個異常,而不是返回我們想要的未保存實例。
...
after_rollback
StandardError: StandardError
from /PATH/shop/app/models/product.rb:40:in `block in <class:Product>'
...
after_commit
中的實例當(dāng)我們在回調(diào)中使用當(dāng)前實例的時候,它并沒有保存到數(shù)據(jù)庫中,只有當(dāng)數(shù)據(jù)庫事務(wù) commit
之后,這個實例才會被保存,所以我們在 after_commit
回調(diào)中讀取它數(shù)據(jù)庫中的 id,并在這里設(shè)置它和其他實例的關(guān)聯(lián)。
使用回調(diào)可以適當(dāng)精簡邏輯代碼,比如我們購買一個商品類型時,在創(chuàng)建訂單后,應(yīng)減少該商品類型的庫存數(shù)量。該 減少數(shù)量
的動作雖然屬于整體邏輯,但是和訂單邏輯是分開的,而它的觸發(fā)點正好在訂單 create
動作完成后,所以我們把它放到 after_create
中。
首先我們給 variants 增加 on_hand 屬性,表示當(dāng)前持有的數(shù)量:
rails g migration add_on_hand_to_variants on_hand:integer
在 order.rb 中編寫回調(diào):
after_create do
line_items.each do |line_item|
line_item.variant.decrement!(:on_hand, line_item.quantity)
end
end