# Active Record 回調
本文介紹如何介入 Active Record 對象的生命周期。
讀完本文,你將學到:
* Active Record 對象的生命周期;
* 如何編寫回調方法響應對象聲明周期內發生的事件;
* 如何把常用的回調封裝到特殊的類中;
### Chapters
1. [對象的生命周期](#%E5%AF%B9%E8%B1%A1%E7%9A%84%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F)
2. [回調簡介](#%E5%9B%9E%E8%B0%83%E7%AE%80%E4%BB%8B)
* [注冊回調](#%E6%B3%A8%E5%86%8C%E5%9B%9E%E8%B0%83)
3. [可用的回調](#%E5%8F%AF%E7%94%A8%E7%9A%84%E5%9B%9E%E8%B0%83)
* [創建對象](#%E5%88%9B%E5%BB%BA%E5%AF%B9%E8%B1%A1)
* [更新對象](#%E6%9B%B4%E6%96%B0%E5%AF%B9%E8%B1%A1)
* [銷毀對象](#%E9%94%80%E6%AF%81%E5%AF%B9%E8%B1%A1)
* [`after_initialize` 和 `after_find`](#after_initialize-%E5%92%8C-after_find)
* [`after_touch`](#after_touch)
4. [執行回調](#%E6%89%A7%E8%A1%8C%E5%9B%9E%E8%B0%83)
5. [跳過回調](#%E8%B7%B3%E8%BF%87%E5%9B%9E%E8%B0%83)
6. [終止執行](#%E7%BB%88%E6%AD%A2%E6%89%A7%E8%A1%8C)
7. [關聯回調](#%E5%85%B3%E8%81%94%E5%9B%9E%E8%B0%83)
8. [條件回調](#%E6%9D%A1%E4%BB%B6%E5%9B%9E%E8%B0%83)
* [使用 Symbol](#%E4%BD%BF%E7%94%A8-symbol)
* [使用字符串](#%E4%BD%BF%E7%94%A8%E5%AD%97%E7%AC%A6%E4%B8%B2)
* [使用 Proc](#%E4%BD%BF%E7%94%A8-proc)
* [回調的多重條件](#%E5%9B%9E%E8%B0%83%E7%9A%84%E5%A4%9A%E9%87%8D%E6%9D%A1%E4%BB%B6)
9. [回調類](#%E5%9B%9E%E8%B0%83%E7%B1%BB)
10. [事務回調](#%E4%BA%8B%E5%8A%A1%E5%9B%9E%E8%B0%83)
### 1 對象的生命周期
在 Rails 程序運行過程中,對象可以被創建、更新和銷毀。Active Record 為對象的生命周期提供了很多鉤子,讓你控制程序及其數據。
回調可以在對象的狀態改變之前或之后觸發指定的邏輯操作。
### 2 回調簡介
回調是在對象生命周期的特定時刻執行的方法。回調方法可以在 Active Record 對象創建、保存、更新、刪除、驗證或從數據庫中讀出時執行。
#### 2.1 注冊回調
在使用回調之前,要先注冊。回調方法的定義和普通的方法一樣,然后使用類方法注冊:
```
class User < ActiveRecord::Base
validates :login, :email, presence: true
before_validation :ensure_login_has_a_value
protected
def ensure_login_has_a_value
if login.nil?
self.login = email unless email.blank?
end
end
end
```
這種類方法還可以接受一個代碼塊。如果操作可以使用一行代碼表述,可以考慮使用代碼塊形式。
```
class User < ActiveRecord::Base
validates :login, :email, presence: true
before_create do
self.name = login.capitalize if name.blank?
end
end
```
注冊回調時可以指定只在對象生命周期的特定事件發生時執行:
```
class User < ActiveRecord::Base
before_validation :normalize_name, on: :create
# :on takes an array as well
after_validation :set_location, on: [ :create, :update ]
protected
def normalize_name
self.name = self.name.downcase.titleize
end
def set_location
self.location = LocationService.query(self)
end
end
```
一般情況下,都把回調方法定義為受保護的方法或私有方法。如果定義成公共方法,回調就可以在模型外部調用,違背了對象封裝原則。
### 3 可用的回調
下面列出了所有可用的 Active Record 回調,按照執行各操作時觸發的順序:
#### 3.1 創建對象
* `before_validation`
* `after_validation`
* `before_save`
* `around_save`
* `before_create`
* `around_create`
* `after_create`
* `after_save`
#### 3.2 更新對象
* `before_validation`
* `after_validation`
* `before_save`
* `around_save`
* `before_update`
* `around_update`
* `after_update`
* `after_save`
#### 3.3 銷毀對象
* `before_destroy`
* `around_destroy`
* `after_destroy`
創建和更新對象時都會觸發 `after_save`,但不管注冊的順序,總在 `after_create` 和 `after_update` 之后執行。
#### 3.4 `after_initialize` 和 `after_find`
`after_initialize` 回調在 Active Record 對象初始化時執行,包括直接使用 `new` 方法初始化和從數據庫中讀取記錄。`after_initialize` 回調不用直接重定義 Active Record 的 `initialize` 方法。
`after_find` 回調在從數據庫中讀取記錄時執行。如果同時注冊了 `after_find` 和 `after_initialize` 回調,`after_find` 會先執行。
`after_initialize` 和 `after_find` 沒有對應的 `before_*` 回調,但可以像其他回調一樣注冊。
```
class User < ActiveRecord::Base
after_initialize do |user|
puts "You have initialized an object!"
end
after_find do |user|
puts "You have found an object!"
end
end
>> User.new
You have initialized an object!
=> #<User id: nil>
>> User.first
You have found an object!
You have initialized an object!
=> #<User id: 1>
```
#### 3.5 `after_touch`
`after_touch` 回調在觸碰 Active Record 對象時執行。
```
class User < ActiveRecord::Base
after_touch do |user|
puts "You have touched an object"
end
end
>> u = User.create(name: 'Kuldeep')
=> #<User id: 1, name: "Kuldeep", created_at: "2013-11-25 12:17:49", updated_at: "2013-11-25 12:17:49">
>> u.touch
You have touched an object
=> true
```
可以結合 `belongs_to` 一起使用:
```
class Employee < ActiveRecord::Base
belongs_to :company, touch: true
after_touch do
puts 'An Employee was touched'
end
end
class Company < ActiveRecord::Base
has_many :employees
after_touch :log_when_employees_or_company_touched
private
def log_when_employees_or_company_touched
puts 'Employee/Company was touched'
end
end
>> @employee = Employee.last
=> #<Employee id: 1, company_id: 1, created_at: "2013-11-25 17:04:22", updated_at: "2013-11-25 17:05:05">
# triggers @employee.company.touch
>> @employee.touch
Employee/Company was touched
An Employee was touched
=> true
```
### 4 執行回調
下面的方法會觸發執行回調:
* `create`
* `create!`
* `decrement!`
* `destroy`
* `destroy!`
* `destroy_all`
* `increment!`
* `save`
* `save!`
* `save(validate: false)`
* `toggle!`
* `update_attribute`
* `update`
* `update!`
* `valid?`
`after_find` 回調由以下查詢方法觸發執行:
* `all`
* `first`
* `find`
* `find_by`
* `find_by_*`
* `find_by_*!`
* `find_by_sql`
* `last`
`after_initialize` 回調在新對象初始化時觸發執行。
`find_by_*` 和 `find_by_*!` 是為每個屬性生成的動態查詢方法,詳情參見“[動態查詢方法](active_record_querying.html#dynamic-finders)”一節。
### 5 跳過回調
和數據驗證一樣,回調也可跳過,使用下列方法即可:
* `decrement`
* `decrement_counter`
* `delete`
* `delete_all`
* `increment`
* `increment_counter`
* `toggle`
* `touch`
* `update_column`
* `update_columns`
* `update_all`
* `update_counters`
使用這些方法是要特別留心,因為重要的業務邏輯可能在回調中完成。如果沒弄懂回調的作用直接跳過,可能導致數據不合法。
### 6 終止執行
在模型中注冊回調后,回調會加入一個執行隊列。這個隊列中包含模型的數據驗證,注冊的回調,以及要執行的數據庫操作。
整個回調鏈包含在一個事務中。如果任何一個 `before_*` 回調方法返回 `false` 或拋出異常,整個回調鏈都會終止執行,撤銷事務;而 `after_*` 回調只有拋出異常才能達到相同的效果。
`ActiveRecord::Rollback` 之外的異常在回調鏈終止之后,還會由 Rails 再次拋出。拋出 `ActiveRecord::Rollback` 之外的異常,可能導致不應該拋出異常的方法(例如 `save` 和 `update_attributes`,應該返回 `true` 或 `false`)無法執行。
### 7 關聯回調
回調能在模型關聯中使用,甚至可由關聯定義。假如一個用戶發布了多篇文章,如果用戶刪除了,他發布的文章也應該刪除。下面我們在 `Post` 模型中注冊一個 `after_destroy` 回調,應用到 `User` 模型上:
```
class User < ActiveRecord::Base
has_many :posts, dependent: :destroy
end
class Post < ActiveRecord::Base
after_destroy :log_destroy_action
def log_destroy_action
puts 'Post destroyed'
end
end
>> user = User.first
=> #<User id: 1>
>> user.posts.create!
=> #<Post id: 1, user_id: 1>
>> user.destroy
Post destroyed
=> #<User id: 1>
```
### 8 條件回調
和數據驗證類似,也可以在滿足指定條件時再調用回調方法。條件通過 `:if` 和 `:unless` 選項指定,選項的值可以是 Symbol、字符串、`Proc` 或數組。`:if` 選項指定什么時候調用回調。如果要指定何時不調用回調,使用 `:unless` 選項。
#### 8.1 使用 Symbol
:if 和 :unless 選項的值為 Symbol 時,表示要在調用回調之前執行對應的判斷方法。使用 `:if` 選項時,如果判斷方法返回 `false`,就不會調用回調;使用 `:unless` 選項時,如果判斷方法返回 `true`,就不會調用回調。Symbol 是最常用的設置方式。使用這種方式注冊回調時,可以使用多個判斷方法檢查是否要調用回調。
```
class Order < ActiveRecord::Base
before_save :normalize_card_number, if: :paid_with_card?
end
```
#### 8.2 使用字符串
`:if` 和 `:unless` 選項的值還可以是字符串,但必須是 RUby 代碼,傳入 `eval` 方法中執行。當字符串表示的條件非常短時才應該是使用這種形式。
```
class Order < ActiveRecord::Base
before_save :normalize_card_number, if: "paid_with_card?"
end
```
#### 8.3 使用 Proc
`:if` 和 `:unless` 選項的值還可以是 Proc 對象。這種形式最適合用在一行代碼能表示的條件上。
```
class Order < ActiveRecord::Base
before_save :normalize_card_number,
if: Proc.new { |order| order.paid_with_card? }
end
```
#### 8.4 回調的多重條件
注冊條件回調時,可以同時使用 `:if` 和 `:unless` 選項:
```
class Comment < ActiveRecord::Base
after_create :send_email_to_author, if: :author_wants_emails?,
unless: Proc.new { |comment| comment.post.ignore_comments? }
end
```
### 9 回調類
有時回調方法可以在其他模型中重用,我們可以將其封裝在類中。
在下面這個例子中,我們為 `PictureFile` 模型定義了一個 `after_destroy` 回調:
```
class PictureFileCallbacks
def after_destroy(picture_file)
if File.exist?(picture_file.filepath)
File.delete(picture_file.filepath)
end
end
end
```
在類中定義回調方法時(如上),可把模型對象作為參數傳入。然后可以在模型中使用這個回調:
```
class PictureFile < ActiveRecord::Base
after_destroy PictureFileCallbacks.new
end
```
注意,因為回調方法被定義成實例方法,所以要實例化 `PictureFileCallbacks`。如果回調要使用實例化對象的狀態,使用這種定義方式很有用。不過,一般情況下,定義為類方法更說得通:
```
class PictureFileCallbacks
def self.after_destroy(picture_file)
if File.exist?(picture_file.filepath)
File.delete(picture_file.filepath)
end
end
end
```
如果按照這種方式定義回調方法,就不用實例化 `PictureFileCallbacks`:
```
class PictureFile < ActiveRecord::Base
after_destroy PictureFileCallbacks
end
```
在回調類中可以定義任意數量的回調方法。
### 10 事務回調
還有兩個回調會在數據庫事務完成時觸發:`after_commit` 和 `after_rollback`。這兩個回調和 `after_save` 很像,只不過在數據庫操作提交或回滾之前不會執行。如果模型要和數據庫事務之外的系統交互,就可以使用這兩個回調。
例如,在前面的例子中,`PictureFile` 模型中的記錄刪除后,還要刪除相應的文件。如果執行 `after_destroy` 回調之后程序拋出了異常,事務就會回滾,文件會被刪除,但模型的狀態前后不一致。假設在下面的代碼中,`picture_file_2` 是不合法的,那么調用 `save!` 方法會拋出異常。
```
PictureFile.transaction do
picture_file_1.destroy
picture_file_2.save!
end
```
使用 `after_commit` 回調可以解決這個問題。
```
class PictureFile < ActiveRecord::Base
after_commit :delete_picture_file_from_disk, on: [:destroy]
def delete_picture_file_from_disk
if File.exist?(filepath)
File.delete(filepath)
end
end
end
```
`:on` 選項指定什么時候出發回調。如果不設置 `:on` 選項,每各個操作都會觸發回調。
`after_commit` 和 `after_rollback` 回調確保模型的創建、更新和銷毀等操作在事務中完成。如果這兩個回調拋出了異常,會被忽略,因此不會干擾其他回調。因此,如果回調可能拋出異常,就要做適當的補救和處理。
### 反饋
歡迎幫忙改善指南質量。
如發現任何錯誤,歡迎修正。開始貢獻前,可先行閱讀[貢獻指南:文檔](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#contributing-to-the-rails-documentation)。
翻譯如有錯誤,深感抱歉,歡迎 [Fork](https://github.com/ruby-china/guides/fork) 修正,或至此處[回報](https://github.com/ruby-china/guides/issues/new)。
文章可能有未完成或過時的內容。請先檢查 [Edge Guides](http://edgeguides.rubyonrails.org) 來確定問題在 master 是否已經修掉了。再上 master 補上缺少的文件。內容參考 [Ruby on Rails 指南準則](ruby_on_rails_guides_guidelines.html)來了解行文風格。
最后,任何關于 Ruby on Rails 文檔的討論,歡迎到 [rubyonrails-docs 郵件群組](http://groups.google.com/group/rubyonrails-docs)。
- Ruby on Rails 指南 (651bba1)
- 入門
- Rails 入門
- 模型
- Active Record 基礎
- Active Record 數據庫遷移
- Active Record 數據驗證
- Active Record 回調
- Active Record 關聯
- Active Record 查詢
- 視圖
- Action View 基礎
- Rails 布局和視圖渲染
- 表單幫助方法
- 控制器
- Action Controller 簡介
- Rails 路由全解
- 深入
- Active Support 核心擴展
- Rails 國際化 API
- Action Mailer 基礎
- Active Job 基礎
- Rails 程序測試指南
- Rails 安全指南
- 調試 Rails 程序
- 設置 Rails 程序
- Rails 命令行
- Rails 緩存簡介
- Asset Pipeline
- 在 Rails 中使用 JavaScript
- 引擎入門
- Rails 應用的初始化過程
- Autoloading and Reloading Constants
- 擴展 Rails
- Rails 插件入門
- Rails on Rack
- 個性化Rails生成器與模板
- Rails應用模版
- 貢獻 Ruby on Rails
- Contributing to Ruby on Rails
- API Documentation Guidelines
- Ruby on Rails Guides Guidelines
- Ruby on Rails 維護方針
- 發布記
- A Guide for Upgrading Ruby on Rails
- Ruby on Rails 4.2 發布記
- Ruby on Rails 4.1 發布記
- Ruby on Rails 4.0 Release Notes
- Ruby on Rails 3.2 Release Notes
- Ruby on Rails 3.1 Release Notes
- Ruby on Rails 3.0 Release Notes
- Ruby on Rails 2.3 Release Notes
- Ruby on Rails 2.2 Release Notes