As your business logic gets complex you may need to implement transactions. The classic example is a bank funds transfer from account A to account B. If the withdrawal from account A fails then the deposit to account B should either never take place or be rolled back.
All the complexity is handled by
ActiveRecord::Transactions. Any model class or instance has a method named
.transaction. When called and passed a block, that block will be executed inside a database transaction. If there’s an exception raised, the transaction will automatically be rolled back.
Let’s work with the account transfer scenario. It could be implemented like this:
1 2 3 4 5 6 7 8
First we fetch the account objects then start a transaction with
Account.transaction. It actually makes no difference which
ActiveRecord class or instance we call this method on. We could also have used any of these:
The choice would make absolutely no difference in the execution.
Rails will open a transaction in the database engine, then start executing the block. There are three possibilities:
- If no exceptions occur during the block then Rails closes the transaction and the database commits the data
- If there is an exception, Rails will tell the database to cancel the transaction and no data is changed
- If the entire Rails process or server dies then the transaction will timeout and be cancelled by the database
The critical step to notice is the use of
.save! instead of
.save. The former will raise an exception when the operation fails, while the latter will just return
false. If we just used
.save our transaction would never fail. If you wanted to use
.save, here’s one possible refactoring:
1 2 3 4 5
It’s ideal to run as few instructions as possible inside the transaction because keeping the connection open is taxing on the database. You could pull the two math operation out to save a few microseconds. Here’s a final version:
1 2 3 4 5 6 7
There are two additional callbacks available when working with transactions.
This callback fires when the transaction succeeds.
This callback fires when the transaction fails.
Here’s a sample model using a transaction and both callbacks:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
- Rails API for