I want a nested transaction to fail the parent transaction.
Lets say I have the following model
class Task < ApplicationRecord
def change_status(status, performed_by)
ActiveRecord::Base.transaction do
update!(status: status)
task_log.create!(status: status, performed_by: performed_by)
end
end
end
I always want the update
and task_log
creation to be a transaction that performs together, or not at all.
And lets say if I have a controller that allows me to update multiple tasks
class TaskController < ApplicationController
def close_tasks
tasks = Task.where(id: params[:_json])
ActiveRecord::Base.transaction do
tasks.find_each do |t|
t.change_status(:close, current_user)
end
end
end
end
I want it so that if any of the change_status
fails, that the entire request gets rolled back, from the Parent level transaction.
However, this isn't the expected behavior in Rails, referring to the documentation on Nested Transactions
They give two examples.
User.transaction do
User.create(username: 'Kotori')
User.transaction do
User.create(username: 'Nemu')
raise ActiveRecord::Rollback
end
end
Which will create both Users
"Kotori" and "Nemu", since the Parent never see's the raise
Then the following example:
User.transaction do
User.create(username: 'Kotori')
User.transaction(requires_new: true) do
User.create(username: 'Nemu')
raise ActiveRecord::Rollback
end
end
Which only creates only "Kotori", because only the nested transaction failed.
So how can I make Rails understand if there is a failure in a Nested Transaction, to fail the Parent Transaction. Continuing from the example above, I want it so that neither "Kotori" and "Nemu" are created.
You can make sure the transactions are not joinable
User.transaction(joinable:false) do
User.create(username: 'Kotori')
User.transaction(requires_new: true, joinable: false) do
User.create(username: 'Nemu') and raise ActiveRecord::Rollback
end
end
This will result in something akin to:
SQL (12.3ms) SAVE TRANSACTION active_record_1
SQL (11.7ms) SAVE TRANSACTION active_record_2
SQL (11.1ms) ROLLBACK TRANSACTION active_record_2
SQL (13.6ms) SAVE TRANSACTION active_record_2
SQL (10.7ms) SAVE TRANSACTION active_record_3
SQL (11.2ms) ROLLBACK TRANSACTION active_record_3
SQL (11.7ms) ROLLBACK TRANSACTION active_record_2
Where as your current example results in
SQL (12.3ms) SAVE TRANSACTION active_record_1
SQL (13.9ms) SAVE TRANSACTION active_record_2
SQL (28.8ms) ROLLBACK TRANSACTION active_record_2
While requires_new: true
creates a "new" transaction (generally via a save point) the rollback only applies to that transaction. When that transaction rolls back it simply discards the transaction and utilizes the save point.
By using requires_new: true, joinable: false
rails will create save points for these new transactions to emulate the concept of a true nested transaction and when the roll back is called it will rollback all the transactions.
You can think of it this way:
requires_new: true
keeps this transaction from joining its parentjoinable: false
means the parent transaction cannot be joined by its childrenWhen using both you can ensure that any transaction is never discarded and that ROLLBACK anywhere will result in ROLLBACK everywhere.