更多内容请访问 rubyonrails.org:

Active Record 回调

本指南教你如何介入 Active Record 对象的生命周期。

阅读本指南后,您将了解

  • 在 Active Record 对象的生命周期中发生某些事件时。
  • 如何注册、运行和跳过响应这些事件的回调。
  • 如何创建关系型、关联、条件和事务回调。
  • 如何创建封装通用行为以供回调重用的对象。

1. 对象生命周期

在 Rails 应用程序的正常运行中,对象可能会被创建、更新和销毁。Active Record 提供了介入此对象生命周期的钩子,以便你可以控制应用程序及其数据。

回调允许你在对象状态更改之前或之后触发逻辑。它们是在对象生命周期的特定时刻被调用的方法。通过回调,可以编写在 Active Record 对象初始化、创建、保存、更新、删除、验证或从数据库加载时运行的代码。

class BirthdayCake < ApplicationRecord
  after_create -> { Rails.logger.info("Congratulations, the callback has run!") }
end
irb> BirthdayCake.create
Congratulations, the callback has run!

如你所见,有许多生命周期事件和多种介入方式 — 可以在它们之前、之后甚至围绕它们。

2. 回调注册

要使用可用的回调,你需要实现并注册它们。实现可以通过多种方式完成,例如使用普通方法、块和 proc,或者使用类或模块定义自定义回调对象。让我们逐一介绍这些实现技术。

你可以通过调用普通方法来实现的宏风格类方法注册回调。

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

  before_validation :ensure_username_has_value

  private
    def ensure_username_has_value
      if username.blank?
        self.username = email
      end
    end
end

宏风格类方法也可以接收一个块。如果块中的代码很短,只有一行,则考虑使用这种风格

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

  before_validation do
    self.username = email if username.blank?
  end
end

或者,你可以向回调传递一个 proc 以触发。

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

  before_validation ->(user) { user.username = user.email if user.username.blank? }
end

最后,你可以定义一个自定义回调对象,如下所示。我们将在稍后详细介绍。

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

  before_validation AddUsername
end

class AddUsername
  def self.before_validation(record)
    if record.username.blank?
      record.username = record.email
    end
  end
end

2.1. 注册回调以在生命周期事件时触发

回调也可以注册为仅在某些生命周期事件时触发,这可以通过使用 :on 选项完成,并允许完全控制回调何时以及在何种上下文中触发。

上下文就像一个类别或场景,你希望某些验证适用于其中。当你验证 ActiveRecord 模型时,可以指定一个上下文来分组验证。这允许你在不同情况下应用不同的验证集。在 Rails 中,验证有一些默认上下文,例如 :create、:update 和 :save。

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

  before_validation :ensure_username_has_value, on: :create

  # :on takes an array as well
  after_validation :set_location, on: [ :create, :update ]

  private
    def ensure_username_has_value
      if username.blank?
        self.username = email
      end
    end

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

将回调方法声明为私有是一种好做法。如果保持公共,它们可以从模型外部调用,并违反对象封装原则。

避免在回调方法中使用 updatesave 或任何其他对对象造成副作用的方法。

例如,避免在回调中调用 update(attribute: "value")。这种做法可能会修改模型的_状态_,并可能在提交期间导致不可预见的副作用。

相反,你可以在 before_createbefore_update 或更早的回调中直接赋值(例如,self.attribute = "value"),以采取更安全的方法。

3. 可用回调

这是所有可用的 Active Record 回调列表,按它们在相应操作期间被调用的顺序排列

3.1. 创建对象

有关使用这两个回调的示例,请参见after_commit / after_rollback 部分

下面有一些示例展示了如何使用这些回调。我们根据它们关联的操作进行分组,最后展示它们如何组合使用。

3.1.1. 验证回调

验证回调在记录通过valid?(或其别名validate)或invalid?方法直接验证时,或通过 createupdatesave 间接验证时触发。它们在验证阶段之前和之后调用。

class User < ApplicationRecord
  validates :name, presence: true
  before_validation :titleize_name
  after_validation :log_errors

  private
    def titleize_name
      self.name = name.downcase.titleize if name.present?
      Rails.logger.info("Name titleized to #{name}")
    end

    def log_errors
      if errors.any?
        Rails.logger.error("Validation failed: #{errors.full_messages.join(', ')}")
      end
    end
