From time to time you have to wrap some magic code in an ActiveRecord transaction. In such cases it’s better to make sure your code does its job or you’ll face the problems sooner or later. Even if you code only for fun, it’s worth to know a few things about transactions before you fall into the deep forest of debugging strange issues you can avoid. Just to make sure we all know what we’re talking about, let me paste the Rails API description of the AR transactions:
Transactions are protective blocks where SQL statements are only permanent if they can all succeed as one atomic action. The classic example is a transfer between two accounts where you can only have a deposit if the withdrawal succeeded and vice versa. Transactions 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.
I love this description :-) it’s everything you basically need to know to understand what an ActiveRecord transaction is. Ready for a few quick tips? Go on!
Use bang methods
You know what is the difference between
save! methods. The first one returns
false, while the second one raises an exception if something goes wrong. Have you ever wondered how this information may be useful? I have good news for you! ActiveRecord transactions are rolled back when an exception occur. Transactions are not catching
false, so if you want your transaction to do its job, it’s always better to use the bang methods (or handle raising the errors on your own).
save is not a good example, cause error raised by calling it means something is really wrong, but I hope you get the point :-)
Rescue the exceptions
In most cases you should
rescue the exception whenever it happen, not only while dealing with the transactions. Bare in mind that all the exceptions raised within transactions are propagated after triggering the rollback. Catch them in your code!
Automatic transaction wrapping
destroy methods are automatically wrapped within a transaction. Wait… what? So why should I use the bang methods above or something? The thing is that these methods, when fail, are rolling back only the commits living around them, e.g. due to the usage of
dependent: :destroy option. When the relation is not so obvious or you do not have any related callbacks, then you should use an explicit transaction and catch the exceptions.
This kind of exception is raised when there is an error on the database level. According to the official Rails documentation you should avoid catching these errors within the transaction block if you do not want to complicate the situation even more.
Thas’s it for now. Please drop me a line in a comment if you think I’ve missed something or something is not clear ;-)