Что такое Rails callbacks (before_action, after_create и т.д.) и каковы их риски?
Callbacks в Rails — хуки жизненного цикла объектов (before_save, after_create, after_commit и др.) и контроллеров (before_action). Основные риски: скрытая логика, проблемы с транзакциями при after_save вместо after_commit, замедление bulk-операций.
Rails callbacks: жизненный цикл объектов
Callbacks в Rails — это хуки, которые вызываются автоматически в определённые моменты жизненного цикла объекта ActiveRecord. Они позволяют вклиниться до или после операций создания, обновления, удаления, валидации и т.д.
Основные группы callbacks
- Валидация:
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_initialize,after_find - Коммит транзакции:
after_commit,after_rollback
Controller callbacks (before_action)
before_action — это callback контроллера (не модели), который выполняется до указанного экшена. Используется для аутентификации, авторизации, загрузки ресурсов.
class ArticlesController < ApplicationController
before_action :authenticate_user!
before_action :set_article, only: [:show, :edit, :update, :destroy]
before_action :authorize_author!, only: [:edit, :update, :destroy]
def show
# @article уже загружен
end
private
def set_article
@article = Article.find(params[:id])
end
def authorize_author!
redirect_to root_path, alert: 'Forbidden' unless @article.author == current_user
end
end
Callback модели: пример с after_create
class User < ApplicationRecord
after_create :send_welcome_email
after_create :create_default_profile
before_save :normalize_email
before_destroy :check_no_active_orders
private
def send_welcome_email
UserMailer.welcome(self).deliver_later
end
def create_default_profile
Profile.create!(user: self, bio: '')
end
def normalize_email
self.email = email.downcase.strip
end
def check_no_active_orders
if orders.active.exists?
errors.add(:base, 'Cannot delete user with active orders')
throw :abort
end
end
end
after_commit vs after_save
after_save срабатывает внутри транзакции — данные ещё не закоммичены в БД. after_commit срабатывает после успешного коммита, что важно для side-эффектов (email, очереди, вебхуки).
class Order < ApplicationRecord
# Плохо: Sidekiq может прочитать запись до коммита
after_save :enqueue_fulfillment
# Хорошо: задача ставится в очередь только после коммита
after_commit :enqueue_fulfillment, on: [:create, :update]
private
def enqueue_fulfillment
FulfillmentJob.perform_later(id)
end
end
Прерывание callback-цепочки
В Rails 5+ для прерывания цепочки в before_* callbacks нужно явно вызвать throw :abort. Возврат false уже не останавливает цепочку.
before_save :check_quota
def check_quota
throw :abort if quota_exceeded?
end
Подводные камни
- after_save вместо after_commit для очередей: задача в Sidekiq/Resque может запуститься до коммита транзакции и не найти запись по ID.
- Скрытая бизнес-логика: callbacks прячут поведение внутри модели, что затрудняет понимание потока выполнения и юнит-тестирование изолированных компонентов.
- Цепочки callbacks замедляют bulk-операции:
User.create!в цикле триггерит все callbacks; для массовой вставки используйтеinsert_all(callbacks не вызываются). - Обход callbacks через update_column: методы
update_column,update_columns,update_all,delete,delete_allпропускают callbacks и валидации. - Рекурсивные вызовы: callback вызывает
saveвнутри себя → бесконечная рекурсия. Используйтеupdate_columnили флаги-гарды. - before_destroy и throw :abort: если не вызвать
throw :abort, запись всё равно удалится даже если вы добавили ошибку вerrors. - after_initialize вызывается при каждом find: это тяжёлый callback — не выполняйте в нём дорогостоящие операции, он вызывается на каждый загруженный объект из БД.
- Тестирование: skip_callback: в тестах часто обходят callbacks через
User.skip_callback(:create, :after, :send_welcome_email), что скрывает реальное поведение и может привести к ложным зелёным тестам.
Common mistakes
- Сводить callbacks risks к названию метода без lifecycle и failure path.
- Игнорировать модель runtime: Rails 8.1 строит приложение вокруг Rack, routing, controllers, Active Record, views и conventions over configuration.
- Не отделять validation, authorization, transaction boundary и business logic.
What the interviewer is testing
- Объясняет callbacks risks через конкретную точку lifecycle в Ruby on Rails.
- Приводит корректный минимальный пример без вымышленных методов или callbacks.
- Называет edge cases: пустые значения, ошибки, транзакции, безопасность или concurrency.