end
irb> user = User.new(name: "", email: "john.doe@example.com", password: "abc123456")
=> #<User id: nil, email: "john.doe@example.com", created_at: nil, updated_at: nil, name: "">

irb> user.valid?
Name titleized to
Validation failed: Name can't be blank
=> false

3.1.2. 保存回调

保存回调在记录通过 createupdatesave 方法持久化(即“保存”)到底层数据库时触发。它们在对象保存之前、之后和围绕对象保存时调用。

class User < ApplicationRecord
  before_save :hash_password
  around_save :log_saving
  after_save :update_cache

  private
    def hash_password
      self.password_digest = BCrypt::Password.create(password)
      Rails.logger.info("Password hashed for user with email: #{email}")
    end

    def log_saving
      Rails.logger.info("Saving user with email: #{email}")
      yield
      Rails.logger.info("User saved with email: #{email}")
    end

    def update_cache
      Rails.cache.write(["user_data", self], attributes)
      Rails.logger.info("Update Cache")
    end
end
irb> user = User.create(name: "Jane Doe", password: "password", email: "jane.doe@example.com")

Password hashed for user with email: jane.doe@example.com
Saving user with email: jane.doe@example.com
User saved with email: jane.doe@example.com
Update Cache
=> #<User id: 1, email: "jane.doe@example.com", created_at: "2024-03-20 16:02:43.685500000 +0000", updated_at: "2024-03-20 16:02:43.685500000 +0000", name: "Jane Doe">

3.1.3. 创建回调

创建回调在记录首次持久化(即“保存”)到底层数据库时触发 — 换句话说,当我们通过 createsave 方法保存一个新记录时。它们在对象创建之前、之后和围绕对象创建时调用。

class User < ApplicationRecord
  before_create :set_default_role
  around_create :log_creation
  after_create :send_welcome_email

  private
    def set_default_role
      self.role = "user"
      Rails.logger.info("User role set to default: user")
    end

    def log_creation
      Rails.logger.info("Creating user with email: #{email}")
      yield
      Rails.logger.info("User created with email: #{email}")
    end

    def send_welcome_email
      UserMailer.welcome_email(self).deliver_later
      Rails.logger.info("User welcome email sent to: #{email}")
    end
end
irb> user = User.create(name: "John Doe", email: "john.doe@example.com")

User role set to default: user
Creating user with email: john.doe@example.com
User created with email: john.doe@example.com
User welcome email sent to: john.doe@example.com
=> #<User id: 10, email: "john.doe@example.com", created_at: "2024-03-20 16:19:52.405195000 +0000", updated_at: "2024-03-20 16:19:52.405195000 +0000", name: "John Doe">

3.2. 更新对象

更新回调在现有记录持久化(即“保存”)到底层数据库时触发。它们在对象更新之前、之后和围绕对象更新时调用。

after_save 回调在创建和更新操作时都会触发。然而,它始终在更具体的 after_createafter_update 回调之后执行,无论宏调用的顺序如何。同样,before 和 around save 回调也遵循相同的规则:before_save 在创建/更新之前运行,around_save 在创建/更新操作周围运行。需要注意的是,保存回调将始终在更具体的创建/更新回调之前/围绕/之后运行。

我们已经讨论了验证保存回调。有关使用这两个回调的示例,请参见after_commit / after_rollback 部分

3.2.1. 更新回调

class User < ApplicationRecord
  before_update :check_role_change
  around_update :log_updating
  after_update :send_update_email

  private
    def check_role_change
      if role_changed?
        Rails.logger.info("User role changed to #{role}")
      end
    end

    def log_updating
      Rails.logger.info("Updating user with email: #{email}")
      yield
      Rails.logger.info("User updated with email: #{email}")
    end

    def send_update_email
      UserMailer.update_email(self).deliver_later
      Rails.logger.info("Update email sent to: #{email}")
    end
end
irb> user = User.find(1)
=> #<User id: 1, email: "john.doe@example.com", created_at: "2024-03-20 16:19:52.405195000 +0000", updated_at: "2024-03-20 16:19:52.405195000 +0000", name: "John Doe", role: "user" >

