Transaction in Rails

In an application I wrote for a client I needed transactions to handle a batch import of records from a legacy table into different models.

I browsed the documentation, googled for info and even asked in #rubyonrails, but wasn’t able to get any help, so I had to resort to good old trial and error.

First I tried nested transactions (the old way):

FirstModel.transaction do 
  SecondModel.transaction do
    ThirdModel.transaction do
      FourthModel.transaction do
        fourth.do_stuff
        third.save
        second.handle_your_stuff
        first.good_old_foo_bar
      end
    end
  end
end

I had a couple of problems with this code:

  • Ugly
  • Deprecated

    It also didn’t seem to work for me, so I banged my head against my laptop for half a hour or so and suddenly the rails documentation started to make sense, so I wrote this code instead:

    transaction do
      first.save!
      second.save!
      third.save!
      fourth.save!
    end

    The reason I used save! was that the documentation says that a transaction block catches exceptions. Unfortunately that’s not true and to make it work I had to remove the exclamation marks.

    Now I had another problem. I included the transaction code in a class I put in lib/, ImportJob, and I was using it with script/runner:

    script/runner 'ImportJob.run' -e ENVIRONMENT
    

    All of a sudden I started getting method missing errors on transaction. Now I could investigate and do the right thing or just hack a solution – I decided to hack a solution :)

    class ImportJob < ActiveRecord::Migration
    end

    Deriving ImportJob from Migration solved my problems. Now if anyone has a cleaner solution I will be happy to implement it but at least I solved my problem with transactions (and I hope this will be helpful for someone else too).

    Update: Tim pointed out some flaws in my code and some more testing revealed that I needed to have an exception to trigger a Rollback, so I modified the block:

    begin
      transaction do
        first.save!
        second.save!
        third.save!
        fourth.save!
      end
    rescue ActiveRecord::RecordInvalid => invalid
      # do whatever you wish to warn the user, or log something
    end

    This works fine, thanks Tim!

    6 Responses to “Transaction in Rails”

    1. Tim Says:

      Transactions roll back if you raise an exception within one. You’ll need to raise the exception if you want the transaction to roll back, and you’ll need to catch it yourself if you don’t want it to propagate further.

      I don’t think the documentation says anything to contradict this does it?

    2. Giovanni Intini Says:

      The documentation doesn’t say anything to contradict what you say.

      What it doesn’t say is that my code works. If one of the saves is false then it gets rolled back.

      It also doesn’t say I should catch it to avoid interrupting my application.

    3. Tim Says:

      This sounded curious so I thought I’d double check.

      There is a section called ‘Exception Handling’ at the bottom of the top part of the transaction document page:

      http://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html

      It says “Also have in mind that exceptions thrown within a transaction block will be propagated (after triggering the ROLLBACK), so you should be ready to catch those in your application code.”

      Perhaps it’s recent :)

      A transaction shouldn’t roll back unless something raises an exception – if you think your code rolls back a transaction without raising an exception then check it out more carefully. You may be mistaken, or it may be a bug in rails.

    4. Giovanni Intini Says:

      As we say in Italy “you put the cricket into my ear”, that means you convinced me to test the code some more.

      I’m pretty sure rollbacks are triggered this way because I did tests with live data, but who knows what’s happening under the hood? :)

    5. Giovanni Intini Says:

      Ok, I did some more testing and you were correct :)

      I’ll update the post with the right code.

    6. david Says:

      if FirstModel,SecondModel,ThirdModel,FourthModel all of them use same db connection, code like this will work right
      *Model.transaction do
      fourth.do_stuff
      third.save
      second.handle_your_stuff
      first.good_old_foo_bar
      end
      *=[First|Second|Third|Forth]