Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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
  end
end

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?

like image 559
Mike Manfrin Avatar asked Mar 14 '14 19:03

Mike Manfrin


1 Answers

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
  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)

Update:

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.

like image 170
Nimir Avatar answered Nov 15 '22 20:11

Nimir