更多内容请访问 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: "[email protected]", password: "abc123456")
=> #<User id: nil, email: "[email protected]", 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: "[email protected]")

Password hashed for user with email: [email protected]
Saving user with email: [email protected]
User saved with email: [email protected]
Update Cache
=> #<User id: 1, email: "[email protected]", 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: "[email protected]")

User role set to default: user
Creating user with email: [email protected]
User created with email: [email protected]
User welcome email sent to: [email protected]
=> #<User id: 10, email: "[email protected]", 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 在 create/update 之前运行,around_save 在 create/update 操作周围运行。重要的是要注意,save 回调始终在更具体的 create/update 回调之前/周围/之后运行。

我们已经介绍了验证保存回调。有关使用这两个回调的示例,请参阅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: "[email protected]", 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: [email protected]
User updated with email: [email protected]
Update email sent to: [email protected]

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: "[email protected]")
Confirmation email sent to: [email protected]
=> #<User id: 1, email: "[email protected]", ...>

irb> user.update(email: "[email protected]")
Notification sent to admin about critical info update for: [email protected]
=> 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: "[email protected]", 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

每当使用 new 直接实例化 Active Record 对象或从数据库加载记录时,都会调用 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 选项来实现,这些选项可以接受符号、ProcArray

当您希望指定回调应该调用的条件时,可以使用 :if 选项。如果您希望指定回调不应该调用的条件,则可以使用 :unless 选项。

5.1 使用符号与 :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 条件都评估为 true 且没有 :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: '[email protected]')

以上代码将更新用户的电子邮件地址,而不会触发 before_save 回调。

应谨慎使用这些方法,因为回调中可能包含重要的业务规则和应用程序逻辑,您不希望绕过它们。在不了解潜在影响的情况下绕过它们可能会导致数据无效。

7 抑制回调

在某些情况下,您可能需要暂时阻止 Rails 应用程序中执行某些回调。当您希望在某些操作期间跳过特定操作(而不永久禁用回调)时,这很有用。

Rails 提供了一种使用 ActiveRecord::Suppressor 模块 抑制回调的机制。通过使用此模块,您可以将要抑制回调的代码块包装起来,以确保在该特定操作期间不执行它们。

让我们考虑一个场景,我们有一个 User 模型,其中有一个回调,它在用户注册后向新用户发送欢迎电子邮件。但是,可能存在我们希望创建用户而无需发送欢迎电子邮件的情况,例如在使用测试数据填充数据库时。

class User < ApplicationRecord
  after_create :send_welcome_email

  def send_welcome_email
    puts "Welcome email sent to #{self.email}"
  end
end

在此示例中,after_create 回调会在每次创建新用户时触发 send_welcome_email 方法。

要创建用户而不发送欢迎电子邮件,可以使用 ActiveRecord::Suppressor 模块,如下所示

User.suppress do
  User.create(name: "Jane", email: "[email protected]")
end

在上面的代码中,User.suppress 块确保在创建 "Jane" 用户时不执行 send_welcome_email 回调,使我们能够创建用户而不发送欢迎电子邮件。

使用 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_commitafter_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 都使用单个回调。以下是这些操作的一些常见别名

让我们来看一些例子

与其使用 after_commiton 选项来销毁,如下所示

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

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 对象,因为我们将回调声明为实例方法。如果回调利用了实例化对象的 state,这将特别有用。但是,通常情况下,将回调声明为类方法更有意义

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

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



返回顶部