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
将回调方法声明为私有被认为是最佳实践。如果将其保留为公有,则可以从模型外部调用它们,从而违反了对象封装的原则。
避免使用 update
、save
或任何其他会在回调方法中导致对象副作用的方法。
例如,避免在回调中调用 update(attribute: "value")
。这种做法可能会修改模型的状态,并在提交期间可能导致无法预料的副作用。
相反,您可以使用更安全的方法,在 before_create
、before_update
或更早的回调中直接分配值(例如,self.attribute = "value"
)。
3 可用回调
以下是所有可用 Active Record 回调的列表,按在相应操作期间**它们被调用的顺序**排列。
3.1 创建对象
before_validation
after_validation
before_save
around_save
before_create
around_create
after_create
after_save
after_commit
/after_rollback
有关使用这两个回调的示例,请参阅after_commit
/ after_rollback
部分。
以下是一些示例,展示了如何使用这些回调。我们已按它们关联的操作进行了分组,最后展示了它们如何组合使用。
3.1.1 验证回调
验证回调在每次通过 valid?
(或其别名 validate
)或 invalid?
方法直接验证记录时触发,或者通过 create
、update
或 save
间接触发。它们在验证阶段之前和之后被调用。
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 保存回调
保存回调在每次将记录持久化(即“保存”)到底层数据库时触发,通过 create
、update
或 save
方法触发。它们在对象保存之前、之后和周围被调用。
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 创建回调
创建回调在每次将记录**首次**持久化(即“保存”)到底层数据库时触发 - 换句话说,当我们保存新记录时,通过 create
或 save
方法触发。它们在对象创建之前、之后和周围被调用。
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 更新对象
更新回调在每次将**现有**记录持久化(即“保存”)到底层数据库时触发。它们在对象更新之前、之后和周围被调用。
before_validation
after_validation
before_save
around_save
before_update
around_update
after_update
after_save
after_commit
/after_rollback
after_save
回调在创建和更新操作中都会触发。但是,它始终在更具体的回调 after_create
和 after_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_create
和 after_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_initialize
和 after_find
每当使用 new
直接实例化 Active Record 对象或从数据库加载记录时,都会调用 after_initialize
回调。它有助于避免直接覆盖 Active Record 的 initialize
方法。
从数据库加载记录时,将调用 after_find
回调。如果同时定义了 after_find
和 after_initialize
,则 after_find
会在 after_initialize
之前调用。
after_initialize
和 after_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 使用符号与 :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
和 :unless
与 Proc
对象关联。此选项最适合编写简短的验证方法,通常是一行代码。
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 跳过回调
与 验证 一样,也可以使用以下方法跳过回调
decrement!
decrement_counter
delete
delete_all
delete_by
increment!
increment_counter
insert
insert!
insert_all
insert_all!
touch_all
update_column
update_columns
update_all
update_counters
upsert
upsert_all
让我们考虑一个 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"
这意外地破坏了不希望 create
和 save
等方法引发异常的代码。
如果在回调链期间发生异常,Rails 将重新引发它,除非它是 ActiveRecord::Rollback
或 ActiveRecord::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_commit
和 after_rollback
数据库事务完成后,将触发两个额外的回调:after_commit
和 after_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_commit
或 after_rollback
回调。但是,如果在这些回调中的一个中引发了异常,则该异常将冒泡,并且任何剩余的 after_commit
或 after_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_save
、after_update
和 after_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_commit
或 after_rollback
回调中执行的代码本身并不包含在事务中。
在单个事务的上下文中,如果您在数据库中表示同一记录,那么在 after_commit
和 after_rollback
回调中需要注意一个关键的行为。这些回调仅针对事务中更改的特定记录的第一个对象触发。其他加载的对象,尽管代表相同的数据库记录,但不会触发各自的 after_commit
或 after_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
回调很常见。有时您可能还想对 create
和 update
都使用单个回调。以下是这些操作的一些常见别名
让我们来看一些例子
与其使用 after_commit
和 on
选项来销毁,如下所示
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_commit
和 after_update_commit
也是如此。
但是,如果您使用 after_create_commit
和 after_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_commit
、after_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
您可以在回调对象中声明任意数量的回调。