Skip to content
This repository
Browse code

Add after_commit and after_rollback callbacks to ActiveRecord that ar…

…e called after transactions either commit or rollback on all records saved or destroyed in the transaction.

[#2991 state:committed]

Signed-off-by: Jeremy Kemper <jeremy@bitsweat.net>
  • Loading branch information...
commit da840d13da865331297d5287391231b1ed39721b 1 parent 20f0e9f
Brian Durand authored June 02, 2009 jeremy committed April 29, 2010
2  activerecord/CHANGELOG
... ...
@@ -1,5 +1,7 @@
1 1
 *Rails 3.0.0 [beta 4/release candidate] (unreleased)*
2 2
 
  3
+* New callbacks: after_commit and after_rollback. Do expensive operations like image thumbnailing after_commit instead of after_save.  #2991 [Brian Durand]
  4
+
3 5
 * Serialized attributes are not converted to YAML if they are any of the formats that can be serialized to XML (like Hash, Array and Strings). [José Valim]
4 6
 
5 7
 * Destroy uses optimistic locking. If lock_version on the record you're destroying doesn't match lock_version in the database, a StaleObjectError is raised.  #1966 [Curtis Hawthorne]
56  activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -122,6 +122,8 @@ def transaction(options = {})
122 122
         requires_new = options[:requires_new] || !last_transaction_joinable
123 123
 
124 124
         transaction_open = false
  125
+        @_current_transaction_records ||= []
  126
+
125 127
         begin
126 128
           if block_given?
127 129
             if requires_new || open_transactions == 0
@@ -132,6 +134,7 @@ def transaction(options = {})
132 134
               end
133 135
               increment_open_transactions
134 136
               transaction_open = true
  137
+              @_current_transaction_records.push([])
135 138
             end
136 139
             yield
137 140
           end
@@ -141,8 +144,10 @@ def transaction(options = {})
141 144
             decrement_open_transactions
142 145
             if open_transactions == 0
143 146
               rollback_db_transaction
  147
+              rollback_transaction_records(true)
144 148
             else
145 149
               rollback_to_savepoint
  150
+              rollback_transaction_records(false)
146 151
             end
147 152
           end
148 153
           raise unless database_transaction_rollback.is_a?(ActiveRecord::Rollback)
@@ -157,20 +162,35 @@ def transaction(options = {})
157 162
           begin
158 163
             if open_transactions == 0
159 164
               commit_db_transaction
  165
+              commit_transaction_records
160 166
             else
161 167
               release_savepoint
  168
+              save_point_records = @_current_transaction_records.pop
  169
+              unless save_point_records.blank?
  170
+                @_current_transaction_records.push([]) if @_current_transaction_records.empty?
  171
+                @_current_transaction_records.last.concat(save_point_records)
  172
+              end
162 173
             end
163 174
           rescue Exception => database_transaction_rollback
164 175
             if open_transactions == 0
165 176
               rollback_db_transaction
  177
+              rollback_transaction_records(true)
166 178
             else
167 179
               rollback_to_savepoint
  180
+              rollback_transaction_records(false)
168 181
             end
169 182
             raise
170 183
           end
171 184
         end
172 185
       end
173 186
 
  187
+      # Register a record with the current transaction so that its after_commit and after_rollback callbacks
  188
+      # can be called.
  189
+      def add_transaction_record(record)
  190
+        last_batch = @_current_transaction_records.last
  191
+        last_batch << record if last_batch
  192
+      end
  193
+
174 194
       # Begins the transaction (and turns off auto-committing).
175 195
       def begin_db_transaction()    end
176 196
 
@@ -268,6 +288,42 @@ def sanitize_limit(limit)
268 288
             limit.to_i
269 289
           end
270 290
         end
  291
+
  292
+        # Send a rollback message to all records after they have been rolled back. If rollback
  293
+        # is false, only rollback records since the last save point.
  294
+        def rollback_transaction_records(rollback) #:nodoc
  295
+          if rollback
  296
+            records = @_current_transaction_records.flatten
  297
+            @_current_transaction_records.clear
  298
+          else
  299
+            records = @_current_transaction_records.pop
  300
+          end
  301
+
  302
+          unless records.blank?
  303
+            records.uniq.each do |record|
  304
+              begin
  305
+                record.rolledback!(rollback)
  306
+              rescue Exception => e
  307
+                record.logger.error(e) if record.respond_to?(:logger)
  308
+              end
  309
+            end
  310
+          end
  311
+        end
  312
+
  313
+        # Send a commit message to all records after they have been committed.
  314
+        def commit_transaction_records #:nodoc
  315
+          records = @_current_transaction_records.flatten
  316
+          @_current_transaction_records.clear
  317
+          unless records.blank?
  318
+            records.uniq.each do |record|
  319
+              begin
  320
+                record.committed!
  321
+              rescue Exception => e
  322
+                record.logger.error(e) if record.respond_to?(:logger)
  323
+              end
  324
+            end
  325
+          end
  326
+        end
271 327
     end
272 328
   end
273 329
 end
131  activerecord/lib/active_record/transactions.rb
@@ -12,6 +12,9 @@ class TransactionError < ActiveRecordError # :nodoc:
12 12
       [:destroy, :save, :save!].each do |method|
13 13
         alias_method_chain method, :transactions
14 14
       end
  15
+
  16
+      define_model_callbacks :commit, :commit_on_update, :commit_on_create, :commit_on_destroy, :only => :after
  17
+      define_model_callbacks :rollback, :rollback_on_update, :rollback_on_create, :rollback_on_destroy
15 18
     end
16 19
 
17 20
     # Transactions are protective blocks where SQL statements are only permanent
@@ -108,7 +111,7 @@ class TransactionError < ActiveRecordError # :nodoc:
108 111
     #     rescue ActiveRecord::StatementInvalid
109 112
     #       # ...which we ignore.
110 113
     #     end
111  
-    #     
  114
+    #
112 115
     #     # On PostgreSQL, the transaction is now unusable. The following
113 116
     #     # statement will cause a PostgreSQL error, even though the unique
114 117
     #     # constraint is no longer violated:
@@ -132,7 +135,7 @@ class TransactionError < ActiveRecordError # :nodoc:
132 135
     #       raise ActiveRecord::Rollback
133 136
     #     end
134 137
     #   end
135  
-    #   
  138
+    #
136 139
     #   User.find(:all)  # => empty
137 140
     #
138 141
     # It is also possible to requires a sub-transaction by passing
@@ -147,7 +150,7 @@ class TransactionError < ActiveRecordError # :nodoc:
147 150
     #       raise ActiveRecord::Rollback
148 151
     #     end
149 152
     #   end
150  
-    #   
  153
+    #
151 154
     #   User.find(:all)  # => Returns only Kotori
152 155
     #
153 156
     # Most databases don't support true nested transactions. At the time of
@@ -157,6 +160,26 @@ class TransactionError < ActiveRecordError # :nodoc:
157 160
     # http://dev.mysql.com/doc/refman/5.0/en/savepoints.html
158 161
     # for more information about savepoints.
159 162
     #
  163
+    # === Callbacks
  164
+    #
  165
+    # There are two types of callbacks associated with committing and rolling back transactions:
  166
+    # after_commit and after_rollback.
  167
+    #
  168
+    # The after_commit callbacks are called on every record saved or destroyed within a
  169
+    # transaction immediately after the  transaction is committed. The after_rollback callbacks
  170
+    # are called on every record saved or destroyed within a transaction immediately after the
  171
+    # transaction or savepoint is rolled back.
  172
+    #
  173
+    # Additionally, there are callbacks for after_commit_on_create, after_rollback_on_create,
  174
+    # after_commit_on_update, after_rollback_on_update, after_commit_on_destroy, and
  175
+    # after_rollback_on_destroy which are only called if a record is created, updated or destroyed
  176
+    # in the transaction.
  177
+    #
  178
+    # These callbacks are useful for interacting with other systems since you will be guaranteed
  179
+    # that the callback is only executed when the database is in a permanent state. For example,
  180
+    # after_commit is a good spot to put in a hook to clearing a cache since clearing it from
  181
+    # within a transaction could trigger the cache to be regenerated before the database is updated.
  182
+    #
160 183
     # === Caveats
161 184
     #
162 185
     # If you're on MySQL, then do not use DDL operations in nested transactions
@@ -166,7 +189,7 @@ class TransactionError < ActiveRecordError # :nodoc:
166 189
     # is finished and tries to release the savepoint it created earlier, a
167 190
     # database error will occur because the savepoint has already been
168 191
     # automatically released. The following example demonstrates the problem:
169  
-    # 
  192
+    #
170 193
     #   Model.connection.transaction do                           # BEGIN
171 194
     #     Model.connection.transaction(:requires_new => true) do  # CREATE SAVEPOINT active_record_1
172 195
     #       Model.connection.create_table(...)                    # active_record_1 now automatically released
@@ -197,24 +220,55 @@ def save_with_transactions(*args) #:nodoc:
197 220
     end
198 221
 
199 222
     def save_with_transactions! #:nodoc:
200  
-      rollback_active_record_state! { self.class.transaction { save_without_transactions! } }
  223
+      with_transaction_returning_status(:save_without_transactions!)
201 224
     end
202 225
 
203 226
     # Reset id and @new_record if the transaction rolls back.
204 227
     def rollback_active_record_state!
205  
-      id_present = has_attribute?(self.class.primary_key)
206  
-      previous_id = id
207  
-      previous_new_record = new_record?
  228
+      remember_transaction_record_state
208 229
       yield
209 230
     rescue Exception
210  
-      @new_record = previous_new_record
211  
-      if id_present
212  
-        self.id = previous_id
  231
+      restore_transaction_record_state
  232
+      raise
  233
+    ensure
  234
+      clear_transaction_record_state
  235
+    end
  236
+
  237
+    # Call the after_commit callbacks
  238
+    def committed! #:nodoc:
  239
+      if transaction_record_state(:new_record)
  240
+        _run_commit_on_create_callbacks
  241
+      elsif transaction_record_state(:destroyed)
  242
+        _run_commit_on_destroy_callbacks
213 243
       else
214  
-        @attributes.delete(self.class.primary_key)
215  
-        @attributes_cache.delete(self.class.primary_key)
  244
+        _run_commit_on_update_callbacks
  245
+      end
  246
+      _run_commit_callbacks
  247
+    ensure
  248
+      clear_transaction_record_state
  249
+    end
  250
+
  251
+    # Call the after rollback callbacks. The restore_state argument indicates if the record
  252
+    # state should be rolled back to the beginning or just to the last savepoint.
  253
+    def rolledback!(force_restore_state = false) #:nodoc:
  254
+      if transaction_record_state(:new_record)
  255
+        _run_rollback_on_create_callbacks
  256
+      elsif transaction_record_state(:destroyed)
  257
+        _run_rollback_on_destroy_callbacks
  258
+      else
  259
+        _run_rollback_on_update_callbacks
  260
+      end
  261
+      _run_rollback_callbacks
  262
+    ensure
  263
+      restore_transaction_record_state(force_restore_state)
  264
+    end
  265
+
  266
+    # Add the record to the current transaction so that the :after_rollback and :after_commit callbacks
  267
+    # can be called.
  268
+    def add_to_transaction
  269
+      if self.class.connection.add_transaction_record(self)
  270
+        remember_transaction_record_state
216 271
       end
217  
-      raise
218 272
     end
219 273
 
220 274
     # Executes +method+ within a transaction and captures its return value as a
@@ -226,10 +280,59 @@ def rollback_active_record_state!
226 280
     def with_transaction_returning_status(method, *args)
227 281
       status = nil
228 282
       self.class.transaction do
  283
+        add_to_transaction
229 284
         status = send(method, *args)
230 285
         raise ActiveRecord::Rollback unless status
231 286
       end
232 287
       status
233 288
     end
  289
+
  290
+    protected
  291
+
  292
+    # Save the new record state and id of a record so it can be restored later if a transaction fails.
  293
+    def remember_transaction_record_state #:nodoc
  294
+      @_start_transaction_state ||= {}
  295
+      unless @_start_transaction_state.include?(:new_record)
  296
+        @_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key)
  297
