Ruby on RailsSeniorTechnical

Как Rails обрабатывает транзакции базы данных и что такое ApplicationRecord.transaction?

ApplicationRecord.transaction оборачивает блок в SQL-транзакцию с автоматическим ROLLBACK при исключении. Для вложенных транзакций используйте savepoints через requires_new: true.

Транзакции в Rails: ApplicationRecord.transaction

Базовое использование

# Всё внутри блока выполняется атомарно
ApplicationRecord.transaction do
  account_from = Account.lock.find(params[:from_id])
  account_to   = Account.lock.find(params[:to_id])

  raise "Insufficient funds" if account_from.balance < params[:amount]

  account_from.decrement!(:balance, params[:amount])
  account_to.increment!(:balance, params[:amount])
end
# При любом исключении — автоматический ROLLBACK

Транзакция на уровне модели

# Эквивалентно ApplicationRecord.transaction — одно и то же соединение
User.transaction do
  user = User.create!(email: params[:email])
  Profile.create!(user: user, bio: params[:bio])
end

# Можно вызвать на экземпляре
@user.transaction do
  @user.update!(role: :admin)
  AuditLog.create!(actor: current_user, action: "promote", target: @user)
end

Savepoints (вложенные транзакции)

ApplicationRecord.transaction do
  User.create!(email: "alice@example.com")

  # requires_new: true создаёт SAVEPOINT
  ApplicationRecord.transaction(requires_new: true) do
    User.create!(email: "bob@example.com")
    raise ActiveRecord::Rollback  # откатывает только до savepoint
  end

  # Alice сохранена, Bob — нет
end

ActiveRecord::Rollback vs другие исключения

ApplicationRecord.transaction do
  user.update!(status: :banned)
  raise ActiveRecord::Rollback  # тихий откат, исключение НЕ пробрасывается выше
end
# Код после transaction выполняется нормально

ApplicationRecord.transaction do
  raise RuntimeError, "something broke"  # ROLLBACK + исключение всплывает
end
# RuntimeError нужно перехватить выше

after_commit и after_rollback колбэки

class Order < ApplicationRecord
  after_commit :send_confirmation_email, on: :create
  after_rollback :log_failure

  private

  def send_confirmation_email
    # Вызывается ТОЛЬКО после успешного COMMIT
    OrderMailer.confirmation(self).deliver_later
  end

  def log_failure
    Rails.logger.error "Order #{id} transaction rolled back"
  end
end

Блокировка строк (pessimistic locking)

ApplicationRecord.transaction do
  # SELECT ... FOR UPDATE — блокирует строку до COMMIT
  inventory = Inventory.lock.find(product_id)
  raise "Out of stock" if inventory.quantity < requested
  inventory.decrement!(:quantity, requested)
end

# Optimistic locking через lock_version (без транзакции)
product = Product.find(id)
product.update!(price: new_price)  # автоматически проверяет lock_version
# При конфликте — ActiveRecord::StaleObjectError

Подводные камни

  • Вызов внешних сервисов (HTTP, email, Sidekiq jobs) внутри транзакции опасен: если транзакция откатится после вызова, внешнее действие уже выполнилось. Используйте after_commit для side effects.
  • Вложенные транзакции без requires_new: true не создают реальный savepoint — внутренний ActiveRecord::Rollback откатывает всю внешнюю транзакцию.
  • save/update (без bang) внутри транзакции не вызывают исключение при ошибке валидации — транзакция не откатится автоматически. Используйте save!/update!.
  • Длинные транзакции держат блокировки на строки и замедляют всю систему — выносите бизнес-логику за пределы транзакции, оставляя внутри только DB-операции.
  • Deadlock возникает при блокировке строк в разном порядке из разных потоков — всегда захватывайте блокировки в одном и том же детерминированном порядке (например, по возрастанию ID).
  • after_commit в тестах по умолчанию не срабатывает при использовании use_transactional_fixtures = true — нужен test_after_commit gem или отключение транзакционных фикстур.
  • Multi-DB приложения: ApplicationRecord.transaction работает только на одном соединении — для распределённых транзакций между БД нужны компенсирующие операции (saga pattern).
  • ActiveRecord::Rollback не пробрасывается сквозь вложенные транзакции без requires_new: true — поведение неочевидно и часто удивляет.

Common mistakes

  • Сводить transactions к названию метода без 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

  • Объясняет transactions через конкретную точку lifecycle в Ruby on Rails.
  • Приводит корректный минимальный пример без вымышленных методов или callbacks.
  • Называет edge cases: пустые значения, ошибки, транзакции, безопасность или concurrency.

Sources

Related topics