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 在新对象被实例化后也会被触发。
总共有十九个回调,这提供了对如何在 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 有时也是合适的(例如用于创建混入)。
方法引用回调通过指定对象中可用的受保护或私有方法来工作,如下所示:
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 异常。不会向 errors 对象添加任何内容。
取消回调¶ ↑
如果 before_* 回调抛出 :abort,所有后续的回调和相关的操作都将被取消。回调通常按定义的顺序运行,但模型上定义为方法的那些回调除外,它们最后被调用。
回调的排序¶ ↑
有时应用程序代码需要按照特定顺序执行回调。例如,before_destroy 回调(本例中为 log_children)应该在通过 dependent: :destroy 选项销毁 children 关联中的记录之前执行。
让我们看一下下面的代码:
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)
根据 proc 是否包含在 Topic 模型的 before_save 回调链中,返回 true 或 false。
常量
| 回调 | = | [ :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 ] |