aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/transactions.rb
diff options
context:
space:
mode:
authorBrian Durand <brian@embellishedvisions.com>2009-06-02 14:42:22 -0500
committerJeremy Kemper <jeremy@bitsweat.net>2010-04-29 12:24:15 -0700
commitda840d13da865331297d5287391231b1ed39721b (patch)
tree70ffe972358503c16fadd04a1dda336abba9c216 /activerecord/lib/active_record/transactions.rb
parent20f0e9f3203560f34f451a37130caf237bb94867 (diff)
downloadrails-da840d13da865331297d5287391231b1ed39721b.tar.gz
rails-da840d13da865331297d5287391231b1ed39721b.tar.bz2
rails-da840d13da865331297d5287391231b1ed39721b.zip
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 <jeremy@bitsweat.net>
Diffstat (limited to 'activerecord/lib/active_record/transactions.rb')
-rw-r--r--activerecord/lib/active_record/transactions.rb131
1 files changed, 117 insertions, 14 deletions
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