Spec-Zone.ru › Ruby on Rails 8.1

module ActiveRecord::Callbacks

Подключенные модули:
ActiveModel::Validations::Callbacks

Колбэки 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. В объект ошибок ничего не добавляется.

Отмена колбэков

Если колбэк 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. Чтобы этого избежать, можно использовать параметр prepend для колбэка before_destroy.

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. Также можно вызвать ROLLBACK, выбросив исключение в любом из колбэков, включая хуки after_*. Однако в этом случае клиент должен быть готов к исключению, поскольку обычный вызов #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 proc, определенный как rest_when_dead, используйте свойство filter объекта колбэка:

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

Возвращает true или false в зависимости от того, содержится ли proc в цепочке колбэков before_save модели Topic.

Константы

CALLBACKS

© 2004–2021 David Heinemeier Hansson
Licensed under the MIT License.

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API