irb> user.update(role: "admin")
User role changed to admin
Updating user with email: john.doe@example.com
User updated with email: john.doe@example.com
Update email sent to: john.doe@example.com

3.2.2. 使用回调组合

通常,你需要使用回调组合来实现所需行为。例如,你可能希望在创建用户后发送确认邮件,但仅当用户是新用户且未被更新时。当用户更新时,如果关键信息发生更改,你可能希望通知管理员。在这种情况下,你可以同时使用 after_createafter_update 回调。

class User < ApplicationRecord
  after_create :send_confirmation_email
  after_update :notify_admin_if_critical_info_updated

  private
    def send_confirmation_email
      UserMailer.confirmation_email(self).deliver_later
      Rails.logger.info("Confirmation email sent to: #{email}")
    end

    def notify_admin_if_critical_info_updated
      if saved_change_to_email? || saved_change_to_phone_number?
        AdminMailer.user_critical_info_updated(self).deliver_later
        Rails.logger.info("Notification sent to admin about critical info update for: #{email}")
      end
    end
end
irb> user = User.create(name: "John Doe", email: "john.doe@example.com")
Confirmation email sent to: john.doe@example.com
=> #<User id: 1, email: "john.doe@example.com", ...>

irb> user.update(email: "john.doe.new@example.com")
Notification sent to admin about critical info update for: john.doe.new@example.com
=> true

3.3. 销毁对象

销毁回调在记录被销毁时触发,但在记录被删除时忽略。它们在对象销毁之前、之后和围绕对象销毁时调用。

查找使用 after_commit / after_rollback 的示例

3.3.1. 销毁回调

class User < ApplicationRecord
  before_destroy :check_admin_count
  around_destroy :log_destroy_operation
  after_destroy :notify_users

  private
    def check_admin_count
      if admin? && User.where(role: "admin").count == 1
        throw :abort
      end
      Rails.logger.info("Checked the admin count")
    end

    def log_destroy_operation
      Rails.logger.info("About to destroy user with ID #{id}")
      yield
      Rails.logger.info("User with ID #{id} destroyed successfully")
    end

    def notify_users
      UserMailer.deletion_email(self).deliver_later
      Rails.logger.info("Notification sent to other users about user deletion")
    end
end
irb> user = User.find(1)
=> #<User id: 1, email: "john.doe@example.com", created_at: "2024-03-20 16:19:52.405195000 +0000", updated_at: "2024-03-20 16:19:52.405195000 +0000", name: "John Doe", role: "admin">

irb> user.destroy
Checked the admin count
About to destroy user with ID 1
User with ID 1 destroyed successfully
Notification sent to other users about user deletion

3.4. after_initializeafter_find

每当 Active Record 对象实例化时,无论是直接使用 new 还是从数据库加载记录时,都会调用 after_initialize 回调。这对于避免直接覆盖 Active Record 的 initialize 方法很有用。

当从数据库加载记录时,会调用 after_find 回调。如果同时定义了 after_findafter_initialize,则 after_find 会在 after_initialize 之前调用。

after_initializeafter_find 回调没有 before_* 对应项。

它们可以像其他 Active Record 回调一样注册。

class User < ApplicationRecord
  after_initialize do |user|
    Rails.logger.info("You have initialized an object!")
  end

  after_find do |user|
    Rails.logger.info("You have found an object!")
  end
end
irb> User.new
You have initialized an object!
=> #<User id: nil>

irb> User.first
You have found an object!
You have initialized an object!
=> #<User id: 1>

3.5. after_touch

每当 Active Record 对象被触摸时,都会调用 after_touch 回调。你可以在API 文档中阅读有关 touch 的更多信息

class User < ApplicationRecord
  after_touch do |user|
    Rails.logger.info("You have touched an object")
  end
end
irb> user = 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">

irb> user.touch
You have touched an object
=> true

它可以与 belongs_to 一起使用

class Book < ApplicationRecord
  belongs_to :library, touch: true
  after_touch do
    Rails.logger.info("A Book was touched")
  end
end

class Library < ApplicationRecord
  has_many :books
  after_touch :log_when_books_or_library_touched

  private
    def log_when_books_or_library_touched
      Rails.logger.info("Book/Library was touched")
    end
