Active Record 回调
回调是 Active Record 对象生命周期中的钩子,允许您在对象状态发生变化之前或之后触发逻辑。这可以用来确保关联和依赖对象在调用 ActiveRecord::Base#destroy 时被删除(通过覆盖 before_destroy
),或者在验证之前对属性进行调整(通过覆盖 before_validation
)。例如,考虑对新记录进行 ActiveRecord::Base#save 调用时触发的回调
-
(-)
save
-
(-)
valid
-
(1)
before_validation
-
(-)
validate
-
(2)
after_validation
-
(3)
before_save
-
(4)
before_create
-
(-)
create
-
(5)
after_create
-
(6)
after_save
-
(7)
after_commit
此外,还可以配置 after_rollback
回调,以便在发出回滚时触发。有关 after_commit
和 after_rollback
的更多详细信息,请查看 ActiveRecord::Transactions
。
此外,每当触碰对象时,都会触发 after_touch
回调。
最后,after_find
和 after_initialize
回调将为每个通过查找器找到和实例化的对象触发,其中 after_initialize
也将在实例化新对象后触发。
总共有 19 个回调,这为控制如何在 Active Record 生命周期的每个状态中做出反应和进行准备提供了很大的灵活性。对现有记录调用 ActiveRecord::Base#save 的调用顺序类似,只是每个 _create
回调都被相应的 _update
回调替换。
示例
class CreditCard < ActiveRecord::Base
# Strip everything but digits, so the user can specify "555 234 34" or
# "5552-3434" and both will mean "55523434"
before_validation(on: :create) do
self.number = number.gsub(/[^0-9]/, "") if attribute_present?("number")
end
end
class Subscription < ActiveRecord::Base
before_create :record_signup
private
def record_signup
self.signed_up_on = Date.today
end
end
class Firm < ActiveRecord::Base
# Disables access to the system, for associated clients and people when the firm is destroyed
before_destroy { |record| Person.where(firm_id: record.id).update_all(access: 'disabled') }
before_destroy { |record| Client.where(client_of: record.id).update_all(access: 'disabled') }
end
可继承的回调队列
除了可覆盖的回调方法之外,还可以通过使用回调宏来注册回调。它们的主要优点是宏将行为添加到回调队列中,该队列在继承层次结构中保持完整。
class Topic < ActiveRecord::Base
before_destroy :destroy_author
end
class Reply < Topic
before_destroy :destroy_readers
end
当运行 Topic#destroy
时,只调用 destroy_author
。当运行 Reply#destroy
时,会调用 destroy_author
和 destroy_readers
。
重要提示:为了使回调队列的继承正常工作,您必须在指定关联之前指定回调。否则,您可能会在父级注册回调之前触发子级的加载,并且这些回调将不会被继承。
回调类型
回调宏接受三种类型的回调:方法引用(符号)、回调对象、内联方法(使用 proc)。方法引用和回调对象是推荐的方法,使用 proc 的内联方法有时是合适的(例如,用于创建 mix-in)。
方法引用回调通过指定对象中可用的受保护或私有方法来工作,如下所示
class Topic < ActiveRecord::Base
before_destroy :delete_parents
private
def delete_parents
self.class.delete_by(parent_id: id)
end
end
回调对象具有以回调命名的方法,该方法以记录作为唯一的参数调用,例如
class BankAccount < ActiveRecord::Base
before_save EncryptionWrapper.new
after_save EncryptionWrapper.new
after_initialize EncryptionWrapper.new
end
class EncryptionWrapper
def before_save(record)
record.credit_card_number = encrypt(record.credit_card_number)
end
def after_save(record)
record.credit_card_number = decrypt(record.credit_card_number)
end
alias_method :after_initialize, :after_save
private
def encrypt(value)
# Secrecy is committed
end
def decrypt(value)
# Secrecy is unveiled
end
end
因此,您指定要在一个给定回调上发送消息的对象。当触发该回调时,对象将具有一个与回调名称相同的方法,该方法将被发送消息。通过传入其他初始化数据(例如要处理的属性的名称),可以使这些回调更加灵活
class BankAccount < ActiveRecord::Base
before_save EncryptionWrapper.new("credit_card_number")
after_save EncryptionWrapper.new("credit_card_number")
after_initialize EncryptionWrapper.new("credit_card_number")
end
class EncryptionWrapper
def initialize(attribute)
@attribute = attribute
end
def before_save(record)
record.send("#{@attribute}=", encrypt(record.send("#{@attribute}")))
end
def after_save(record)
record.send("#{@attribute}=", decrypt(record.send("#{@attribute}")))
end
alias_method :after_initialize, :after_save
private
def encrypt(value)
# Secrecy is committed
end
def decrypt(value)
# Secrecy is unveiled
end
end
before_validation*
返回语句
如果 before_validation
回调抛出 :abort
,则进程将被中止,并且 ActiveRecord::Base#save 将返回 false
。如果调用 ActiveRecord::Base#save!,它将引发 ActiveRecord::RecordInvalid
异常。不会将任何内容追加到错误对象。
取消回调
如果 before_*
回调抛出 :abort
,则所有后续回调和关联操作都将被取消。回调通常按定义顺序运行,除了在模型上定义为方法的回调,这些回调最后调用。
回调排序
有时,应用程序代码要求回调按特定顺序执行。例如,before_destroy
回调(在本例中为 log_children
)应在 children
关联中的记录被 dependent: :destroy
选项销毁之前执行。
让我们看看下面的代码
class Topic < ActiveRecord::Base
has_many :children, dependent: :destroy
before_destroy :log_children
private
def log_children
# Child processing
end
end
在本例中,问题是当执行 before_destroy
回调时,children
关联中的记录不再存在,因为 ActiveRecord::Base#destroy 回调首先执行。您可以使用 before_destroy
回调上的 prepend
选项来避免这种情况。
class Topic < ActiveRecord::Base
has_many :children, dependent: :destroy
before_destroy :log_children, prepend: true
private
def log_children
# Child processing
end
end
这样,before_destroy
在 dependent: :destroy
被调用之前执行,并且数据仍然可用。
此外,在某些情况下,您希望同一类型的多个回调按顺序执行。
例如
class Topic < ActiveRecord::Base
has_many :children
after_save :log_children
after_save :do_something_else
private
def log_children
# Child processing
end
def do_something_else
# Something else
end
end
在本例中,log_children
在 do_something_else
之前执行。这适用于所有非事务性回调以及 before_commit
。
对于事务性 after_
回调(after_commit
、after_rollback
等),可以通过配置设置顺序。
config.active_record.run_after_transaction_callbacks_in_order_defined = false
当设置为 true
(从 Rails 7.1 开始的默认值)时,回调按定义顺序执行,就像上面的示例一样。当设置为 false
时,顺序将颠倒,因此 do_something_else
在 log_children
之前执行。
事务
#save、#save! 或 #destroy 调用的整个回调链在事务中运行。这包括 after_*
钩子。如果一切顺利,一旦链完成,就会执行 COMMIT
。
如果 before_*
回调取消操作,将发出 ROLLBACK
。您还可以在任何回调中引发异常,包括 after_*
钩子,从而触发 ROLLBACK
。但是,请注意,在这种情况下,客户端需要知道这一点,因为普通的 #save 将引发此异常,而不是静默地返回 false
。
调试回调
可以通过对象上的 _*_callbacks
方法访问回调链。Active Model Callbacks 支持 :before
、:after
和 :around
作为 kind
属性的值。kind
属性定义回调在链中的哪个部分运行。
要查找 before_save
回调链中的所有回调
Topic._save_callbacks.select { |cb| cb.kind.eql?(:before) }
返回一个回调对象数组,这些对象构成 before_save
链。
要进一步检查 before_save
链是否包含定义为 rest_when_dead
的 proc,请使用回调对象的 filter
属性
Topic._save_callbacks.select { |cb| cb.kind.eql?(:before) }.collect(&:filter).include?(:rest_when_dead)
返回 true 或 false,具体取决于 Topic 模型上的 before_save
回调链中是否包含 proc。
常量
CALLBACKS | = | [ :after_initialize, :after_find, :after_touch, :before_validation, :after_validation, :before_save, :around_save, :after_save, :before_create, :around_create, :after_create, :before_update, :around_update, :after_update, :before_destroy, :around_destroy, :after_destroy, :after_commit, :after_rollback ] |