In rails when we want some series of tasks to be atomic we use transaction. But how should we write our code to support transaction?
Let me explain how transaction in rails works. If any code inside a transaction raises an exception
- the transaction block rescues it
- rolls back the database and
- re throws the exception
So we need to make sure that statements that are not complete, raise exception. Here are some scenarios with explanation.
For all the scenarios the Comment model is like,
class Comment < ActiveRecord::Base
belongs_to :post
validates_presence_of :body
end
As model.collection << child_model does not raises exception the post is saved, as the transaction is unaware of the data save failure.
test 'transaction should not rollback transaction with collection create method' do<br /> is_comment_saved = false<br /> Post.transaction do<br /> post = Post.new({:title=> 'Test title', :body => 'A sample post', :published => true})<br /> post.save!<br /> is_comment_saved = post.comments << Comment.new({:published => true})<br /> #Comment with out body should be not saved for validation<br /> #but this does not raise exception so the transaction is not rolled back<br /> #I know that rails convention is that you first assign the comment and then save post<br /> #But there might be situation when you might want to save model which is unrelated to post in the same transaction<br /> end<br /><br /> assert_equal(1, Post.count)<br /> assert_equal(0, Comment.count)<br /> assert_equal(false, is_comment_saved)<br />end
To support transaction we need to write some thing like this,
test 'transaction should rollback transaction with throwing exception manually' do<br /> is_comment_saved = false<br /> begin<br /> Post.transaction do<br /> post = Post.new({:title=> 'Test title', :body => 'A sample post', :published => true})<br /> post.save!<br /> #Comment with out body should be not saved for validation<br /> raise Exception unless post.comments << Comment.new({:published => true})<br /> end<br /> rescue Exception<br /> end<br /><br /> assert_equal(0, Post.count)<br /> assert_equal(0, Comment.count)<br /> assert_equal(false, is_comment_saved)<br />end
model.collection << returns false the operation fails. So raising an exception while it returns false causes the transaction to rollback.
Let me give you a scenario with nested transaction.
test 'default Nested transaction should rollback with raising Exception' do<br /> begin<br /> Post.transaction do<br /> post = Post.new({:title=> 'Test title', :body => 'A sample post', :published => true})<br /> post.save!<br /> Comment.transaction(:requires_new => true) do<br /> post.comments.create({:body => 'A sample comment', :published => true})<br /> raise Exception<br /> end<br /> end<br /> rescue Exception => error<br /> end<br /><br /> assert_equal(0, Post.count)<br /> assert_equal(0, Comment.count)<br />end
Note: For nested transaction in rails 2.3.2 by default any transaction inherits the transaction of it’s parent transaction. To make a new child transaction you need to pass “:requires_new => true” as the parameter of transaction method.
Identical to single transaction nested transaction behaves same if you throw an exception. So if you really mean to use a child transaction, you will need to handle exception inside or around the child transaction.
The above example behaves same even if we do not use “:requires_new => true” for child transaction. now If you want to break the child transaction and not to interfere the parent transaction, you can use “ActiveRecord::Rollback”.
Transaction relays all the exception except ActiveRecord::Rollback. If you raise ActiveRecord::Rollback then the steps are
- the transaction block rescues it
- rolls back the database and
- DOES NOT re throw the exception
Here is an example,
test 'nested transaction should not rollback parent transaction that throws ActiveRecord Rollback in child transaction' do<br /> begin<br /> Post.transaction do<br /> post = Post.new({:title=> 'Test title', :body => 'A sample post', :published => true})<br /> post.save!<br /> Comment.transaction(:requires_new => true) do<br /> comment = Comment.new({:body => 'Test', :published => true, :post_id => post.id})<br /> comment.save!<br /> raise ActiveRecord::Rollback<br /> end<br /> end<br /> rescue Exception<br /> end<br /> assert_equal(1, Post.count)<br /> assert_equal(0, Comment.count)<br />end
In rails 2.3.2 ActiveRecord::Rollback does not work for child transaction that is not defined as :requires_new => true.
Here is another scenario for that test,
test 'default nested transaction should not rollback even child trasnsaction that throws exception in child transaction' do<br /> Post.transaction do<br /> post = Post.new({:title=> 'Test title', :body => 'A sample post', :published => true})<br /> post.save!<br /> Comment.transaction do<br /> comment = Comment.new({:body => 'Test', :published => true, :post_id => post.id})<br /> comment.save!<br /> raise ActiveRecord::Rollback<br /> end<br /> end<br /> assert_equal(1, Post.count)<br /> #Here comment is saved too<br /> assert_equal(1, Comment.count)<br />end
As you can see raising ActiveRecord::Rollback does not impact the result.
These are some of the scenario explained, which should help you to choose appropriate way to use transaction for your application.
You can download the test project from here.