What are Transactions?
Transactions are protective blocks where SQL statements are only permanent if
they can all succeed as one atomic action. Transactions are used to enforce the
integrity of the database and guard the data against program errors or database
break-downs.
So basically you should use transaction blocks whenever you have a number of
statements that must be executed together or not at all.
Transaction makes sure either all the statements persist or none do.
Transactions in Active Record
Rails provides transaction as a class method under the ActiveRecord::Base
class. We can use the method like this:
All changes, that the statements within the transaction block tries to make, get
committed to the database, only if each of the statement run without raising an
error. If any statement raises an error, then all the changes will be rollback.
Transactions in action
The classic example of Transactions in action is a transfer between two accounts
where you can only have a deposit if the withdrawal succeeded and vice versa.
Implemented naively our code might look something like this:
While ideally the implementation works, what will happen if our program crashes
at any point after we debit David's account?
Then we would have debited the amount from David's account but not successfully
transferred it to Mary's account, leading to the money essentially disappearing.
Wrapping the above queries in a transaction block will make sure that the
withdrawal only occurs if none of the other functions throw an exception. This
will help us in maintaining data integrity.
Different Active Record classes in a single transaction
A transaction is a class method, therefore it must be called upon an Active
Record class. However it is not necessary that all objects in the block should
be an instance of that class. This is because transactions are per-database
connection and not per-model.
For example let's consider two models Balance and Account and we want to
transactionally save both the models.
Irrespective of the class the transaction is called upon, we can call save! on
objects of both the models in the block.
We can call transaction on Account:
The above block will save balance as well as account even though we are
calling the method on Account class.
The transaction method is also available as a model instance method. For
example, we can also do this:
Note: A transaction acts on a single database connection. If you have
multiple class-specific databases, the transaction will not protect interaction
amongst them. One workaround is to begin a transaction on each class whose model
needs altering.
Calling transactions on ActiveRecord::Base vs Model Class vs Model Instance
As mentioned in the previous section, we can call the transaction method on
ActiveRecord::Base class, model class, or a model instance. You might be
wondering how these three are different.
Rails provides the transaction method under the ActiveRecord::Base Class.
Since every model inherits ActiveRecord::Base the transaction method is
available to every model class.
The transaction method called on a model class works exactly like
ActiveRecord::Base.transaction unless we specifically override it to set or
get its database connection.
Calling the transaction method on model instance is a convenience method that
let's us write more syntactically beautiful code.
For example, let's consider the following code snippet:
Under the hood account.transaction calls the transaction method on the
Account class. It is equivalent to the following code snippet:
Transactional nature of save and destroy methods
One important implementation of transactions are in save and destroy
methods of Active Record. Both methods are automatically wrapped in a
transaction that ensures that whatever you do in validations or callbacks will
happen under its protected cover. So you can use validations to check for values
that the transaction depends on or you can raise exceptions in the callbacks to
rollback, including after_* callbacks.
Exception handling and rolling back
There are two common ways to raise an exception in a Rails transaction:
- Using Active Record bang methods like save!,update!, etc.
- Manually raising an exception.
Generally the exceptions in a transaction block will be propagated, after
triggering ROLLBACK. So you should be ready to catch those exceptions in your
application code.
Let's say we have a transaction that creates a new Account and updates another
account like this:
The create! and update! method will throw an exception if something goes
wrong.
Note: If we were to use, the non-bang version of these methods, that is
create or update, then instead they would indicate failure via their return
value and the transaction would keep running.
If for some reason we want to use the non-bang version of these methods, then we
can always raise an exception manually to check the return value like this:
Now we can rescue this error in our code:
There is one caveat while rescuing exceptions in transactions. While we can
rescue most exceptions in the transaction block, we should not catch
ActiveRecord::StatementInvalid exception in transaction block. This is
because ActiveRecord::StatementInvalid indicates that an error occurred at the
database level, for example a unique constraint is violated.
On some database systems, such as PostgreSQL, database errors inside a
transaction cause the entire transaction to become unusable until it's restarted
from the beginning.
Let's demonstrate this problem using a User model that has a unique constraint
on email:
In such cases, you should restart the entire transaction if an
ActiveRecord::StatementInvalid occurred.
Previously we have mentioned that exceptions thrown within a transaction block
will be propagated. The one exception to this is when ActiveRecord::Rollback
exception is raised which will trigger a ROLLBACK when raised, but not be
re-raised by the transaction block.
This exception is important to consider when we make use of nested transactions
in the upcoming section.
Nested Transactions
The transactions we have used so far allow us to only work with a single
database in a block. This is suitable for our project since we are only using
one database.
But what if we are working on a project with multiple databases and we need to
ensure data integrity across multiple databases? This is where nesting Active
Record transactions come in handy.
For example, say we have two models User and Account that point to two
different databases. We want to withdraw the subscription amount from
Account and then update subscription status in User. Obviously, we want to
achieve this in a transactional way.
We can use nested transaction to make sure both the statements execute together
or not at all:
If any part of the inner transaction fails, it will cause the outer transaction
to be aborted. This allows us to transactionally update records across multiple
databases.
The above snippet makes sure that the subscribed value is only updated if the
subscription_amount is withdrawn and vice versa.
As mentioned in the previous section there is one caveat when an
ActiveRecord::Rollback error is raised.
The above snippet results in both Oliver and Sam being created.
The reason behind this can be explained in two parts.
The first part has to the do with the way ActiveRecord::Rollback exception is
raised in Rails. In the above snippet the exception is caught by the inner
transaction block, which triggers a ROLLBACK. However since
ActiveRecord::Rollback exception is not re-raised by the transaction block,
the outer transaction is unaware of the exception ever being raised.
The second part of the reason has to do with the way nested transactions are
implemented in Rails. At the time of writing, most databases don't support
nested transactions at the database level. The only database that we're aware of
that supports nested transactions is MS-SQL. Because of this, ActiveRecord
emulates nested transactions by using savepoints. You can refer to the Official
MySQL Reference on Savepoints (Link is not available) for more information about
savepoints.
Since the inner transaction is emulated and not treated as a real transaction,
when the outer transaction sees no error raised in its block it commits all
statements instead of rolling back the inner transaction's statements.
A way around this is passing requires_new: true to the transaction method of
the inner transaction, that we think may raise the ActiveRecord::Rollback
exception. What this does is it instructs Rails to treat the nested emulated
transaction as real. If anything goes wrong, then the database rolls back to the
beginning of the sub-transaction without rolling back the parent transaction. We
can add it to the previous example:
This will result in only Oliver being created.
Transaction Callbacks
There are two types of callbacks associated with transactions:
- after_commit
- after_rollback
after_commit callbacks are called on every record saved or destroyed within a
transaction immediately after the transaction is committed.
after_rollback callbacks are called on every record saved or destroyed within
a transaction immediately after the transaction or savepoint is rolled back.
These callbacks are useful for interacting with other systems since you will be
guaranteed that the callback is only executed when the database is in a
permanent state.
For example, after_commit is a good spot to put in a method that clears cache
since clearing it from within a transaction could trigger the cache to be
regenerated before the database is updated.
References