end
irb> book = Book.last
=> #<Book id: 1, library_id: 1, created_at: "2013-11-25 17:04:22", updated_at: "2013-11-25 17:05:05">

irb> book.touch # triggers book.library.touch
A Book was touched
Book/Library was touched
=> true

4. 运行回调

以下方法会触发回调

  • create
  • create!
  • destroy
  • destroy!
  • destroy_all
  • destroy_by
  • save
  • save!
  • save(validate: false)
  • save!(validate: false)
  • toggle!
  • touch
  • update_attribute
  • update_attribute!
  • update
  • update!
  • valid?
  • validate

此外,after_find 回调由以下查找方法触发

  • all
  • first
  • find
  • find_by
  • find_by!
  • find_by_*
  • find_by_*!
  • find_by_sql
  • last
  • sole
  • take

每当类的_新对象_初始化时,都会触发 after_initialize 回调。

find_by_*find_by_*! 方法是为每个属性自动生成的动态查找器。在动态查找器部分了解更多信息。

5. 条件回调

验证一样,我们也可以根据给定谓词的满足情况来使回调方法的调用成为条件。我们可以使用 :if:unless 选项来实现,它们可以接受一个符号、一个 Proc 或一个 Array

当你想要指定回调应该在什么条件下被调用时,可以使用 :if 选项。如果你想要指定回调不应该在什么条件下被调用时,那么可以使用 :unless 选项。

5.1. 结合 Symbol 使用 :if:unless

你可以将 :if:unless 选项与一个符号关联,该符号对应于在回调之前调用的谓词方法的名称。

当使用 :if 选项时,如果谓词方法返回 false,则回调不会执行;当使用 :unless 选项时,如果谓词方法返回 true,则回调不会执行。这是最常见的选项。

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

使用这种注册形式,还可以注册几个不同的谓词,这些谓词应被调用以检查回调是否应执行。我们将在多个回调条件部分中介绍这一点。

5.2. 结合 Proc 使用 :if:unless

可以将 :if:unlessProc 对象关联。此选项最适合编写简短的验证方法,通常是单行代码。

class Order < ApplicationRecord
  before_save :normalize_card_number,
    if: ->(order) { order.paid_with_card? }
end

由于 proc 在对象的上下文中求值,因此也可以将其写成

class Order < ApplicationRecord
  before_save :normalize_card_number, if: -> { paid_with_card? }
end

5.3. 多个回调条件

:if:unless 选项还接受一个 proc 数组或作为符号的方法名

class Comment < ApplicationRecord
  before_save :filter_content,
    if: [:subject_to_parental_control?, :untrusted_author?]
end

你可以轻松地在条件列表中包含一个 proc

class Comment < ApplicationRecord
  before_save :filter_content,
    if: [:subject_to_parental_control?, -> { untrusted_author? }]
end

5.4. 同时使用 :if:unless

回调可以在同一个声明中同时使用 :if:unless

class Comment < ApplicationRecord
  before_save :filter_content,
    if: -> { forum.parental_control? },
    unless: -> { author.trusted? }
end

只有当所有 :if 条件和所有 :unless 条件均评估为 true 时,回调才会运行。

6. 跳过回调

验证一样,也可以使用以下方法跳过回调

让我们考虑一个 User 模型,其中 before_save 回调记录用户电子邮件地址的任何更改

class User < ApplicationRecord
  before_save :log_email_change

  private
    def log_email_change
      if email_changed?
        Rails.logger.info("Email changed from #{email_was} to #{email}")
      end
    end
end

现在,假设有一种情况,你希望更新用户的电子邮件地址而不触发 before_save 回调来记录电子邮件更改。你可以为此目的使用 update_columns 方法

irb> user = User.find(1)
irb> user.update_columns(email: 'new_email@example.com')

上述操作将更新用户的电子邮件地址,而不会触发 before_save 回调。

这些方法应谨慎使用,因为回调中可能存在重要的业务规则和应用程序逻辑,你不想绕过它们。不理解潜在影响而绕过它们可能会导致数据无效。

7. 禁止保存

在某些情况下,你可能需要在回调中暂时阻止记录被保存。如果你有一个具有复杂嵌套关联的记录,并且希望在某些操作期间跳过保存特定记录,而无需永久禁用回调或引入复杂的条件逻辑,这会很有用。

