更多内容 rubyonrails.org: 更多内容

Active Record 回调

本文介绍如何介入 Active Record 对象的生命周期。

读完本文后,您将学到:

1 对象的生命周期

在 Rails 应用正常运作期间,对象可以被创建、更新或删除。Active Record 为对象的生命周期提供了钩子,使我们可以控制应用及其数据。

回调使我们可以在对象状态更改之前或之后触发逻辑。

2 回调概述

回调是在对象生命周期的某些时刻被调用的方法。通过回调,我们可以编写在创建、保存、更新、删除、验证或从数据库中加载 Active Record 对象时执行的代码。

2.1 注册回调

回调在使用之前需要注册。我们可以先把回调定义为普通方法,然后使用宏式类方法把这些普通方法注册为回调:

class User < ApplicationRecord
  validates :login, :email, presence: true

  before_validation :ensure_login_has_a_value

  private
    def ensure_login_has_a_value
      if login.nil?
        self.login = email unless email.blank?
      end
    end
end

宏式类方法也接受块。如果块中的代码短到可以放在一行里,可以考虑使用这种编程风格:

class User < ApplicationRecord
  validates :login, :email, presence: true

  before_create do
    self.name = login.capitalize if name.blank?
  end
end

回调也可以注册为仅被某些生命周期事件触发:

class User < ApplicationRecord
  before_validation :normalize_name, on: :create

  # :on 选项的值也可以是数组
  after_validation :set_location, on: [ :create, :update ]

  private
    def normalize_name
      self.name = name.downcase.titleize
    end

    def set_location
      self.location = LocationService.query(self)
    end
end

通常应该把回调定义为私有方法。如果把回调定义为公共方法,就可以从模型外部调用回调,这样做违反了对象封装原则。

3 可用的回调

下面按照回调在 Rails 应用正常运作期间被调用的顺序,列出所有可用的 Active Record 回调。

3.1 创建对象

  • before_validation
  • after_validation
  • before_save
  • around_save
  • before_create
  • around_create
  • after_create
  • after_save
  • after_commit/after_rollback

3.2 更新对象

  • before_validation
  • after_validation
  • before_save
  • around_save
  • before_update
  • around_update
  • after_update
  • after_save
  • after_commit/after_rollback

3.3 删除对象

  • before_destroy
  • around_destroy
  • after_destroy
  • after_commit/after_rollback

无论按什么顺序注册回调,在创建和更新对象时,after_save 回调总是在更明确的 after_createafter_update 回调之后被调用。

3.4 after_initializeafter_find 回调

当 Active Record 对象被实例化时,不管是通过直接使用 new 方法还是从数据库加载记录,都会调用 after_initialize 回调。使用这个回调可以避免直接覆盖 Active Record 的 initialize 方法。

当 Active Record 从数据库中加载记录时,会调用 after_find 回调。如果同时定义了 after_initializeafter_find 回调,会先调用 after_find 回调。

after_initializeafter_find 回调没有对应的 before_* 回调,这两个回调的注册方式和其他 Active Record 回调一样。

class User < ApplicationRecord
  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 回调

当我们在 Active Record 对象上调用 touch 方法时,会调用 after_touch 回调。

class User < ApplicationRecord
  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

after_touch 回调可以和 belongs_to 一起使用:

class Employee < ApplicationRecord
  belongs_to :company, touch: true
  after_touch do
    puts 'An Employee was touched'
  end
end

class Company < ApplicationRecord
  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_*! 方法是为每个属性自动生成的动态查找方法。关于动态查找方法的更多介绍,请参阅 动态查找方法

5 跳过回调

和验证一样,我们可以跳过回调。使用下面这些方法可以跳过回调:

  • decrement
  • decrement_counter
  • delete
  • delete_all
  • increment
  • increment_counter
  • toggle
  • touch
  • update_column
  • update_columns
  • update_all
  • update_counters

请慎重地使用这些方法,因为有些回调包含了重要的业务规则和应用逻辑,在不了解潜在影响的情况下就跳过回调,可能导致无效数据。

6 停止执行

回调在模型中注册后,将被加入队列等待执行。这个队列包含了所有模型的验证、已注册的回调和将要执行的数据库操作。

整个回调链包装在一个事务中。只要有回调抛出异常,回调链随即停止,并且发出 ROLLBACK 消息。如果想故意停止回调链,可以这么做:

throw :abort

当回调链停止后,Rails 会重新抛出除了 ActiveRecord::RollbackActiveRecord::RecordInvalid 之外的其他异常。这可能导致那些预期 saveupdate_attributes 等方法(通常返回 truefalse )不会引发异常的代码出错。

7 关联回调

