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
.