跳至内容 跳至搜索

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 回调,以便在发出回滚时触发。查看 ActiveRecord::Transactions 了解更多关于 after_commitafter_rollback 的详情。

此外,每当某个对象被触及时,都会触发 after_touch 回调。

最后,对于由查找器找到和实例化的每个对象,都会触发 after_findafter_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 异常。不会将任何内容附加到 errors 对象。

取消回调

如果 `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_childrendo_something_else 之前执行。这适用于所有非事务性回调和 before_commit

对于事务性 after_ 回调(after_commitafter_rollback 等),可以通过配置设置顺序。

config.active_record.run_after_transaction_callbacks_in_order_defined = false

当设置为 true(Rails 7.1 中的默认值)时,回调按定义的顺序执行,就像上面的示例一样。当设置为 false 时,顺序会颠倒,因此 do_something_elselog_children 之前执行。

事务

#save#save!#destroy 调用的整个回调链在事务中运行。其中包括 after_* 挂钩。如果一切顺利,则在链完成之后执行 COMMIT

如果 before_* 回调取消操作,则会发出 ROLLBACK。您还可以在任何回调(包括 after_* 挂钩)中引发异常来触发 ROLLBACK。但是,请注意,在这种情况下,客户端需要意识到这一点,因为普通的 #save 将引发此类异常,而不是静默返回 false

调试回调

可以通过对象上的 _*_callbacks 方法访问回调链。Active Model 回调支持 :before:after:around 作为 kind 属性的值。kind 属性定义回调在链中的哪个部分运行。

要在 before_save 回调链中查找所有回调

Topic._save_callbacks.select { |cb| cb.kind.eql?(:before) }

返回形成 before_save 链的回调对象数组。

若要进一步检查 before_save 链是否包含定义为 rest_when_dead 的过程,请使用回调对象的 filter 属性

Topic._save_callbacks.select { |cb| cb.kind.eql?(:before) }.collect(&:filter).include?(:rest_when_dead)

根据过程是否包含在 Topic 模型的 before_save 回调链中返回 true 或 false。

命名空间
包含的模块

常量

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 ]