Yutaka's blog

Avoid using transaction on models

As transaction describes, Rails transaction behaves weirdly when it's nested.

ActiveRecord::Base.transaction do
  Post.create(title: 'first')
  ActiveRecord::Base.transaction do
    Post.create(title: 'second')
    raise ActiveRecord::Rollback
  end
end

This creates both “first” and “second” posts even ActiveRecord::Rollback is raised! (The nitty-gritty of the behavior is explained in Nested ActiveRecord transaction pitfalls)

So, I want to say: be careful of nested transactions!

However, they often appear when we write transactions on models. We sometimes want to wrap processes in a transaction block on any controllers and may want to wrap special methods declared on models like this:

class FooController < ApplicationController
  def create
    ApplicationRecord.transaction do
      Bar.create!(params.require(:bar))
      Foo.special_method!(params.require(:name)) # <-- `Foo.special_method!` uses `transaction`!
    end

    head :created
  end
end

Oh, there is a nested transaction. It's OK when exceptions other than ActiveRecord::Rollback are raised; this will trigger rollbacks throughout the parent transaction (Note sub-transaction is not respected, so this rollbacks Bar.create! as well as special_method!). However, can we completely control any gems or ActiveRecord not to raise ActiveRecord::Rollback?

No, I think we can't guarantee no ActiveRecord::Rollback will be raised from special_method!.

In the first place, who should decide to use transaction? I think it's Controller or Job. Controllers are responsible for handling HTTP requests, processing something, and passing required information to a View, while Jobs perform any computation asynchronously. Even though "processing something" is done in models, this process is called by controllers or jobs. So, I believe controllers and jobs are the best candidates to wrap transaction.

Thus, models should not use transaction inside their methods in case they are wrapped with "parent transactions" by controllers or jobs.

Note requires_new can produce a sub-transaction to mitigate the surprising behavior with ActiveRecord::Rollback, but can we really say transactions on models should always be isolated from the parent transaction? It depends on controllers or jobs, so models should forgo transaction even if there is an option of requires_new.