Thursday, April 23, 2009

Transaction in rails 2.3.2

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

  1. the transaction block rescues it
  2. rolls back the database and
  3. 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

  1. the transaction block rescues it
  2. rolls back the database and
  3. 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.



3 comments:

Brad said...

Thank you! None of the other examples I could find were including the "raise Exception unless < some action here >" so I could never figure out why my rescue block wasnt behaving!

Jitu said...

You are welcome, Brad.
Happy to hear that it helped.
Actually I think, Rails should support methods like record.save! for associations so that it throws exception when the operation fails, as transaction depends only on Exceptions.

Term Papers said...

I have been visiting various blogs for my term papers writing research. I have found your blog to be quite useful. Keep updating your blog with valuable information... Regards