Recipe 13.16. Using Transactions in ActiveRecord
Problem
You want to perform database operations as a group: if one of the operations fails, it should be as though none of them had ever happened.
Solution
Include active_record/
transactions, and you'll give each ActiveRecord class a TRansaction method. This method starts a database transaction, runs a code block, then commits the transaction. If the code block throws an exception, the database transaction is rolled back.
Here's some simple initialization code to give ActiveRecord access to the database tables for the weblog system first seen in Recipe 13.11:
require 'cookbook_dbconnect'
activerecord_connect # See chapter introduction
class User < ActiveRecord::Base
has_and_belongs_to_many :blog_posts
end
class BlogPost < ActiveRecord::Base
has_and_belongs_to_many :authors, :class_name => 'User'
end
The create_from_new_author method below creates a new entry in the users table, then associates it with a new entry in the blog_posts table. But there's a 50% chance that an exception will be thrown right after the new author is created. If that happens, the author creation is rolled back: in effect, it never happened.
require 'active_record/
transactions'
class BlogPost
def BlogPost.create_from_new_author(author_name, title, content)
transaction do
author = User.create(:name => author_name)
raise 'Random failure!' if rand(2) == 0
create(:authors => [author], :title => title, :content => content)
end
end
end
Since the whole operation is enclosed within a TRansaction block, an exception won't leave the database in a state where the author has been created but the blog entry hasn't:
BlogPost.create_from_new_author('Carol', 'The End Is Near',
'A few more facts of doom…')
# => #<BlogPost:0xb78b7c7c … >
# The method succeeded; Carol's in the database:
User.find(:first, :conditions=>"name='Carol'")
# => #<User:0xb7888ae4 @attributes={"name"=>"Carol", … }>
# Let's do another one…
BlogPost.create_from_new_author('David', 'The End: A Rebuttal',
'The end is actually quite far away…')
# RuntimeError: Random failure!
# The method failed; David's not in the database:
User.find(:first, :conditions=>"name='David'")
# => nil
Discussion
You should use database
transactions whenever one database operation puts the database into an inconsistent state, and a second operation brings the database back into consistency. All kinds of things can go wrong between the first and second operation. The database server might crash or your application might throw an exception. The Ruby interpreter might decide to stop running your thread for an arbitrarily long time, giving other threads a chance to marvel at the inconsistent state of the database. An inconsistent database can cause problems that are very difficult to debug and fix.
ActiveRecord's
transactions piggyback on top of database transactions, so they'll only work if your database supports transactions. Most databases do these days; chances are you won't have trouble unless you're using a MySQL database and not using InnoDB tables. However, most of the open source databases don't support nested
transactions, so you're limited to one transaction at a time with a given database connection.
In addition to a code block, the transaction method can take a number of ActiveRecord objects. These are the objects that participate in the transaction. If the transaction fails, then not only will the database be restored to its previous state, so will the member variables of the objects.
This is useful if you're defining a method that modifies ActiveRecord objects themselves, not just the database representations of those objects. For instance, a shopping cart object might keep a running total that's consulted by the application, but not stored in the database.
See Also
|