Ruby on RailsMiddleCoding

Что такое 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.

Sources

Related topics