+        @_start_transaction_state[:new_record] = @new_record
  298
+      end
  299
+      unless @_start_transaction_state.include?(:destroyed)
  300
+        @_start_transaction_state[:destroyed] = @new_record
  301
+      end
  302
+      @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1
  303
+    end
  304
+
  305
+    # Clear the new record state and id of a record.
  306
+    def clear_transaction_record_state #:nodoc
  307
+      if defined?(@_start_transaction_state)
  308
+        @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
  309
+        remove_instance_variable(:@_start_transaction_state) if @_start_transaction_state[:level] < 1
  310
+      end
  311
+    end
  312
+
  313
+    # Restore the new record state and id of a record that was previously saved by a call to save_record_state.
  314
+    def restore_transaction_record_state(force = false) #:nodoc
  315
+      if defined?(@_start_transaction_state)
  316
+        @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
  317
+        if @_start_transaction_state[:level] < 1
  318
+          restore_state = remove_instance_variable(:@_start_transaction_state)
  319
+          if restore_state
  320
+            @new_record = restore_state[:new_record]
  321
+            @destroyed = restore_state[:destroyed]
  322
+            if restore_state[:id]
  323
+              self.id = restore_state[:id]
  324
+            else
  325
+              @attributes.delete(self.class.primary_key)
  326
