It seems that rails docs are not straightforward or easy to understand in this section.
Transactions are meant to silently fail if an ActiveRecord::Rollback is raised inside the block, but if any other error is raised, the transactions will issue a rollback and the exception will be passed on.
Looking into the rails docs first example:
User.transaction do
User.create(username: 'Kotori')
User.transaction do
User.create(username: 'Nemu')
raise ActiveRecord::Rollback
end
end
An ActiveRecord::Rollback is being raised in the inner transaction, and therefore it should avoid the creation of the users, but as all database statements in the nested transaction block become part of the parent transaction and the ActiveRecord::Rollback
exception in the nested block does not carry up a ROLLBACK action to the parent transaction, both users are created. As I wrote before, ActiveRecord::Rollback exceptions will be intentionally rescued and swallowed without any consequences, and the parent transaction won't detect the exception.
If we take the same example, but we raise a different exception:
User.transaction do
User.create(username: 'Kotori')
User.transaction do
User.create(username: 'Nemu')
raise ArgumentError
end
end
This will work as expected. The transactions are nested and joined correctly in only one connection (this is default behaviour), therefore, Nemu and Kotori won't be created. It also doesn't matter where the error is raised, if it is raised in the parent or child transactions it will still rollback all statements.
We can also achieve a different result by creating real sub-transaction by passing requires_new: true
to the inner transaction.
User.transaction do
User.create(username: 'Kotori')
User.transaction(requires_new: true) do
User.create(username: 'Nemu')
raise ActiveRecord::Rollback
end
end
This would treat each transaction separately and if an exception is raised in the inner transaction the database rolls back to the beginning of the sub-transaction without rolling back the parent transaction. Therefore the example above would only create the Kotori user.
The documentation gives just a bit of information about the 2 options that we can pass to the transaction method: joinable and requires_new.
This options help us create real sub-transactions and treat nested transactions as individual database connections, which therefore helps us avoid dependancy between parent-child transactions when intentionally raising an ActiveRecord::Rollback
exception. Each option is intended to be used depending on the nested hierarchy level of the transaction.
joinable: default true. Allows us to tell the outer transaction if we want the inner transaction to be joined within the same connection. If this value is set to false and the inner transaction raises a rollback exception it wont affect the outer transaction.
requires_new: default nil. Allows us to tell the inner transaction if we want it run in a different connection. If this value is set to true and a rollback exception is raised, it wont affect the parent transaction.
So, this two options are meant to be used to run transactions in individual database connections depending on the nested hierarchy you can control.
Unfortunately not all database support real nested-transactions but Active Record emulates nested transactions by using savepoints, so the behaviour should be the same at least with postgres and MS-SQL.
Extra: it seems that the rails team is already working towards updating this functionality https://github.com/rails/rails/pull/44518
joinable
documented anywhere? – Wormhole