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 youll 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.

Heres 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 theres 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 leave the database in a state where the author has been created but the blog entry hasn :

BlogPost.create_from_new_author(Carol, The End Is Near, A few more facts of doom…) # => # # The method succeeded; Carols in the database: User.find(:first, :conditions=>"name=Carol") # => #"Carol", … }> # Lets 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; Davids 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.

ActiveRecords transactions piggyback on top of database transactions, so theyll only work if your database supports transactions. Most databases do these days; chances are you won have trouble unless you e using a MySQL database and not using InnoDB tables. However, most of the open source databases don support nested transactions, so you e 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 e 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 thats consulted by the application, but not stored in the database.

See Also

Категории