+              @attributes_cache.delete(self.class.primary_key)
  327
+            end
  328
+          end
  329
+        end
  330
+      end
  331
+    end
  332
+
  333
+    # Determine if a record was created or destroyed in a transaction. State should be one of :new_record or :destroyed.
  334
+    def transaction_record_state(state) #:nodoc
  335
+      @_start_transaction_state[state] if defined?(@_start_transaction_state)
  336
+    end
234 337
   end
235 338
 end
244  activerecord/test/cases/transaction_callbacks_test.rb
... ...
@@ -0,0 +1,244 @@
  1
+require "cases/helper"
  2
+require 'models/topic'
  3
+require 'models/reply'
  4
+
  5
+class TransactionCallbacksTest < ActiveRecord::TestCase
  6
+  self.use_transactional_fixtures = false
  7
+  fixtures :topics
  8
+
  9
+  class TopicWithCallbacks < ActiveRecord::Base
  10
+    set_table_name :topics
  11
+
  12
+    after_commit{|record| record.send(:do_after_commit, nil)}
  13
+    after_commit_on_create{|record| record.send(:do_after_commit, :create)}
  14
+    after_commit_on_update{|record| record.send(:do_after_commit, :update)}
  15
+    after_commit_on_destroy{|record| record.send(:do_after_commit, :destroy)}
  16