Rails 提供了一种使用 ActiveRecord::Suppressor 模块来防止保存记录的机制。通过使用此模块,你可以包装一段代码,在该代码块中你希望避免保存特定类型的记录,而这些记录否则将被代码块保存。

让我们考虑一个用户有许多通知的场景。创建 User 也会自动创建 Notification 记录。

class User < ApplicationRecord
  has_many :notifications

  after_create :create_welcome_notification

  def create_welcome_notification
    notifications.create(event: "sign_up")
  end
end

class Notification < ApplicationRecord
  belongs_to :user
end

要创建一个不创建通知的用户,我们可以使用 ActiveRecord::Suppressor 模块,如下所示

Notification.suppress do
  User.create(name: "Jane", email: "jane@example.com")
end

在上面的代码中,Notification.suppress 块确保在创建“Jane”用户期间不会保存 Notification

使用 Active Record Suppressor 可能会引入复杂性和意外行为。禁止保存可能会模糊应用程序的预期流程,导致理解和维护代码库变得困难。请仔细考虑使用抑制器的影响,确保彻底的文档和周到的测试,以减轻意外副作用和测试失败的风险。

8. 中止执行

当你开始为模型注册新回调时,它们将被排队执行。此队列将包括模型的所有验证、已注册的回调以及要执行的数据库操作。

整个回调链都包裹在一个事务中。如果任何回调引发异常,执行链将被中止并发出回滚,并且错误将重新引发。

class Product < ActiveRecord::Base
  before_validation do
    raise "Price can't be negative" if total_price < 0
  end
end

Product.create # raises "Price can't be negative"

这会意外地破坏不期望 createsave 等方法引发异常的代码。

如果在回调链中发生异常,Rails 会重新引发它,除非它是 ActiveRecord::RollbackActiveRecord::RecordInvalid 异常。相反,你应该使用 throw :abort 来有意中止链。如果任何回调抛出 :abort,则该过程将被中止,并且 create 将返回 false。

class Product < ActiveRecord::Base
  before_validation do
    throw :abort if total_price < 0
  end
end

Product.create # => false

但是,当调用 create! 时,它将引发 ActiveRecord::RecordNotSaved。此异常表示由于回调中断,记录未保存。

User.create! # => raises an ActiveRecord::RecordNotSaved

当在任何销毁回调中调用 throw :abort 时,destroy 将返回 false

class User < ActiveRecord::Base
  before_destroy do
    throw :abort if still_active?
  end
end

User.first.destroy # => false

但是,当调用 destroy! 时,它将引发 ActiveRecord::RecordNotDestroyed

User.first.destroy! # => raises an ActiveRecord::RecordNotDestroyed

9. 关联回调

关联回调类似于普通回调,但它们由关联集合生命周期中的事件触发。有四个可用的关联回调

  • before_add
  • after_add
  • before_remove
  • after_remove

你可以通过向关联添加选项来定义关联回调。

假设你有一个作者可以拥有许多书籍的示例。但是,在将书籍添加到作者集合之前,你希望确保作者尚未达到其书籍限制。你可以通过添加 before_add 回调来检查限制。

class Author < ApplicationRecord
  has_many :books, before_add: :check_limit

  private
    def check_limit(_book)
      if books.count >= 5
        errors.add(:base, "Cannot add more than 5 books for this author")
        throw(:abort)
      end
    end
end

如果 before_add 回调抛出 :abort,则对象不会添加到集合中。

有时你可能希望对关联对象执行多个操作。在这种情况下,你可以通过将它们作为数组传递来在单个事件上堆叠回调。此外,Rails 会将正在添加或删除的对象传递给回调供你使用。

class Author < ApplicationRecord
  has_many :books, before_add: [:check_limit, :calculate_shipping_charges]

  def check_limit(_book)
    if books.count >= 5
      errors.add(:base, "Cannot add more than 5 books for this author")
      throw(:abort)
    end
  end

  def calculate_shipping_charges(book)
    weight_in_pounds = book.weight_in_pounds || 1
    shipping_charges = weight_in_pounds * 2

    shipping_charges
  end
end

