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_commitgem или отключение транзакционных фикстур.- 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.