+    after_rollback{|record| record.send(:do_after_rollback, nil)}
  17
+    after_rollback_on_create{|record| record.send(:do_after_rollback, :create)}
  18
+    after_rollback_on_update{|record| record.send(:do_after_rollback, :update)}
  19
+    after_rollback_on_destroy{|record| record.send(:do_after_rollback, :destroy)}
  20
+
  21
+    def history
  22
+      @history ||= []
  23
+    end
  24
+
  25
+    def after_commit_block(on = nil, &block)
  26
+      @after_commit ||= {}
  27
+      @after_commit[on] ||= []
  28
+      @after_commit[on] << block
  29
+    end
  30
+
  31
+    def after_rollback_block(on = nil, &block)
  32
+      @after_rollback ||= {}
  33
+      @after_rollback[on] ||= []
  34
+      @after_rollback[on] << block
  35
+    end
  36
+
  37
+    def do_after_commit(on)
  38
+      blocks = @after_commit[on] if defined?(@after_commit)
  39
+      blocks.each{|b| b.call(self)} if blocks
  40
+    end
  41
+
  42
+    def do_after_rollback(on)
  43
+      blocks = @after_rollback[on] if defined?(@after_rollback)
  44
+      blocks.each{|b| b.call(self)} if blocks
  45
+    end
  46
