From da840d13da865331297d5287391231b1ed39721b Mon Sep 17 00:00:00 2001 From: Brian Durand Date: Tue, 2 Jun 2009 14:42:22 -0500 Subject: Add after_commit and after_rollback callbacks to ActiveRecord that are called after transactions either commit or rollback on all records saved or destroyed in the transaction. [#2991 state:committed] Signed-off-by: Jeremy Kemper --- activerecord/lib/active_record/transactions.rb | 131 ++++++++++++++++++++++--- 1 file changed, 117 insertions(+), 14 deletions(-) (limited to 'activerecord/lib/active_record/transactions.rb') diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index cf0fe8934d..9e48228a40 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -12,6 +12,9 @@ module ActiveRecord [:destroy, :save, :save!].each do |method| alias_method_chain method, :transactions end + + define_model_callbacks :commit, :commit_on_update, :commit_on_create, :commit_on_destroy, :only => :after + define_model_callbacks :rollback, :rollback_on_update, :rollback_on_create, :rollback_on_destroy end # Transactions are protective blocks where SQL statements are only permanent @@ -108,7 +111,7 @@ module ActiveRecord # rescue ActiveRecord::StatementInvalid # # ...which we ignore. # end - # + # # # On PostgreSQL, the transaction is now unusable. The following # # statement will cause a PostgreSQL error, even though the unique # # constraint is no longer violated: @@ -132,7 +135,7 @@ module ActiveRecord # raise ActiveRecord::Rollback # end # end - # + # # User.find(:all) # => empty # # It is also possible to requires a sub-transaction by passing @@ -147,7 +150,7 @@ module ActiveRecord # raise ActiveRecord::Rollback # end # end - # + # # User.find(:all) # => Returns only Kotori # # Most databases don't support true nested transactions. At the time of @@ -157,6 +160,26 @@ module ActiveRecord # http://dev.mysql.com/doc/refman/5.0/en/savepoints.html # for more information about savepoints. # + # === Callbacks + # + # There are two types of callbacks associated with committing and rolling back transactions: + # after_commit and after_rollback. + # + # The after_commit callbacks are called on every record saved or destroyed within a + # transaction immediately after the transaction is committed. The after_rollback callbacks + # are called on every record saved or destroyed within a transaction immediately after the + # transaction or savepoint is rolled back. + # + # Additionally, there are callbacks for after_commit_on_create, after_rollback_on_create, + # after_commit_on_update, after_rollback_on_update, after_commit_on_destroy, and + # after_rollback_on_destroy which are only called if a record is created, updated or destroyed + # in the transaction. + # + # These callbacks are useful for interacting with other systems since you will be guaranteed + # that the callback is only executed when the database is in a permanent state. For example, + # after_commit is a good spot to put in a hook to clearing a cache since clearing it from + # within a transaction could trigger the cache to be regenerated before the database is updated. + # # === Caveats # # If you're on MySQL, then do not use DDL operations in nested transactions @@ -166,7 +189,7 @@ module ActiveRecord # is finished and tries to release the savepoint it created earlier, a # database error will occur because the savepoint has already been # automatically released. The following example demonstrates the problem: - # + # # Model.connection.transaction do # BEGIN # Model.connection.transaction(:requires_new => true) do # CREATE SAVEPOINT active_record_1 # Model.connection.create_table(...) # active_record_1 now automatically released @@ -197,24 +220,55 @@ module ActiveRecord end def save_with_transactions! #:nodoc: - rollback_active_record_state! { self.class.transaction { save_without_transactions! } } + with_transaction_returning_status(:save_without_transactions!) end # Reset id and @new_record if the transaction rolls back. def rollback_active_record_state! - id_present = has_attribute?(self.class.primary_key) - previous_id = id - previous_new_record = new_record? + remember_transaction_record_state yield rescue Exception - @new_record = previous_new_record - if id_present - self.id = previous_id + restore_transaction_record_state + raise + ensure + clear_transaction_record_state + end + + # Call the after_commit callbacks + def committed! #:nodoc: + if transaction_record_state(:new_record) + _run_commit_on_create_callbacks + elsif transaction_record_state(:destroyed) + _run_commit_on_destroy_callbacks else - @attributes.delete(self.class.primary_key) - @attributes_cache.delete(self.class.primary_key) + _run_commit_on_update_callbacks + end + _run_commit_callbacks + ensure + clear_transaction_record_state + end + + # Call the after rollback callbacks. The restore_state argument indicates if the record + # state should be rolled back to the beginning or just to the last savepoint. + def rolledback!(force_restore_state = false) #:nodoc: + if transaction_record_state(:new_record) + _run_rollback_on_create_callbacks + elsif transaction_record_state(:destroyed) + _run_rollback_on_destroy_callbacks + else + _run_rollback_on_update_callbacks + end + _run_rollback_callbacks + ensure + restore_transaction_record_state(force_restore_state) + end + + # Add the record to the current transaction so that the :after_rollback and :after_commit callbacks + # can be called. + def add_to_transaction + if self.class.connection.add_transaction_record(self) + remember_transaction_record_state end - raise end # Executes +method+ within a transaction and captures its return value as a @@ -226,10 +280,59 @@ module ActiveRecord def with_transaction_returning_status(method, *args) status = nil self.class.transaction do + add_to_transaction status = send(method, *args) raise ActiveRecord::Rollback unless status end status end + + protected + + # Save the new record state and id of a record so it can be restored later if a transaction fails. + def remember_transaction_record_state #:nodoc + @_start_transaction_state ||= {} + unless @_start_transaction_state.include?(:new_record) + @_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key) + @_start_transaction_state[:new_record] = @new_record + end + unless @_start_transaction_state.include?(:destroyed) + @_start_transaction_state[:destroyed] = @new_record + end + @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1 + end + + # Clear the new record state and id of a record. + def clear_transaction_record_state #:nodoc + if defined?(@_start_transaction_state) + @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 + remove_instance_variable(:@_start_transaction_state) if @_start_transaction_state[:level] < 1 + end + end + + # Restore the new record state and id of a record that was previously saved by a call to save_record_state. + def restore_transaction_record_state(force = false) #:nodoc + if defined?(@_start_transaction_state) + @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 + if @_start_transaction_state[:level] < 1 + restore_state = remove_instance_variable(:@_start_transaction_state) + if restore_state + @new_record = restore_state[:new_record] + @destroyed = restore_state[:destroyed] + if restore_state[:id] + self.id = restore_state[:id] + else + @attributes.delete(self.class.primary_key) + @attributes_cache.delete(self.class.primary_key) + end + end + end + end + end + + # Determine if a record was created or destroyed in a transaction. State should be one of :new_record or :destroyed. + def transaction_record_state(state) #:nodoc + @_start_transaction_state[state] if defined?(@_start_transaction_state) + end end end -- cgit v1.2.3