同样,如果 before_remove 回调抛出 :abort,则对象不会从集合中移除。

这些回调仅在通过关联集合添加或删除关联对象时调用。

# Triggers `before_add` callback
author.books << book
author.books = [book, book2]

# Does not trigger the `before_add` callback
book.update(author_id: 1)

10. 级联关联回调

当关联对象更改时,可以执行回调。它们通过模型关联工作,生命周期事件可以在关联上级联并触发回调。

假设一个用户拥有多篇文章的示例。如果用户被销毁,用户的文章也应该被销毁。让我们通过 User 模型与 Article 模型的关联,为 User 模型添加一个 after_destroy 回调

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

class Article < ApplicationRecord
  after_destroy :log_destroy_action

  def log_destroy_action
    Rails.logger.info("Article destroyed")
  end
end
irb> user = User.first
=> #<User id: 1>
irb> user.articles.create!
=> #<Article id: 1, user_id: 1>
irb> user.destroy
Article destroyed
=> #<User id: 1>

使用 before_destroy 回调时,应将其放置在 dependent: :destroy 关联之前(或使用 prepend: true 选项),以确保它们在记录被 dependent: :destroy 删除之前执行。

11. 事务回调

11.1. after_commitafter_rollback

另外两个回调由数据库事务的完成触发:after_commitafter_rollback。这些回调与 after_save 回调非常相似,不同之处在于它们直到数据库更改提交或回滚后才执行。当 Active Record 模型需要与不属于数据库事务的外部系统交互时,它们最有用。

考虑一个 PictureFile 模型,它需要在相应记录销毁后删除文件。

class PictureFile < ApplicationRecord
  after_destroy :delete_picture_file_from_disk

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

如果在调用 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 选项,回调将在每个生命周期事件时触发。阅读更多关于 :on 的信息

当事务完成时,将为在该事务中创建、更新或销毁的所有模型调用 after_commitafter_rollback 回调。但是,如果其中一个回调中引发异常,则该异常将向上冒泡,并且任何剩余的 after_commitafter_rollback 方法将不会执行。

class User < ActiveRecord::Base
  after_commit { raise "Intentional Error" }
  after_commit {
    # This won't get called because the previous after_commit raises an exception
    Rails.logger.info("This will not be logged")
  }
end

如果你的回调代码引发异常,你需要捕获它并在回调中处理它,以允许其他回调运行。

after_commit 提供了与 after_saveafter_updateafter_destroy 非常不同的保证。例如,如果 after_save 中发生异常,事务将回滚并且数据不会持久化。

class User < ActiveRecord::Base
  after_save do
    # If this fails the user won't be saved.
    EventLog.create!(event: "user_saved")
  end
end

然而,在 after_commit 期间,数据已经持久化到数据库中,因此任何异常都不会再回滚任何内容。

class User < ActiveRecord::Base
  after_commit do
    # If this fails the user was already saved.
    EventLog.create!(event: "user_saved")
  end
end

after_commitafter_rollback 回调中执行的代码本身不包含在事务中。

在单个事务的上下文中,如果你在数据库中表示同一条记录,则在 after_commitafter_rollback 回调中有一个需要注意的关键行为。这些回调仅在事务中更改的特定记录的第一个对象上触发。其他已加载的对象,尽管代表相同的数据库记录,但其相应的 after_commitafter_rollback 回调不会触发。

class User < ApplicationRecord
  after_commit :log_user_saved_to_db, on: :update

  private
    def log_user_saved_to_db
      Rails.logger.info("User was saved to database")
    end
end
irb> user = User.create
irb> User.transaction { user.save; user.save }
# User was saved to database

这种细微的行为在预期与相同数据库记录关联的每个对象进行独立回调执行的场景中尤其具有影响力。它可能会影响回调序列的流程和可预测性,导致事务后应用程序逻辑中潜在的不一致性。

11.2. after_commit 的别名

仅在创建、更新或删除时使用 after_commit 回调是很常见的。有时你可能还希望对 createupdate 都使用单个回调。以下是这些操作的一些常见别名

让我们通过一些示例

与下面这样使用带有 on 选项的 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

你可以改用 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_create_commitafter_update_commit 也适用同样的情况。

