Spec-Zone.ru › Ruby on Rails 5.1

модуль ActiveRecord::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. Сравните это со следующей ситуацией, где переопределен метод before_destroy:

class Topic < ActiveRecord::Base
  def before_destroy() destroy_author end
end

class Reply < Topic
  def before_destroy() destroy_readers end
end

В этом случае Reply#destroy выполнит только destroy_readers и не destroy_author. Следовательно, используйте макросы обратчиков, если нужно гарантировать, что определенный обратчик вызывается для всей иерархии, и используйте обычные переопределяемые методы, если хотите предоставить каждому потомку право решать, нужно ли вызывать super и запускать наследуемые обратчики.

ВАЖНО: Для работы наследования для очередей обратчиков необходимо указать обратчики до указания ассоциаций. В противном случае вы можете инициировать загрузку потомка до регистрации обратчиков у родителя, и они не будут унаследованы.

Типы обратчиков

Макросы обратчиков принимают четыре типа обратчиков: ссылки на методы (метод), объекты обратчиков, встроенные методы (с использованием блока) и встроенные методы eval (с использованием строки). Ссылки на методы и объекты обратчиков являются рекомендуемыми подходами, встроенные методы с использованием блока иногда подходят (например, для создания плагинов), а встроенные методы eval устарели.

Обработчики, ссылающиеся на метод, работают путём указания защищённого или приватного метода, доступного в объекте, как показано ниже:

class Topic < ActiveRecord::Base
  before_destroy :delete_parents

  private
    def delete_parents
      self.class.delete_all "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 в этом случае) должен выполняться до того, как потомки будут удалены опцией 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 потомки недоступны, так как обработчик 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
  has_many :children

  after_save :log_children
  after_save :do_something_else

  private

  def log_chidren
    # Child processing
  end

  def do_something_else
    # Something else
  end
end

В этом случае log_children выполняется до do_something_else. То же самое относится ко всем не транзакционным обработчикам.

В случае наличия нескольких транзакционных обработчиков, как показано ниже, порядок обратный.

Например:

class Topic
  has_many :children

  after_commit :log_children
  after_commit :do_something_else

  private

  def log_chidren
    # Child processing
  end

  def do_something_else
    # Something else
  end
end

В этом случае 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 блок, определённый как rest_when_dead, используйте свойство filter объекта обратчика:

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

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

Константы

CALLBACKS

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

Spec-Zone.ru

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