+  end
  47
+
  48
+  def setup
  49
+    @first, @second = TopicWithCallbacks.find(1, 3).sort_by { |t| t.id }
  50
+  end
  51
+
  52
+  def test_call_after_commit_after_transaction_commits
  53
+    @first.after_commit_block{|r| r.history << :after_commit}
  54
+    @first.after_rollback_block{|r| r.history << :after_rollback}
  55
+
  56
+    @first.save!
  57
+    assert @first.history, [:after_commit]
  58
+  end
  59
+
  60
+  def test_only_call_after_commit_on_update_after_transaction_commits_for_existing_record
  61
+    commit_callback = []
  62
+    @first.after_commit_block(:create){|r| r.history << :commit_on_create}
  63
+    @first.after_commit_block(:update){|r| r.history << :commit_on_update}
  64
+    @first.after_commit_block(:destroy){|r| r.history << :commit_on_destroy}
  65
+    @first.after_commit_block(:create){|r| r.history << :rollback_on_create}
  66
+    @first.after_commit_block(:update){|r| r.history << :rollback_on_update}
  67
+    @first.after_commit_block(:destroy){|r| r.history << :rollback_on_destroy}
  68
+
  69
+    @first.save!
  70
+    assert @first.history, [:commit_on_update]
  71
+  end
  72
+
  73
+  def test_only_call_after_commit_on_destroy_after_transaction_commits_for_destroyed_record
  74
+    commit_callback = []
  75
+    @first.after_commit_block(:create){|r| r.history << :commit_on_create}
  76
+    @first.after_commit_block(:update){|r| r.history << :commit_on_update}
  77
+    @first.after_commit_block(:destroy){|r| r.history << :commit_on_destroy}
  78
+    @first.after_commit_block(:create){|r| r.history << :rollback_on_create}
  79
+    @first.after_commit_block(:update){|r| r.history << :rollback_on_update}
  80
+    @first.after_commit_block(:destroy){|r| r.history << :rollback_on_destroy}
  81
+
  82
+    @first.destroy
  83
+    assert @first.history, [:commit_on_destroy]
  84
+  end
  85
+
  86
+  def test_only_call_after_commit_on_create_after_transaction_commits_for_new_record
  87
+    @new_record = TopicWithCallbacks.new(:title => "New topic", :written_on => Date.today)
  88
+    @new_record.after_commit_block(:create){|r| r.history << :commit_on_create}
  89
+    @new_record.after_commit_block(:update){|r| r.history << :commit_on_update}
  90
+    @new_record.after_commit_block(:destroy){|r| r.history << :commit_on_destroy}
  91
+    @new_record.after_commit_block(:create){|r| r.history << :rollback_on_create}
  92
+    @new_record.after_commit_block(:update){|r| r.history << :rollback_on_update}
  93
+    @new_record.after_commit_block(:destroy){|r| r.history << :rollback_on_destroy}
  94
+
  95
+    @new_record.save!
  96
+    assert @new_record.history, [:commit_on_create]
  97
+  end
  98
+
  99
+  def test_call_after_rollback_after_transaction_rollsback
  100
+    @first.after_commit_block{|r| r.history << :after_commit}
  101
+    @first.after_rollback_block{|r| r.history << :after_rollback}
  102
+
  103
+    Topic.transaction do
  104
+      @first.save!
  105
+      raise ActiveRecord::Rollback
  106
+    end
  107
+
  108
+    assert @first.history, [:after_rollback]
  109
+  end
  110
+
  111
+  def test_only_call_after_rollback_on_update_after_transaction_rollsback_for_existing_record
  112
+    commit_callback = []
  113
+    @first.after_commit_block(:create){|r| r.history << :commit_on_create}
  114
+    @first.after_commit_block(:update){|r| r.history << :commit_on_update}
  115
+    @first.after_commit_block(:destroy){|r| r.history << :commit_on_destroy}
  116
