From db045dbbf60b53dbe013ef25554fd013baf88134 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 24 Nov 2004 01:04:44 +0000 Subject: Initial git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@4 5ecf4fe2-1ee6-0310-87b1-e25e094e27de --- activerecord/lib/active_record/transactions.rb | 119 +++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 activerecord/lib/active_record/transactions.rb (limited to 'activerecord/lib/active_record/transactions.rb') diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb new file mode 100644 index 0000000000..d440e74346 --- /dev/null +++ b/activerecord/lib/active_record/transactions.rb @@ -0,0 +1,119 @@ +require 'active_record/vendor/simple.rb' +require 'thread' + +module ActiveRecord + module Transactions # :nodoc: + TRANSACTION_MUTEX = Mutex.new + + def self.append_features(base) + super + base.extend(ClassMethods) + + base.class_eval do + alias_method :destroy_without_transactions, :destroy + alias_method :destroy, :destroy_with_transactions + + alias_method :save_without_transactions, :save + alias_method :save, :save_with_transactions + end + end + + # Transactions are protective blocks where SQL statements are only permanent if they can all succeed as one atomic action. + # The classic example is a transfer between two accounts where you can only have a deposit if the withdrawal succedded and + # vice versa. Transaction enforce the integrity of the database and guards the data against program errors or database break-downs. + # So basically you should use transaction blocks whenever you have a number of statements that must be executed together or + # not at all. Example: + # + # transaction do + # david.withdrawal(100) + # mary.deposit(100) + # end + # + # This example will only take money from David and give to Mary if neither +withdrawal+ nor +deposit+ raises an exception. + # Exceptions will force a ROLLBACK that returns the database to the state before the transaction was begun. Be aware, though, + # that the objects by default will _not_ have their instance data returned to their pre-transactional state. + # + # == Transactions are not distributed across database connections + # + # A transaction acts on a single database connection. If you have + # multiple class-specific databases, the transaction will not protect + # interaction among them. One workaround is to begin a transaction + # on each class whose models you alter: + # + # Student.transaction do + # Course.transaction do + # course.enroll(student) + # student.units += course.units + # end + # end + # + # This is a poor solution, but full distributed transactions are beyond + # the scope of Active Record. + # + # == Save and destroy are automatically wrapped in a transaction + # + # Both Base#save and Base#destroy come wrapped in a transaction that ensures that whatever you do in validations or callbacks + # will happen under the protected cover of a transaction. So you can use validations to check for values that the transaction + # depend on or you can raise exceptions in the callbacks to rollback. + # + # == Object-level transactions + # + # You can enable object-level transactions for Active Record objects, though. You do this by naming the each of the Active Records + # that you want to enable object-level transactions for, like this: + # + # Account.transaction(david, mary) do + # david.withdrawal(100) + # mary.deposit(100) + # end + # + # If the transaction fails, David and Mary will be returned to their pre-transactional state. No money will have changed hands in + # neither object nor database. + # + # == Exception handling + # + # 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. + # + # Tribute: Object-level transactions are implemented by Transaction::Simple by Austin Ziegler. + module ClassMethods + def transaction(*objects, &block) + TRANSACTION_MUTEX.lock + + begin + objects.each { |o| o.extend(Transaction::Simple) } + objects.each { |o| o.start_transaction } + + result = connection.transaction(&block) + + objects.each { |o| o.commit_transaction } + return result + rescue Exception => object_transaction_rollback + objects.each { |o| o.abort_transaction } + raise + ensure + TRANSACTION_MUTEX.unlock + end + end + end + + def transaction(*objects, &block) + self.class.transaction(*objects, &block) + end + + def destroy_with_transactions #:nodoc: + if TRANSACTION_MUTEX.locked? + destroy_without_transactions + else + transaction { destroy_without_transactions } + end + end + + def save_with_transactions(perform_validation = true) #:nodoc: + if TRANSACTION_MUTEX.locked? + save_without_transactions(perform_validation) + else + transaction { save_without_transactions(perform_validation) } + end + end + end +end -- cgit v1.2.3