Why does Rails ignore a Rollback in a (pseudo)nested transaction?

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

The 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?

Answer #1:

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 requires_new: true

User.transaction do
  User.create(username: 'Kotori')
  User.transaction(requires_new: true) do
    User.create(username: 'Nemu')
    raise ActiveRecord::Rollback

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 nested_attributes

while option(2) is to support the savepoint workaround if you don't.

Answered By: Nimir

Answer #2:

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.

  1. Rails transaction do blocks have slightly different behaviors depending on the type of exception raised within them:

    • When ActiveRecord::Rollback exceptions are raised within a transaction do block, those exceptions are rescued by the transaction do block and do not bubble up farther.
    • All other kinds of exceptions are rescued and re-raised by a transaction do block and do continue to bubble up beyond that block.
  2. 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

Answered By: Ben Sheldon
The answers/resolutions are collected from stackoverflow, are licensed under cc by-sa 2.5 , cc by-sa 3.0 and cc by-sa 4.0 .

# More Articles