+    @first.after_commit_block(:create){|r| r.history << :rollback_on_create}
  117
+    @first.after_commit_block(:update){|r| r.history << :rollback_on_update}
  118
+    @first.after_commit_block(:destroy){|r| r.history << :rollback_on_destroy}
  119
+
  120
+    Topic.transaction do
  121
+      @first.save!
  122
+      raise ActiveRecord::Rollback
  123
+    end
  124
+
  125
+    assert @first.history, [:rollback_on_update]
  126
+  end
  127
+
  128
+  def test_only_call_after_rollback_on_destroy_after_transaction_rollsback_for_destroyed_record
  129
+    commit_callback = []
  130
+    @first.after_commit_block(:create){|r| r.history << :commit_on_create}
  131
+    @first.after_commit_block(:update){|r| r.history << :commit_on_update}
  132
+    @first.after_commit_block(:destroy){|r| r.history << :commit_on_update}
  133
+    @first.after_commit_block(:create){|r| r.history << :rollback_on_create}
  134
+    @first.after_commit_block(:update){|r| r.history << :rollback_on_update}
  135
+    @first.after_commit_block(:destroy){|r| r.history << :rollback_on_destroy}
  136
+
  137
+    Topic.transaction do
  138
+      @first.destroy
  139
+      raise ActiveRecord::Rollback
  140
+    end
  141
+
  142
+    assert @first.history, [:rollback_on_destroy]
  143
+  end
  144
+
  145
+  def test_only_call_after_rollback_on_create_after_transaction_rollsback_for_new_record
  146
+    @new_record = TopicWithCallbacks.new(:title => "New topic", :written_on => Date.today)
  147
+    @new_record.after_commit_block(:create){|r| r.history << :commit_on_create}
  148
+    @new_record.after_commit_block(:update){|r| r.history << :commit_on_update}
  149
+    @new_record.after_commit_block(:destroy){|r| r.history << :commit_on_destroy}
  150
+    @new_record.after_commit_block(:create){|r| r.history << :rollback_on_create}
  151
+    @new_record.after_commit_block(:update){|r| r.history << :rollback_on_update}
  152
+    @new_record.after_commit_block(:destroy){|r| r.history << :rollback_on_destroy}
  153
+
  154
+    Topic.transaction do
  155
+      @new_record.save!
  156
+      raise ActiveRecord::Rollback
  157
+    end
  158
+
  159
+    assert @new_record.history, [:rollback_on_create]
  160
+  end
  161
+
  162
+  def test_call_after_rollback_when_commit_fails
  163
+    @first.connection.class.send(:alias_method, :real_method_commit_db_transaction, :commit_db_transaction)
  164
+    begin
  165
+      @first.connection.class.class_eval do
  166
+        def commit_db_transaction; raise "boom!"; end
  167
+      end
  168
+
  169
+      @first.after_commit_block{|r| r.history << :after_commit}
  170
+      @first.after_rollback_block{|r| r.history << :after_rollback}
  171
+
  172
+      assert !@first.save rescue nil
  173
+      assert @first.history == [:after_rollback]
  174
+    ensure
  175
+      @first.connection.class.send(:remove_method, :commit_db_transaction)
  176
+      @first.connection.class.send(:alias_method, :commit_db_transaction, :real_method_commit_db_transaction)
  177
+    end
  178
+  end
  179
+
  180
+  def test_only_call_after_rollback_on_records_rolled_back_to_a_savepoint
  181
+    def @first.rollbacks(i=0); @rollbacks ||= 0; @rollbacks += i if i; end
  182
+    def @first.commits(i=0); @commits ||= 0; @commits += i if i; end
  183
+    @first.after_rollback_block{|r| r.rollbacks(1)}
  184
+    @first.after_commit_block{|r| r.commits(1)}
  185
+
  186
+    def @second.rollbacks(i=0); @rollbacks ||= 0; @rollbacks += i if i; end
  187
+    def @second.commits(i=0); @commits ||= 0; @commits += i if i; end
  188
+    @second.after_rollback_block{|r| r.rollbacks(1)}
  189