回调不仅可以在模型关联中使用,还可以通过模型关联定义。假设有一个用户在博客中发表了多篇文章,现在我们要删除这个用户,那么这个用户的所有文章也应该删除,为此我们通过 Article 模型和 User 模型的关联来给 User 模型添加一个 after_destroy 回调:

class User < ApplicationRecord
  has_many :articles, dependent: :destroy
end

class Article < ApplicationRecord
  after_destroy :log_destroy_action

  def log_destroy_action
    puts 'Article destroyed'
  end
end

>> user = User.first
=> #<User id: 1>
>> user.articles.create!
=> #<Article id: 1, user_id: 1>
>> user.destroy
Article destroyed
=> #<User id: 1>

8 条件回调

和验证一样,我们可以在满足指定条件时再调用回调方法。为此,我们可以使用 :if:unless 选项,选项的值可以是符号、Proc 或数组。要想指定在哪些条件下调用回调,可以使用 :if 选项。要想指定在哪些条件下不调用回调,可以使用 :unless 选项。

8.1 使用符号作为 :if:unless 选项的值

可以使用符号作为 :if:unless 选项的值,这个符号用于表示先于回调调用的断言方法。当使用 :if 选项时,如果断言方法返回 false 就不会调用回调;当使用 :unless 选项时,如果断言方法返回 true 就不会调用回调。使用符号作为 :if:unless 选项的值是最常见的方式。在使用这种方式注册回调时,我们可以同时使用几个不同的断言,用于检查是否应该调用回调。

class Order < ApplicationRecord
  before_save :normalize_card_number, if: :paid_with_card?
end

8.2 使用 Proc 作为 :if:unless 选项的值

最后,可以使用 Proc 作为 :if:unless 选项的值。在验证方法非常短时最适合使用这种方式,这类验证方法通常只有一行代码:

class Order < ApplicationRecord
  before_save :normalize_card_number,
    if: Proc.new { |order| order.paid_with_card? }
end

8.3 在条件回调中使用多个条件

在编写条件回调时,我们可以在同一个回调声明中混合使用 :if:unless 选项:

class Comment < ApplicationRecord
  after_create :send_email_to_author, if: :author_wants_emails?,
    unless: Proc.new { |comment| comment.article.ignore_comments? }
end

9 回调类

有时需要在其他模型中重用已有的回调方法,为了解决这个问题,Active Record 允许我们用类来封装回调方法。有了回调类,回调方法的重用就变得非常容易。

在下面的例子中,我们为 PictureFile 模型创建了 PictureFileCallbacks 回调类,在这个回调类中包含了 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 < ApplicationRecord
  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 < ApplicationRecord
  after_destroy PictureFileCallbacks
end

我们可以根据需要在回调类中声明任意多个回调。

10 事务回调

after_commitafter_rollback 这两个回调会在数据库事务完成时触发。它们和 after_save 回调非常相似,区别在于它们在数据库变更已经提交或回滚后才会执行,常用于 Active Record 模型需要和数据库事务之外的系统交互的场景。

例如,在前面的例子中,PictureFile 模型中的记录删除后,还要删除相应的文件。如果 after_destroy 回调执行后应用引发异常,事务就会回滚,文件会被删除,模型会保持不一致的状态。例如,假设在下面的代码中,picture_file_2 对象是无效的,那么调用 save! 方法会引发错误:

PictureFile.transaction do
  picture_file_1.destroy
  picture_file_2.save!
end

通过使用 after_commit 回调,我们可以解决这个问题:

class PictureFile < ApplicationRecord
  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_create_commit
  • after_update_commit
  • after_destroy_commit
class PictureFile < ApplicationRecord
  after_destroy_commit :delete_picture_file_from_disk

  def delete_picture_file_from_disk
    if File.exist?(filepath)
      File.delete(filepath)
    end
  end
end

在事务中创建、更新或删除模型时会调用 after_commitafter_rollback 回调。然而,如果其中有一个回调引发异常,异常会向上冒泡,后续 after_commitafter_rollback 回调不再执行。因此,如果回调代码可能引发异常,就需要在回调中救援并进行适当处理,以便让其他回调继续运行。

反馈

我们鼓励您帮助提高本指南的质量。

如果看到如何错字或错误,请反馈给我们。 您可以阅读我们的文档贡献指南。

您还可能会发现内容不完整或不是最新版本。 请添加缺失文档到 master 分支。请先确认 Edge Guides 是否已经修复。 关于用语约定,请查看Ruby on Rails 指南指导

无论什么原因,如果你发现了问题但无法修补它,请创建 issue

最后,欢迎到 rubyonrails-docs 邮件列表参与任何有关 Ruby on Rails 文档的讨论。

中文翻译反馈

贡献:https://github.com/ruby-china/guides