跳至內容 跳至搜尋

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 也會在新物件被實例化後觸發。

總共有十九個回呼,它們提供了對如何在 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_authordestroy_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_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_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)

根據 Topic 模型上的 before_save 回呼鏈中是否包含 proc 返回 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 ]