+    @second.after_commit_block{|r| r.commits(1)}
  190
+
  191
+    Topic.transaction do
  192
+      @first.save!
  193
+      Topic.transaction(:requires_new => true) do
  194
+        @second.save!
  195
+        raise ActiveRecord::Rollback
  196
+      end
  197
+    end
  198
+
  199
+    assert 1, @first.commits
  200
+    assert 0, @first.rollbacks
  201
+    assert 1, @second.commits
  202
+    assert 1, @second.rollbacks
  203
+  end
  204
+
  205
+  def test_only_call_after_rollback_on_records_rolled_back_to_a_savepoint_when_release_savepoint_fails
  206
+    def @first.rollbacks(i=0); @rollbacks ||= 0; @rollbacks += i if i; end
  207
+    def @first.commits(i=0); @commits ||= 0; @commits += i if i; end
  208
+
  209
+    @second.after_rollback_block{|r| r.rollbacks(1)}
  210
+    @second.after_commit_block{|r| r.commits(1)}
  211
+
  212
+    Topic.transaction do
  213
+      @first.save
  214
+      Topic.transaction(:requires_new => true) do
  215
+        @first.save!
  216
+        raise ActiveRecord::Rollback
  217
+      end
  218
+      Topic.transaction(:requires_new => true) do
  219
+        @first.save!
  220
+        raise ActiveRecord::Rollback
  221
+      end
  222
+    end
  223
+
  224
+    assert 1, @first.commits
  225
+    assert 2, @first.rollbacks
  226
+  end
  227
+
  228
+  def test_after_transaction_callbacks_should_not_raise_errors
  229
+    def @first.last_after_transaction_error=(e); @last_transaction_error = e; end
  230
+    def @first.last_after_transaction_error; @last_transaction_error; end
  231
+    @first.after_commit_block{|r| r.last_after_transaction_error = :commit; raise "fail!";}
  232
+    @first.after_rollback_block{|r| r.last_after_transaction_error = :rollback; raise "fail!";}
  233
+
  234
+    @first.save!
  235
+    assert_equal @first.last_after_transaction_error, :commit
  236
+
  237
+    Topic.transaction do
  238
+      @first.save!
  239
+      raise ActiveRecord::Rollback
  240
+    end
  241
+
  242
+    assert_equal @first.last_after_transaction_error, :rollback
  243
+  end
  244
+end
61  activerecord/test/cases/transactions_test.rb
@@ -262,22 +262,22 @@ def test_no_savepoint_in_nested_transaction_without_force
262 262
     assert !@first.reload.approved?
263 263
     assert !@second.reload.approved?
264 264
   end if Topic.connection.supports_savepoints?
265  
-  
  265
+
266 266
   def test_many_savepoints
267 267
     Topic.transaction do
268 268
       @first.content = "One"
269 269
       @first.save!
270  
-      
  270
+
271 271
       begin
272 272
         Topic.transaction :requires_new => true do
273 273
           @first.content = "Two"
274 274
           @first.save!
275  
-          
  275
+
276 276
           begin
277 277
             Topic.transaction :requires_new => true do
278 278
               @first.content = "Three"
279 279
               @first.save!
280  
-              
  280
+
281 281
               begin
282 282
                 Topic.transaction :requires_new => true do
283 283
                   @first.content = "Four"
@@ -286,22 +286,22 @@ def test_many_savepoints
286 286
                 end
287 287
               rescue
288 288
               end
289  
-              
  289
+
290 290
               @three = @first.reload.content
291 291
               raise
292 292
             end
293 293
           rescue
294 294
           end
295  
-          
  295
+
296 296
           @two = @first.reload.content
297 297
           raise
298 298
         end
299 299
       rescue
300 300
       end
301  
-      
  301
+
302 302
       @one = @first.reload.content
303 303
     end
304  
-    
  304
+
305 305
     assert_equal "One", @one
306 306
     assert_equal "Two", @two
307 307
     assert_equal "Three", @three
@@ -319,7 +319,34 @@ def test_rollback_when_commit_raises
319 319
       end
