As per the docs ActiveRecord::Transactions::ClassMethods, a non-new nested transaction will ignore a Rollback. From the docs:
User.transaction do User.create(username: 'Kotori') User.transaction do User.create(username: 'Nemu') raise ActiveRecord::Rollback end end
raise ActiveRecord::Rollback is ignored because it is in a child transaction (or rather, it is still within the parent transaction and not its own). I do not understand why the Rollback call would be ignored by both? I can see that since the child 'transaction' isn't really a transaction, that it would not roll back the 'Nemu' block, but why does it not trigger a rollback for the parent? Does the child transaction hide the rollback somehow?
In other words, why is it that there appears to be no way to roll back a parent transaction from within a nested child?
Actually this is exactly how Nested Transactions was designed for. I quote from oracle docs:
A nested transaction is used to provide a transactional guarantee for a subset of operations performed within the scope of a larger transaction. Doing this allows you to commit and abort the subset of operations independently of the larger transaction.
So, a child transaction in a regular nested transaction has no say regarding how him or the other children or parent (larger transaction) could behave, other than changing mutual data or failing for an exception.
But you can grant him (child transaction) a very limited voting chance on his destiny by utilizing the
sub-transaction feature as stated at rails docs by passing
User.transaction do User.create(username: 'Kotori') User.transaction(requires_new: true) do User.create(username: 'Nemu') raise ActiveRecord::Rollback end end
Which as the docs say: only creates 'Kotori'. since the powerful 'Nemu' child chose to die silently.
More details about Nested transaction rules (oracle docs)
To better understand why rails
nested transactions works this way, you need to know a bit more about how nested transactions works in DB level, I quote from rails api docs:
Most databases don’t support true nested transactions ...In order to get around this problem, #transaction will emulate the effect of nested transactions, by using savepoints: http://dev.mysql.com/doc/refman/5.0/en/savepoint.html
Ok, then the docs describes the behavior of a
nested transaction in the two mentioned cases as follows:
In case of a nested call, #transaction will behave as follows:
The block will be run without doing anything. All database statements that happen within the block are effectively appended to the already open database transaction.
However, if :requires_new is set, the block will be wrapped in a database savepoint acting as a sub-transaction.
I imagine careful, only imagine that:
option(1) (without requires_new) is there in case you used a DBMS that fully supports
nested transactions or you are happy with the "fake" behavior of
while option(2) is to support the
savepoint workaround if you don't.
This is because of an interaction how
transaction do blocks specifically handle
ActiveRecord::Rollback exceptions that are raised within those blocks and how Rails joins together nested
transaction do blocks by default.
transaction do blocks have slightly different behaviors depending on the type of exception raised within them:
ActiveRecord::Rollbackexceptions are raised within a
transaction doblock, those exceptions are rescued by the
transaction doblock and do not bubble up farther.
transaction doblock and do continue to bubble up beyond that block.
By default, Rails "joins" nested transactions together. This means that a transaction will only be aborted when the most-exterior transaction has an exception bubble up through it.
Together, these two behaviors mean that when a
ActiveRecord::Rollback is raised within a nested transaction, it is rescued by the interior
transaction do block and does not re-raise; the exterior
transaction do block, because it does not receive the exception, completes successfully.
To stress, if you raise any exception other than an
ActiveRecord::Rollback, it will continue to bubble up through multiple
transaction do blocks and the exterior transaction will abort as expected.
As mentioned elsewhere, you can force Rails' nested transactions to not "join" their parent with
transaction(requires_new: true) do; as well as force parent transactions not to be joined by children with
transaction(joinable: false) do. It's been recommended to always use both
transaction(joinable: false, requires_new: true) do