但是,如果将 after_create_commitafter_update_commit 回调与相同的方法名一起使用,则只允许最后一个定义的回调生效,因为它们都在内部别名为 after_commit,这会覆盖以前使用相同方法名定义的回调。

class User < ApplicationRecord
  after_create_commit :log_user_saved_to_db
  after_update_commit :log_user_saved_to_db

  private
    def log_user_saved_to_db
      # This only gets called once
      Rails.logger.info("User was saved to database")
    end
end
irb> user = User.create # prints nothing

irb> user.save # updating @user
User was saved to database

在这种情况下,最好使用 after_save_commit,它是对创建和更新都使用 after_commit 回调的别名

class User < ApplicationRecord
  after_save_commit :log_user_saved_to_db

  private
    def log_user_saved_to_db
      Rails.logger.info("User was saved to database")
    end
end
irb> user = User.create # creating a User
User was saved to database

irb> user.save # updating user
User was saved to database

11.3. 事务回调顺序

默认情况下(从 Rails 7.1 开始),事务回调将按照它们定义的顺序运行。

class User < ActiveRecord::Base
  after_commit { Rails.logger.info("this gets called first") }
  after_commit { Rails.logger.info("this gets called second") }
end

然而,在 Rails 的早期版本中,当定义多个事务性 after_ 回调(after_commitafter_rollback 等)时,回调的运行顺序是相反的。

如果出于某种原因你仍然希望它们反向运行,可以将以下配置设置为 false。回调将以相反的顺序运行。有关更多详细信息,请参见Active Record 配置选项

config.active_record.run_after_transaction_callbacks_in_order_defined = false

这也适用于所有 after_*_commit 变体,例如 after_destroy_commit

11.4. 每事务回调

你还可以在特定事务上注册事务回调,例如 before_commitafter_commitafter_rollback。这在需要执行不特定于模型而是特定于工作单元的操作的情况下很有用。

ActiveRecord::Base.transaction 产生一个 ActiveRecord::Transaction 对象,允许在其上注册所述回调。

Article.transaction do |transaction|
  article.update(published: true)

  transaction.after_commit do
    PublishNotificationMailer.with(article: article).deliver_later
  end
end

11.5. ActiveRecord.after_all_transactions_commit

ActiveRecord.after_all_transactions_commit 是一个回调,允许你在所有当前事务成功提交到数据库后运行代码。

def publish_article(article)
  Article.transaction do
    Post.transaction do
      ActiveRecord.after_all_transactions_commit do
        PublishNotificationMailer.with(article: article).deliver_later
        # An email will be sent after the outermost transaction is committed.
      end
    end
  end
end

注册到 after_all_transactions_commit 的回调将在最外层事务提交后触发。如果任何当前打开的事务回滚,则该块永远不会被调用。如果注册回调时没有打开的事务,则该块将立即被执行。

12. 回调对象

有时,你编写的回调方法非常有用,可以被其他模型重用。Active Record 允许创建封装回调方法的类,以便它们可以重用。

这是一个 after_commit 回调类的示例,用于处理文件系统上废弃文件的清理。此行为可能并非 PictureFile 模型所独有,我们可能希望共享它,因此将其封装到单独的类中是一个好主意。这将使测试该行为和更改它变得更加容易。

class FileDestroyerCallback
  def after_commit(file)
    if File.exist?(file.filepath)
      File.delete(file.filepath)
    end
  end
end

如上所示,在类中声明时,回调方法将接收模型对象作为参数。这将在使用该类的任何模型上工作,如下所示

class PictureFile < ApplicationRecord
  after_commit FileDestroyerCallback.new
end

请注意,我们需要实例化一个新的 FileDestroyerCallback 对象,因为我们将回调声明为实例方法。如果回调使用实例化对象的状态,这尤其有用。然而,通常将回调声明为类方法更有意义

class FileDestroyerCallback
  def self.after_commit(file)
    if File.exist?(file.filepath)
      File.delete(file.filepath)
    end
  end
end

当回调方法以这种方式声明时,在我们的模型中不需要实例化新的 FileDestroyerCallback 对象。

class PictureFile < ApplicationRecord
  after_commit FileDestroyerCallback
end

你可以在回调对象中声明任意数量的回调。



回到顶部