Это руководство научит вас вмешиваться в жизненный цикл ваших объектов Active Record.
После прочтения этого руководства вы узнаете:
В результате обычных операций приложения на Rails, объекты могут быть созданы, обновлены и уничтожены. Active Record дает возможность вмешаться в этот жизненный цикл объекта, таким образом, вы можете контролировать свое приложение и его данные.
Колбэки позволяют вам переключать логику до или после изменения состояния объекта. Это методы, которые вызываются в определенные моменты жизненного цикла объекта. С помощью колбэков можно писать код, который будет выполняться всякий раз, когда объект Active Record инициализируется, создается, сохраняется, обновляется, удаляется, проверяется на валидность или загружается из базы данных.
class BirthdayCake < ApplicationRecord
after_create -> { Rails.logger.info("Congratulations, the callback has run!") }
end
irb> BirthdayCake.create
Congratulations, the callback has run!
Вы увидите, что есть множество событий жизненного цикла, и несколько вариантов вклиниться в них - или до, или после или даже вокруг них.
Для того, чтобы использовать доступные колбэки, их необходимо реализовать и зарегистрировать. Реализация может быть выполнена множеством способов, например, с помощью обычных методов, блоков и proc, или путем определения пользовательских объектов колбэков с использованием классов или модулей. Давайте рассмотрим каждую из этих методик реализации.
Вы можете зарегистрировать колбэки с помощью специального макро-метода класса, который вызывает обычный метод для реализации.
class User < ApplicationRecord
validates :username, :email, presence: true
before_validation :ensure_username_has_value
private
def ensure_username_has_value
if username.blank?
self.username = email
end
end
end
Макро-методы класса также могут получать блок. Их следует использовать, если код внутри блока такой короткий, что помещается в одну строчку.
class User < ApplicationRecord
validates :username, :email, presence: true
before_validation do
self.username = email if username.blank?
end
end
Альтернативно можно передать в колбэк proc, который будут выполнен.
class User < ApplicationRecord
validates :username, :email, presence: true
before_validation ->(user) { user.username = user.email if user.username.blank? }
end
Наконец, можно определить собственный объект колбэка, как показано ниже. мы раскроем их ниже подробнее.
class User < ApplicationRecord
validates :username, :email, presence: true
before_validation AddUsername
end
class AddUsername
def self.before_validation(record)
if record.username.blank?
record.username = record.email
end
end
end
Колбэки также можно регистрировать для срабатывания только на определенных событиях жизненного цикла. Это можно сделать с помощью опции :on, которая позволяет полностью контролировать, когда и в каком контексте будут вызываться ваши колбэки.
Контекст - это категория или сценарий, в котором вы хотите применить определенные проверки. При валидации модели ActiveRecord вы можете указать контекст для группировки проверок. Это позволяет вам иметь разные наборы валидаций, применяемые в разных ситуациях. В Rails существуют определенные контексты по умолчанию для проверок, такие как :create, :update и :save.
class User < ApplicationRecord
validates :username, :email, presence: true
before_validation :ensure_username_has_value, on: :create
# :on также принимает массив
after_validation :set_location, on: [ :create, :update ]
private
def ensure_username_has_value
if username.blank?
self.username = email
end
end
def set_location
self.location = LocationService.query(self)
end
end
Считается хорошей практикой объявлять методы колбэков как private. Если их оставить public, они могут быть вызваны извне модели и нарушить принципы инкапсуляции объекта.
Не рекомендуется использовать методы, такие как update, save или любые другие, которые вызывают побочные эффекты для объекта внутри ваших колбэков.
Например, избегайте вызова update(attribute: "value") внутри колбэка. Эта практика может привести к изменению состояния модели и потенциально вызвать непредвиденные проблемы во время коммита.
Вместо этого, для более безопасного подхода вы можете напрямую присваивать значения (например, self.attribute = "value") в колбэках, таких как before_create, before_update или даже более ранних.
Вот список всех доступных колбэков Active Record, перечисленных в том порядке, в котором они вызываются в течение соответствующих операций:
before_validation
after_validation
before_save
around_save
before_create
around_create
after_create
after_save
after_commit / after_rollback
Примеры after_commit / after_rollback можно найти здесь.
Ниже приведены примеры использования этих колбэков. Мы сгруппировали их по связанным операциям, а затем покажем, как их можно использовать совместно.
Валидационные колбэки вызываются всякий раз, когда запись проверяется на валидность напрямую через методы valid? (или его псевдоним validate) или invalid?, или косвенно через методы create, update или save. Они выполняются до и после этапа валидации.
class User < ApplicationRecord
validates :name, presence: true
before_validation :titleize_name
after_validation :log_errors
private
def titleize_name
self.name = name.downcase.titleize if name.present?
Rails.logger.info("Name titleized to #{name}")
end
def log_errors
if errors.any?
Rails.logger.error("Validation failed: #{errors.full_messages.join(', ')}")
end
end
end
irb> user = User.new(name: "", email: "john.doe@example.com", password: "abc123456")
=> #<User id: nil, email: "john.doe@example.com", created_at: nil, updated_at: nil, name: "">
irb> user.valid?
Name titleized to
Validation failed: Name can't be blank
=> false
Колбэки сохранения вызываются всякий раз, когда запись передается (т.е. "сохраняется") в базу данных с помощью методов create, update или save. Они вызываются до, после и во время сохранения объекта.
class User < ApplicationRecord
before_save :hash_password
around_save :log_saving
after_save :update_cache
private
def hash_password
self.password_digest = BCrypt::Password.create(password)
Rails.logger.info("Password hashed for user with email: #{email}")
end
def log_saving
Rails.logger.info("Saving user with email: #{email}")
yield
Rails.logger.info("User saved with email: #{email}")
end
def update_cache
Rails.cache.write(["user_data", self], attributes)
Rails.logger.info("Update Cache")
end
end
irb> user = User.create(name: "Jane Doe", password: "password", email: "jane.doe@example.com")
Password encrypted for user with email: jane.doe@example.com
Saving user with email: jane.doe@example.com
User saved with email: jane.doe@example.com
Update Cache
=> #<User id: 1, email: "jane.doe@example.com", created_at: "2024-03-20 16:02:43.685500000 +0000", updated_at: "2024-03-20 16:02:43.685500000 +0000", name: "Jane Doe">
колбэки создания вызываются всякий раз, когда запись впервые передается (т.е. "сохраняется") в базу данных. Другими словами, они срабатывают при сохранении новой записи с помощью методов create или save. Они вызываются до, после и во время создания объекта.
class User < ApplicationRecord
before_create :set_default_role
around_create :log_creation
after_create :send_welcome_email
private
def set_default_role
self.role = "user"
Rails.logger.info("User role set to default: user")
end
def log_creation
Rails.logger.info("Creating user with email: #{email}")
yield
Rails.logger.info("User created with email: #{email}")
end
def send_welcome_email
UserMailer.welcome_email(self).deliver_later
Rails.logger.info("User welcome email sent to: #{email}")
end
end
irb> user = User.create(name: "John Doe", email: "john.doe@example.com")
User role set to default: user
Creating user with email: john.doe@example.com
User created with email: john.doe@example.com
User welcome email sent to: john.doe@example.com
=> #<User id: 10, email: "john.doe@example.com", created_at: "2024-03-20 16:19:52.405195000 +0000", updated_at: "2024-03-20 16:19:52.405195000 +0000", name: "John Doe">
Колбэки обновления вызываются всякий раз, когда существующая запись передается (т.е. "сохраняется") в базу данных. Они вызываются до, после и во время обновления объекта.
before_validation
after_validation
before_save
around_save
before_update
around_update
after_update
after_save
after_commit / after_rollback
Колбэк after_save вызывается как при создании, так и при обновлении записей. Однако он всегда выполняется после более специфичных колбэков after_create и after_update, независимо от порядка вызова макросов. Аналогично, колбэки before_save и around_save следуют тому же правилу: before_save запускается перед созданием/обновлением, а around_save — вокруг операций создания/обновления. Важно отметить, что колбэки сохранения всегда будут выполняться до/вокруг/после более специфических колбэков создания/обновления.
Мы уже рассмотрели колбэки валидации и сохранения. Примеры after_commit / after_rollback можно найти здесь.
class User < ApplicationRecord
before_update :check_role_change
around_update :log_updating
after_update :send_update_email
private
def check_role_change
if role_changed?
Rails.logger.info("User role changed to #{role}")
end
end
def log_updating
Rails.logger.info("Updating user with email: #{email}")
yield
Rails.logger.info("User updated with email: #{email}")
end
def send_update_email
UserMailer.update_email(self).deliver_later
Rails.logger.info("Update email sent to: #{email}")
end
end
irb> user = User.find(1)
=> #<User id: 1, email: "john.doe@example.com", created_at: "2024-03-20 16:19:52.405195000 +0000", updated_at: "2024-03-20 16:19:52.405195000 +0000", name: "John Doe", role: "user" >
irb> user.update(role: "admin")
User role changed to admin
Updating user with email: john.doe@example.com
User updated with email: john.doe@example.com
Update email sent to: john.doe@example.com
Во многих случаях для достижения нужного поведения требуется комбинация колбэков. Например, вы можете захотеть отправить приветственное письмо после создания пользователя, но только если это новый пользователь, а не обновляемый. При обновлении информации о пользователе вы можете захотеть уведомить администратора, если были изменены важные данные. В этом случае вы можете использовать вместе колбэки after_create и after_update.
class User < ApplicationRecord
after_create :send_confirmation_email
after_update :notify_admin_if_critical_info_updated
private
def send_confirmation_email
UserMailer.confirmation_email(self).deliver_later
Rails.logger.info("Confirmation email sent to: #{email}")
end
def notify_admin_if_critical_info_updated
if saved_change_to_email? || saved_change_to_phone_number?
AdminMailer.user_critical_info_updated(self).deliver_later
Rails.logger.info("Notification sent to admin about critical info update for: #{email}")
end
end
end
irb> user = User.create(name: "John Doe", email: "john.doe@example.com")
Confirmation email sent to: john.doe@example.com
=> #<User id: 1, email: "john.doe@example.com", ...>
irb> user.update(email: "john.doe.new@example.com")
Notification sent to admin about critical info update for: john.doe.new@example.com
=> true
Колбэки уничтожения вызываются всякий раз, когда запись уничтожается, но игнорируются при удалении записи. Они вызываются до, после и во время уничтожения объекта.
Примеры after_commit / after_rollback можно найти здесь.
class User < ApplicationRecord
before_destroy :check_admin_count
around_destroy :log_destroy_operation
after_destroy :notify_users
private
def check_admin_count
if admin? && User.where(role: "admin").count == 1
throw :abort
end
Rails.logger.info("Checked the admin count")
end
def log_destroy_operation
Rails.logger.info("About to destroy user with ID #{id}")
yield
Rails.logger.info("User with ID #{id} destroyed successfully")
end
def notify_users
UserMailer.deletion_email(self).deliver_later
Rails.logger.info("Notification sent to other users about user deletion")
end
end
irb> user = User.find(1)
=> #<User id: 1, email: "john.doe@example.com", created_at: "2024-03-20 16:19:52.405195000 +0000", updated_at: "2024-03-20 16:19:52.405195000 +0000", name: "John Doe", role: "admin">
irb> user.destroy
Checked the admin count
About to destroy user with ID 1
User with ID 1 destroyed successfully
Notification sent to other users about user deletion
after_initialize и after_findВсякий раз, когда возникает объект Active Record или непосредственно при использовании new, или когда запись загружается из базы данных, будет вызван колбэк after_initialize. Он может быть полезен, чтобы избежать необходимости напрямую переопределять метод Active Record initialize.
При загрузке записи из базы данных, будет вызван колбэк after_find. after_find вызывается перед after_initialize, если они оба определены.
У колбэков after_initialize и after_find нет пары before_*.
Они могут быть зарегистрированы подобно другим колбэкам Active Record.
class User < ApplicationRecord
after_initialize do |user|
Rails.logger.info("You have initialized an object!")
end
after_find do |user|
Rails.logger.info("You have found an object!")
end
end
irb> User.new
You have initialized an object!
=> #<User id: nil>
irb> User.first
You have found an object!
You have initialized an object!
=> #<User id: 1>
after_touchКолбэк after_touch будет вызван, когда на объекте Active Record вызван touch. Подробнее о touch можно прочитать здесь.
class User < ApplicationRecord
after_touch do |user|
Rails.logger.info("You have touched an object")
end
end
irb> user = User.create(name: 'Kuldeep')
=> #<User id: 1, name: "Kuldeep", created_at: "2013-11-25 12:17:49", updated_at: "2013-11-25 12:17:49">
irb> user.touch
You have touched an object
=> true
Он может быть использован совместно с belongs_to:
class Book < ApplicationRecord
belongs_to :library, touch: true
after_touch do
Rails.logger.info("A Book was touched")
end
end
class Library < ApplicationRecord
has_many :books
after_touch :log_when_books_or_library_touched
private
def log_when_books_or_library_touched
Rails.logger.info("Book/Library was touched")
end
end
irb> book = Book.last
=> #<Book id: 1, library_id: 1, created_at: "2013-11-25 17:04:22", updated_at: "2013-11-25 17:05:05">
irb> book.touch # вызывает book.library.touch
A Book was touched
Book/Library was touched
=> true
Следующие методы запускают колбэки:
create
create!
destroy
destroy!
destroy_all
destroy_by
save
save!
save(validate: false)
save!(validate: false)
toggle!
touch
update_attribute
update_attribute!
update
update!
valid?
validate
Дополнительно, колбэк after_find запускается следующими поисковыми методами:
all
first
find
find_by
find_by!
find_by_*
find_by_*!
find_by_sql
last
sole
take
Колбэк after_initialize запускается всякий раз, когда инициализируется новый объект класса.
Методы find_by_* и find_by_*! это динамические методы поиска, генерируемые автоматически для каждого атрибута. Изучите подробнее их в разделе Динамический поиск
Как и в валидациях, возможно сделать вызов метода колбэка условным в зависимости от заданного предиката. Это осуществляется при использовании опций :if и :unless, которые могут принимать символ, Proc или массив.
Опцию :if следует использовать для определения, при каких условиях колбэк должен быть вызван. Если вы хотите определить условия, при которых колбэк не должен быть вызван, используйте опцию :unless.
:if и :unless с SymbolОпции :if и :unless можно связать с символом, соответствующим имени метода предиката, который будет вызван непосредственно перед вызовом колбэка.
При использовании опции :if, колбэк не будет выполнен, если метод предиката возвратит false; при использовании опции :unless, колбэк не будет выполнен, если метод предиката возвратит true. Это самый распространенный вариант.
class Order < ApplicationRecord
before_save :normalize_card_number, if: :paid_with_card?
end
При использовании такой формы регистрации, также возможно зарегистрировать несколько различных предикатов, которые будут вызваны, чтобы проверить, должен ли выполняться колбэк. Мы раскроем это ниже.
:if и :unless с ProcМожно связать :if и :unless с объектом Proc. Этот вариант больше всего подходит при написании коротких методов, обычно однострочных.
class Order < ApplicationRecord
before_save :normalize_card_number,
if: ->(order) { order.paid_with_card? }
end
Так как proc вычисляется в контексте объекта, также возможно написать так:
class Order < ApplicationRecord
before_save :normalize_card_number, if: -> { paid_with_card? }
end
Опции :if и :unless также принимают массив из proc или имен методов в виде символов:
class Comment < ApplicationRecord
before_save :filter_content,
if: [:subject_to_parental_control?, :untrusted_author?]
end
В список условий также можно запросто включить proc:
class Comment < ApplicationRecord
before_save :filter_content,
if: [:subject_to_parental_control?, -> { untrusted_author? }]
end
В колбэках можно смешивать :if и :unless в одном выражении:
class Comment < ApplicationRecord
before_save :filter_content,
if: -> { forum.parental_control? },
unless: -> { author.trusted? }
end
Колбэк запустится только когда все условия :if и не один из условий :unless будут истинны.
Как и в валидациях, возможно пропустить колбэки с помощью следующих методов:
decrement!
decrement_counter
delete
delete_all
delete_by
increment!
increment_counter
insert
insert!
insert_all
insert_all!
touch_all
update_column
update_columns
update_all
update_counters
upsert
upsert_all
Давайте рассмотрим модель User, где колбэк before_save логирует любые изменения адреса электронной почты пользователя:
class User < ApplicationRecord
before_save :log_email_change
private
def log_email_change
if email_changed?
Rails.logger.info("Email changed from #{email_was} to #{email}")
end
end
end
Теперь предположим, что существует сценарий, в котором вы хотите обновить адрес электронной почты пользователя, не вызывая колбэк before_save для регистрации изменения email. Для этого вы можете использовать метод update_columns.
irb> user = User.find(1)
irb> user.update_columns(email: 'new_email@example.com')
Вышесказанное обновит адрес электронной почты пользователя без вызова колбэка before_save.
Эти методы следует использовать с осторожностью, поскольку в колбэках могут быть важные бизнес-правила и логика приложения, которые вы не хотите обходить. Их обход без понимания потенциальных последствий может привести к неверным данным.
В некоторых ситуациях вам может понадобиться временно отключить выполнение определенных колбэков в вашем Rails-приложении. Это полезно, когда вы хотите пропустить конкретные действия во время определенных операций, не отключая колбэки навсегда.
Rails предоставляет механизм подавления колбэков с помощью модуля ActiveRecord::Suppressor. Используя этот модуль, вы можете обернуть блок кода, в котором хотите подавить колбэки, гарантируя, что они не будут выполняться во время этой конкретной операции.
Рассмотрим сценарий, где у нас есть модель User с колбэком, который отправляет приветственное письмо новым пользователям после регистрации. Однако могут быть случаи, когда вы хотите создать пользователя без отправки приветственного письма, например, при заполнении базы данных тестовыми данными.
class User < ApplicationRecord
after_create :send_welcome_email
def send_welcome_email
puts "Welcome email sent to #{self.email}"
end
end
В этом примере колбэк after_create вызывает метод send_welcome_email каждый раз, когда создается новый пользователь.
Чтобы создать пользователя без отправки приветственного письма, мы можем использовать модуль ActiveRecord::Suppressor следующим образом:
User.suppress do
User.create(name: "Jane", email: "jane@example.com")
end
В этом коде блок User.suppress гарантирует, что колбэк send_welcome_email не будет выполнен во время создания пользователя "Jane", позволяя создать пользователя без отправки приветственного письма.
Использование механизма подавления колбэков ActiveRecord, хотя и может быть полезным для выборочного управления их выполнением, может привести к усложнению кода и неожиданному поведению. Подавление колбэков может затемнить логику работы вашего приложения, что со временем затруднит понимание и поддержку кодовой базы. Тщательно взвешивайте необходимость подавления колбэков, обеспечивая тщательную документацию и продуманное тестирование, чтобы снизить риски непреднамеренных побочных эффектов, проблем с производительностью и сбоев тестов.
При регистрации новых колбэков для ваших моделей они будут помещены в очередь на выполнение. Эта очередь будет включать все валидации модели, зарегистрированные колбэки и операцию с базой данных, которая должна быть выполнена.
Вся цепочка колбэков обернута в транзакцию. Если какой-либо колбэк вызывает исключение, цепочка выполнения прерывается, выполняется откат, а ошибка будет выброшена повторно.
class Product < ActiveRecord::Base
before_validation do
raise "Price can't be negative" if total_price < 0
end
end
Product.create # вызовет "Price can't be negative"
Это неожиданно приводит к сбою кода, который не ожидает, что методы вроде create и save будут вызывать исключения.
Если во время цепочки колбэков возникает исключение, Rails повторно выбросит его, за исключением случаев, когда это исключение ActiveRecord::Rollback или ActiveRecord::RecordInvalid. Вместо этого, вы должны использовать throw :abort для преднамеренного прерывания цепочки. Если какой-либо колбэк использует кидает :abort, процесс будет прерван, а create вернет значение create.
class Product < ActiveRecord::Base
before_validation do
throw :abort if total_price < 0
end
end
Product.create # => false
Однако при вызове метода create! будет выброшено исключение ActiveRecord::RecordNotSaved. Это исключение указывает на то, что запись не была сохранена из-за прерывания колбэком.
User.create! # => вызовет ActiveRecord::RecordNotSaved
При throw :abort в любом колбэке уничтожения метод destroy вернет false:
class User < ActiveRecord::Base
before_destroy do
throw :abort if still_active?
end
end
User.first.destroy # => false
Однако, при вызове destroy! будет выброшено ActiveRecord::RecordNotDestroyed.
User.first.destroy! # => вызовет ActiveRecord::RecordNotDestroyed
Колбэки связей похожи на обычные колбэки, но они вызываются событиями в жизненном цикле связанной коллекции. Существует четыре доступных колбэка связей:
before_add
after_add
before_remove
after_remove
Вы можете определить колбэки связей, добавив опции к самой связи.
Представим ситуацию, когда автор может иметь множество книг. Однако, прежде чем добавлять книгу в коллекцию автора, вы хотите убедиться, что автор не достиг своего лимита книг. Этого можно добиться с помощью колбэка before_add, который проверит лимит.
class Author < ApplicationRecord
has_many :books, before_add: :check_limit
private
def check_limit
if books.count >= 5
errors.add(:base, "Cannot add more than 5 books for this author")
throw(:abort)
end
end
end
Если колбэк before_add бросает :abort, объект не добавляется в коллекцию.
Иногда вам может понадобиться выполнить несколько действий с связанным объектом. В этом случае вы можете объединить колбэки для одного события, передав их массивом. Кроме того, Rails автоматически передает колбэку объект, который добавляется или удаляется, для использования вами.
class Author < ApplicationRecord
has_many :books, before_add: [:check_limit, :calculate_shipping_charges]
def check_limit
if books.count >= 5
errors.add(:base, "Cannot add more than 5 books for this author")
throw(:abort)
end
end
def calculate_shipping_charges(book)
weight_in_pounds = book.weight_in_pounds || 1
shipping_charges = weight_in_pounds * 2
shipping_charges
end
end
Аналогично, если колбэк before_remove бросает :abort, объект не будет удален из коллекции.
Эти колбэки вызываются только тогда, когда связанные объекты добавляются или удаляются через коллекцию ассоциации.
# Вызывает колбэк `before_add`
author.books << book
author.books = [book, book2]
# Не вызывает колбэк `before_add`
book.update(author_id: 1)
Колбэки могут быть вызваны при изменении связанных объектов. Они работают через связи моделей, при этом жизненные циклы событий могут ниспадать по связям и запускать колбэки.
Представим ситуацию, когда у пользователя есть много статей. Статьи пользователя должны быть удалены, если сам пользователь удаляется. Давайте добавим колбэк after_destroy к модели User через ее связь с моделью Article:
class User < ApplicationRecord
has_many :articles, dependent: :destroy
end
class Article < ApplicationRecord
after_destroy :log_destroy_action
def log_destroy_action
Rails.logger.info("Article destroyed")
end
end
irb> user = User.first
=> #<User id: 1>
irb> user.articles.create!
=> #<Article id: 1, user_id: 1>
irb> user.destroy
Article destroyed
=> #<User id: 1>
При использовании колбэка before_destroy его следует размещать перед связями с dependent: :destroy (или использовать опцию prepend: true), чтобы гарантировать их выполнение до того, как записи будут удалены с помощью dependent: :destroy.
after_commit и after_rollbackДва дополнительных колбэка вызываются по завершению транзакции базы данных: after_commit и after_rollback. Эти колбэки очень похожи на колбэк after_save, за исключением того, что они не выполняются пока изменения в базе данных не будут подтверждены или обращены. Они наиболее полезны, когда вашим моделям Active Record необходимо взаимодействовать с внешними системами, не являющимися частью транзакции базы данных.
Рассмотрим модель PictureFile. которой необходимо удалить файл после того, как запись уничтожена.
class PictureFile < ApplicationRecord
after_destroy :delete_picture_file_from_disk
def delete_picture_file_from_disk
if File.exist?(filepath)
File.delete(filepath)
end
end
end
Если что-либо вызовет исключение после того, как был вызван колбэк after_destroy, и транзакция откатывается, тогда файл будет удален и модель останется в противоречивом состоянии. Например, предположим, что picture_file_2 в следующем коде не валидна, и метод save! вызовет ошибку.
PictureFile.transaction do
picture_file_1.destroy
picture_file_2.save!
end
Используя колбэк after_commit, можно учесть этот случай.
class PictureFile < ApplicationRecord
after_commit :delete_picture_file_from_disk, on: :destroy
def delete_picture_file_from_disk
if File.exist?(filepath)
File.delete(filepath)
end
end
end
Опция :on определяет, когда будет запущен колбэк. Если не предоставить опцию :on, колбэк будет запущен для каждого события жизненного цикла. Подробнее об :on читайте тут.
Когда транзакция завершается, колбэки after_commit или after_rollback вызываются для всех моделей, созданных, обновленных или уничтоженных внутри этой транзакции. Однако, если внутри одного из этих колбэков возникает исключение, оно будет передано дальше, и любые оставшиеся методы after_commit или after_rollback не будут выполнены.
class User < ActiveRecord::Base
after_commit { raise "Intentional Error" }
after_commit {
# Это не будет вызвано, потому что предыдущий after_commit вызывает исключение.
Rails.logger.info("This will not be logged")
}
end
Если код вашего колбэка вызывает исключение, вам нужно будет перехватить его и обработать внутри колбэка, чтобы дать другим колбэкам возможность выполниться.
after_commit предоставляет совершенно другие гарантии, чем after_save, after_update и after_destroy. Например, если исключение возникает в after_save, транзакция будет отменена, и данные не сохранятся.
class User < ActiveRecord::Base
after_save do
# Если это завершится с ошибкой, пользователь не будет сохранен.
EventLog.create!(event: "user_saved")
end
end
Однако, во время after_commit данные уже были сохранены в базе данных, поэтому любое исключение больше ничего не откатит.
class User < ActiveRecord::Base
after_commit do
# Если это завершится с ошибкой, пользователь уже был сохранен.
EventLog.create!(event: "user_saved")
end
end
Код внутри колбэков after_commit или after_rollback не выполняется в отдельной транзакции.
В контексте одиночной транзакции, важно учитывать поведение колбэков after_commit и after_rollback, когда вы работаете с несколькими объектами, представляющими одну и ту же запись в базе данных. Эти колбэки вызываются только для первого объекта конкретной записи, которая изменяется внутри транзакции. Для других загруженных объектов, даже если они представляют ту же запись в базе данных, их соответствующие колбэки after_commit или after_rollback не будут вызваны.
class User < ApplicationRecord
after_commit :log_user_saved_to_db, on: :update
private
def log_user_saved_to_db
Rails.logger.info("User was saved to database")
end
end
irb> user = User.create
irb> User.transaction { user.save; user.save }
# User was saved to database
Это тонкое поведение особенно влияет на сценарии, где вы ожидаете независимого выполнения колбэков для каждого объекта, связанного с одной и той же записью в базе данных. Оно может повлиять на последовательность и предсказуемость вызова колбэков, что может привести к потенциальным несоответствиям в логике приложения после транзакции.
after_commitИспользование колбэка after_commit только при создании, обновлении или удалении данных является распространенной практикой. Иногда вы также можете захотеть использовать один колбэк для обоих create и update. Вот некоторые распространенные псевдонимы для этих операций:
Давайте разберем несколько примеров:
Вместо использования after_commit с опцией on для уничтожения, как показано ниже:
class PictureFile < ApplicationRecord
after_commit :delete_picture_file_from_disk, on: :destroy
def delete_picture_file_from_disk
if File.exist?(filepath)
File.delete(filepath)
end
end
end
Вместо этого можно использовать after_destroy_commit.
class PictureFile < ApplicationRecord
after_destroy_commit :delete_picture_file_from_disk
def delete_picture_file_from_disk
if File.exist?(filepath)
File.delete(filepath)
end
end
end
То же самое применимо к after_create_commit и after_update_commit.
Однако, если используются after_create_commit и after_update_commit с одним и тем же именем метода, сработает только колбэк, определенный последним, так как они оба являются псевдонимами к after_commit, который переопределяет ранее определенные колбэки с тем же именем метода.
class User < ApplicationRecord
after_create_commit :log_user_saved_to_db
after_update_commit :log_user_saved_to_db
private
def log_user_saved_to_db
# Это будет вызвано один раз
Rails.logger.info("User was saved to database")
end
end
irb> user = User.create # ничего не выводит
irb> user.save # обновление user
User was saved to database
В этом случае лучше использовать after_save_commit, который является псевдонимом для использования колбэка after_commit для создания и обновления записей:
class User < ApplicationRecord
after_save_commit :log_user_saved_to_db
private
def log_user_saved_to_db
Rails.logger.info("User was saved to database")
end
end
irb> user = User.create # создание a User
User was saved to database
irb> user.save # обновление user
User was saved to database
По умолчанию (начиная с Rails 7.1), транзакционные колбэки выполняются в том порядке, в котором они определены.
class User < ActiveRecord::Base
after_commit { Rails.logger.info("this gets called first") }
after_commit { Rails.logger.info("this gets called second") }
end
Впрочем, в предыдущих версиях Rails при определении нескольких транзакционных колбэков after_ (after_commit, after_rollback и т.д.) порядок их выполнения был обратным.
Если по какой-то причине вы по-прежнему хотите, чтобы они выполнялись в обратном порядке, вы можете установить следующую конфигурацию в значение false. Тогда колбэки будут выполняться в обратном порядке.
config.active_record.run_after_transaction_callbacks_in_order_defined = false
Это также применяется ко всем вариациям after_*_commit, таким как after_destroy_commit.
Иногда методы колбэков, которые вы пишете, могут быть настолько полезными, что их можно будет переиспользовать в других моделях. Active Record позволяет создавать классы, которые инкапсулируют методы колбэков, чтобы их можно было переиспользовать.
Вот пример класса колбэка after_commit для очистки ненужных файлов из файловой системы. Это поведение может быть не уникальным для нашей модели PictureFile, и мы можем захотеть поделиться им, поэтому хорошей идеей будет инкапсулировать его в отдельный класс. Это значительно облегчит тестирование и изменение этого поведения.
class FileDestroyerCallback
def after_commit(file)
if File.exist?(file.filepath)
File.delete(file.filepath)
end
end
end
При объявлении внутри класса, как показано выше, методы колбэка будут получать объект модели в качестве параметра. Это будет работать для любой модели, которая использует класс следующим образом:
class PictureFile < ApplicationRecord
after_commit FileDestroyerCallback.new
end
Имейте в виду, что нам нужно было создать новый объект FileDestroyerCallback, поскольку мы объявили наш колбэк как метод экземпляра. Это особенно полезно, если колбэки используют состояние созданного объекта. Однако во многих случаях более разумно объявлять колбэки как методы класса:
class FileDestroyerCallback
def self.after_commit(file)
if File.exist?(file.filepath)
File.delete(file.filepath)
end
end
end
При объявлении метода колбэка таким образом, не потребуется создавать новый объект FileDestroyerCallback в нашей модели.
class PictureFile < ApplicationRecord
after_commit FileDestroyerCallback
end
Внутри объектов колбэков вы можете объявлять столько колбэков, сколько вам нужно.