root/tags/rel_2-0-2/activerecord/lib/active_record/transactions.rb
| Revision 8240, 5.0 kB (checked in by xal, 1 year ago) |
|---|
| Line | |
|---|---|
| 1 | require 'thread' |
| 2 | |
| 3 | module ActiveRecord |
| 4 | module Transactions # :nodoc: |
| 5 | class TransactionError < ActiveRecordError # :nodoc: |
| 6 | end |
| 7 | |
| 8 | def self.included(base) |
| 9 | base.extend(ClassMethods) |
| 10 | |
| 11 | base.class_eval do |
| 12 | [:destroy, :save, :save!].each do |method| |
| 13 | alias_method_chain method, :transactions |
| 14 | end |
| 15 | end |
| 16 | end |
| 17 | |
| 18 | # Transactions are protective blocks where SQL statements are only permanent if they can all succeed as one atomic action. |
| 19 | # The classic example is a transfer between two accounts where you can only have a deposit if the withdrawal succeeded and |
| 20 | # vice versa. Transactions enforce the integrity of the database and guard the data against program errors or database break-downs. |
| 21 | # So basically you should use transaction blocks whenever you have a number of statements that must be executed together or |
| 22 | # not at all. Example: |
| 23 | # |
| 24 | # transaction do |
| 25 | # david.withdrawal(100) |
| 26 | # mary.deposit(100) |
| 27 | # end |
| 28 | # |
| 29 | # This example will only take money from David and give to Mary if neither +withdrawal+ nor +deposit+ raises an exception. |
| 30 | # Exceptions will force a ROLLBACK that returns the database to the state before the transaction was begun. Be aware, though, |
| 31 | # that the objects by default will _not_ have their instance data returned to their pre-transactional state. |
| 32 | # |
| 33 | # == Different ActiveRecord classes in a single transaction |
| 34 | # |
| 35 | # Though the transaction class method is called on some ActiveRecord class, |
| 36 | # the objects within the transaction block need not all be instances of |
| 37 | # that class. |
| 38 | # In this example a <tt>Balance</tt> record is transactionally saved even |
| 39 | # though <tt>transaction</tt> is called on the <tt>Account</tt> class: |
| 40 | # |
| 41 | # Account.transaction do |
| 42 | # balance.save! |
| 43 | # account.save! |
| 44 | # end |
| 45 | # |
| 46 | # == Transactions are not distributed across database connections |
| 47 | # |
| 48 | # A transaction acts on a single database connection. If you have |
| 49 | # multiple class-specific databases, the transaction will not protect |
| 50 | # interaction among them. One workaround is to begin a transaction |
| 51 | # on each class whose models you alter: |
| 52 | # |
| 53 | # Student.transaction do |
| 54 | # Course.transaction do |
| 55 | # course.enroll(student) |
| 56 | # student.units += course.units |
| 57 | # end |
| 58 | # end |
| 59 | # |
| 60 | # This is a poor solution, but full distributed transactions are beyond |
| 61 | # the scope of Active Record. |
| 62 | # |
| 63 | # == Save and destroy are automatically wrapped in a transaction |
| 64 | # |
| 65 | # Both Base#save and Base#destroy come wrapped in a transaction that ensures that whatever you do in validations or callbacks |
| 66 | # will happen under the protected cover of a transaction. So you can use validations to check for values that the transaction |
| 67 | # depends on or you can raise exceptions in the callbacks to rollback. |
| 68 | # |
| 69 | # == Exception handling |
| 70 | # |
| 71 | # Also have in mind that exceptions thrown within a transaction block will be propagated (after triggering the ROLLBACK), so you |
| 72 | # should be ready to catch those in your application code. One exception is the ActiveRecord::Rollback exception, which will |
| 73 | # trigger a ROLLBACK when raised, but not be re-raised by the transaction block. |
| 74 | module ClassMethods |
| 75 | def transaction(&block) |
| 76 | previous_handler = trap('TERM') { raise TransactionError, "Transaction aborted" } |
| 77 | increment_open_transactions |
| 78 | |
| 79 | begin |
| 80 | connection.transaction(Thread.current['start_db_transaction'], &block) |
| 81 | ensure |
| 82 | decrement_open_transactions |
| 83 | trap('TERM', previous_handler) |
| 84 | end |
| 85 | end |
| 86 | |
| 87 | private |
| 88 | def increment_open_transactions #:nodoc: |
| 89 | open = Thread.current['open_transactions'] ||= 0 |
| 90 | Thread.current['start_db_transaction'] = open.zero? |
| 91 | Thread.current['open_transactions'] = open + 1 |
| 92 | end |
| 93 | |
| 94 | def decrement_open_transactions #:nodoc: |
| 95 | Thread.current['open_transactions'] -= 1 |
| 96 | end |
| 97 | end |
| 98 | |
| 99 | def transaction(&block) |
| 100 | self.class.transaction(&block) |
| 101 | end |
| 102 | |
| 103 | def destroy_with_transactions #:nodoc: |
| 104 | transaction { destroy_without_transactions } |
| 105 | end |
| 106 | |
| 107 | def save_with_transactions(perform_validation = true) #:nodoc: |
| 108 | rollback_active_record_state! { transaction { save_without_transactions(perform_validation) } } |
| 109 | end |
| 110 | |
| 111 | def save_with_transactions! #:nodoc: |
| 112 | rollback_active_record_state! { transaction { save_without_transactions! } } |
| 113 | end |
| 114 | |
| 115 | # Reset id and @new_record if the transaction rolls back. |
| 116 | def rollback_active_record_state! |
| 117 | id_present = has_attribute?(self.class.primary_key) |
| 118 | previous_id = id |
| 119 | previous_new_record = @new_record |
| 120 | yield |
| 121 | rescue Exception |
| 122 | @new_record = previous_new_record |
| 123 | if id_present |
| 124 | self.id = previous_id |
| 125 | else |
| 126 | @attributes.delete(self.class.primary_key) |
| 127 | @attributes_cache.delete(self.class.primary_key) |
| 128 | end |
| 129 | raise |
| 130 | end |
| 131 | end |
| 132 | end |
Note: See TracBrowser for help on using the browser.