320 320
     end
321 321
   end
322  
-  
  322
+
  323
+  def test_restore_active_record_state_for_all_records_in_a_transaction
  324
+    topic_1 = Topic.new(:title => 'test_1')
  325
+    topic_2 = Topic.new(:title => 'test_2')
  326
+    Topic.transaction do
  327
+      assert topic_1.save
  328
+      assert topic_2.save
  329
+      @first.save
  330
+      @second.destroy
  331
+      assert_equal false, topic_1.new_record?
  332
+      assert_not_nil topic_1.id
  333
+      assert_equal false, topic_2.new_record?
  334
+      assert_not_nil topic_2.id
  335
+      assert_equal false, @first.new_record?
  336
+      assert_not_nil @first.id
  337
+      assert_equal true, @second.destroyed?
  338
+      raise ActiveRecord::Rollback
  339
+    end
  340
+
  341
+    assert_equal true, topic_1.new_record?
  342
+    assert_nil topic_1.id
  343
+    assert_equal true, topic_2.new_record?
  344
+    assert_nil topic_2.id
  345
+    assert_equal false, @first.new_record?
  346
+    assert_not_nil @first.id
  347
+    assert_equal false, @second.destroyed?
  348
+  end
  349
+
323 350
   if current_adapter?(:PostgreSQLAdapter) && defined?(PGconn::PQTRANS_IDLE)
324 351
     def test_outside_transaction_works
325 352
       assert Topic.connection.outside_transaction?
@@ -328,7 +355,7 @@ def test_outside_transaction_works
328 355
       Topic.connection.rollback_db_transaction
329 356
       assert Topic.connection.outside_transaction?
330 357
     end
331  
-    
  358
+
332 359
     def test_rollback_wont_be_executed_if_no_transaction_active
333 360
       assert_raise RuntimeError do
334 361
         Topic.transaction do
@@ -338,7 +365,7 @@ def test_rollback_wont_be_executed_if_no_transaction_active
338 365
         end
339 366
       end
340 367
     end
341  
-    
  368
+
342 369
     def test_open_transactions_count_is_reset_to_zero_if_no_transaction_active
343 370
       Topic.transaction do
344 371
         Topic.transaction do
@@ -358,12 +385,12 @@ def test_sqlite_add_column_in_transaction
358 385
     #
359 386
     # We go back to the connection for the column queries because
360 387
     # Topic.columns is cached and won't report changes to the DB
361  
-    
  388
+
362 389
     assert_nothing_raised do
363 390
       Topic.reset_column_information
364 391
       Topic.connection.add_column('topics', 'stuff', :string)
365 392
       assert Topic.column_names.include?('stuff')
366  
-      
  393
+
367 394
       Topic.reset_column_information
368 395
       Topic.connection.remove_column('topics', 'stuff')
369 396
       assert !Topic.column_names.include?('stuff')
@@ -382,6 +409,12 @@ def test_sqlite_add_column_in_transaction
382 409
   end
383 410
 
384 411
   private
  412
+    def define_callback_method(callback_method)
  413
+      define_method(callback_method) do
  414
+        self.history << [callback_method, :method]
  415
+      end
  416
+    end
  417
+
385 418
     def add_exception_raising_after_save_callback_to_topic
386 419
       Topic.class_eval <<-eoruby, __FILE__, __LINE__ + 1
387 420
         remove_method(:after_save_for_transaction)
@@ -440,7 +473,7 @@ class TransactionsWithTransactionalFixturesTest < ActiveRecord::TestCase
440 473
 
441 474
   def test_automatic_savepoint_in_outer_transaction
442 475
     @first = Topic.find(1)
443  
-    
  476
+
444 477
     begin
445 478
       Topic.transaction do
446 479
         @first.approved = true

2 notes on commit da840d1

Pat Allan
pat commented on da840d1 May 17, 2010

So happy this is in AR proper. Thanks Brian et al!

W. Andrew Loe III
loe commented on da840d1 May 17, 2010

Fantastic!

Please sign in to comment.
Something went wrong with that request. Please try again.