root/trunk/activerecord/lib/active_record/transactions.rb
| Revision 4312, 4.9 kB (checked in by marcel, 4 years ago) |
|---|
| Line | |
|---|---|
| 1 | require 'active_record/vendor/simple.rb' |
| 2 | Transaction::Simple.send(:remove_method, :transaction) |
| 3 | require 'thread' |
| 4 | |
| 5 | module ActiveRecord |
| 6 | module Transactions # :nodoc: |
| 7 | TRANSACTION_MUTEX = Mutex.new |
| 8 | |
| 9 | class TransactionError < ActiveRecordError # :nodoc: |
| 10 | end |
| 11 | |
| 12 | def self.included(base) |
| 13 | base.extend(ClassMethods) |
| 14 | |
| 15 | base.class_eval do |
| 16 | [:destroy, :save].each do |method| |
| 17 | alias_method_chain method, :transactions |
| 18 | end |
| 19 | end |
| 20 | end |
| 21 | |
| 22 | # Transactions are protective blocks where SQL statements are only permanent if they can all succeed as one atomic action. |
| 23 | # The classic example is a transfer between two accounts where you can only have a deposit if the withdrawal succeeded and |
| 24 | # vice versa. Transactions enforce the integrity of the database and guard the data against program errors or database break-downs. |
| 25 | # So basically you should use transaction blocks whenever you have a number of statements that must be executed together or |
| 26 | # not at all. Example: |
| 27 | # |
| 28 | # transaction do |
| 29 | # david.withdrawal(100) |
| 30 | # mary.deposit(100) |
| 31 | # end |
| 32 | # |
| 33 | # This example will only take money from David and give to Mary if neither +withdrawal+ nor +deposit+ raises an exception. |
| 34 | # Exceptions will force a ROLLBACK that returns the database to the state before the transaction was begun. Be aware, though, |
| 35 | # that the objects by default will _not_ have their instance data returned to their pre-transactional state. |
| 36 | # |
| 37 | # == Transactions are not distributed across database connections |
| 38 | # |
| 39 | # A transaction acts on a single database connection. If you have |
| 40 | # multiple class-specific databases, the transaction will not protect |
| 41 | # interaction among them. One workaround is to begin a transaction |
| 42 | # on each class whose models you alter: |
| 43 | # |
| 44 | # Student.transaction do |
| 45 | # Course.transaction do |
| 46 | # course.enroll(student) |
| 47 | # student.units += course.units |
| 48 | # end |
| 49 | # end |
| 50 | # |
| 51 | # This is a poor solution, but full distributed transactions are beyond |
| 52 | # the scope of Active Record. |
| 53 | # |
| 54 | # == Save and destroy are automatically wrapped in a transaction |
| 55 | # |
| 56 | # Both Base#save and Base#destroy come wrapped in a transaction that ensures that whatever you do in validations or callbacks |
| 57 | # will happen under the protected cover of a transaction. So you can use validations to check for values that the transaction |
| 58 | # depend on or you can raise exceptions in the callbacks to rollback. |
| 59 | # |
| 60 | # == Object-level transactions |
| 61 | # |
| 62 | # You can enable object-level transactions for Active Record objects, though. You do this by naming each of the Active Records |
| 63 | # that you want to enable object-level transactions for, like this: |
| 64 | # |
| 65 | # Account.transaction(david, mary) do |
| 66 | # david.withdrawal(100) |
| 67 | # mary.deposit(100) |
| 68 | # end |
| 69 | # |
| 70 | # If the transaction fails, David and Mary will be returned to their pre-transactional state. No money will have changed hands in |
| 71 | # neither object nor database. |
| 72 | # |
| 73 | # == Exception handling |
| 74 | # |
| 75 | # Also have in mind that exceptions thrown within a transaction block will be propagated (after triggering the ROLLBACK), so you |
| 76 | # should be ready to catch those in your application code. |
| 77 | # |
| 78 | # Tribute: Object-level transactions are implemented by Transaction::Simple by Austin Ziegler. |
| 79 | module ClassMethods |
| 80 | def transaction(*objects, &block) |
| 81 | previous_handler = trap('TERM') { raise TransactionError, "Transaction aborted" } |
| 82 | lock_mutex |
| 83 | |
| 84 | begin |
| 85 | objects.each { |o| o.extend(Transaction::Simple) } |
| 86 | objects.each { |o| o.start_transaction } |
| 87 | |
| 88 | result = connection.transaction(Thread.current['start_db_transaction'], &block) |
| 89 | |
| 90 | objects.each { |o| o.commit_transaction } |
| 91 | return result |
| 92 | rescue Exception => object_transaction_rollback |
| 93 | objects.each { |o| o.abort_transaction } |
| 94 | raise |
| 95 | ensure |
| 96 | unlock_mutex |
| 97 | trap('TERM', previous_handler) |
| 98 | end |
| 99 | end |
| 100 | |
| 101 | def lock_mutex#:nodoc: |
| 102 | Thread.current['open_transactions'] ||= 0 |
| 103 | TRANSACTION_MUTEX.lock if Thread.current['open_transactions'] == 0 |
| 104 | Thread.current['start_db_transaction'] = (Thread.current['open_transactions'] == 0) |
| 105 | Thread.current['open_transactions'] += 1 |
| 106 | end |
| 107 | |
| 108 | def unlock_mutex#:nodoc: |
| 109 | Thread.current['open_transactions'] -= 1 |
| 110 | TRANSACTION_MUTEX.unlock if Thread.current['open_transactions'] == 0 |
| 111 | end |
| 112 | end |
| 113 | |
| 114 | def transaction(*objects, &block) |
| 115 | self.class.transaction(*objects, &block) |
| 116 | end |
| 117 | |
| 118 | def destroy_with_transactions #:nodoc: |
| 119 | transaction { destroy_without_transactions } |
| 120 | end |
| 121 | |
| 122 | def save_with_transactions(perform_validation = true) #:nodoc: |
| 123 | transaction { save_without_transactions(perform_validation) } |
| 124 | end |
| 125 | end |
| 126 | end |
Note: See TracBrowser for help on using the browser.