aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib/active_record')
-rwxr-xr-xactiverecord/lib/active_record/base.rb4
-rw-r--r--activerecord/lib/active_record/callbacks.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb56
-rw-r--r--activerecord/lib/active_record/fixtures.rb12
-rw-r--r--activerecord/lib/active_record/transactions.rb137
5 files changed, 191 insertions, 20 deletions
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index aa2826fb33..7cff6d9f1a 100755
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -1219,7 +1219,9 @@ module ActiveRecord #:nodoc:
begin
constant = candidate.constantize
return constant if candidate == constant.to_s
- rescue NameError
+ rescue NameError => e
+ # We don't want to swallow NoMethodError < NameError errors
+ raise e unless e.instance_of?(NameError)
rescue ArgumentError
end
end
diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb
index 498836aca4..44fee12001 100644
--- a/activerecord/lib/active_record/callbacks.rb
+++ b/activerecord/lib/active_record/callbacks.rb
@@ -31,7 +31,7 @@ module ActiveRecord
# class CreditCard < ActiveRecord::Base
# # Strip everything but digits, so the user can specify "555 234 34" or
# # "5552-3434" or both will mean "55523434"
- # def before_validation_on_create
+ # before_validation(:on => :create) do
# self.number = number.gsub(/[^0-9]/, "") if attribute_present?("number")
# end
# end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
index 0c87e052c4..b9fb452eee 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -122,6 +122,8 @@ module ActiveRecord
requires_new = options[:requires_new] || !last_transaction_joinable
transaction_open = false
+ @_current_transaction_records ||= []
+
begin
if block_given?
if requires_new || open_transactions == 0
@@ -132,6 +134,7 @@ module ActiveRecord
end
increment_open_transactions
transaction_open = true
+ @_current_transaction_records.push([])
end
yield
end
@@ -141,8 +144,10 @@ module ActiveRecord
decrement_open_transactions
if open_transactions == 0
rollback_db_transaction
+ rollback_transaction_records(true)
else
rollback_to_savepoint
+ rollback_transaction_records(false)
end
end
raise unless database_transaction_rollback.is_a?(ActiveRecord::Rollback)
@@ -157,20 +162,35 @@ module ActiveRecord
begin
if open_transactions == 0
commit_db_transaction
+ commit_transaction_records
else
release_savepoint
+ save_point_records = @_current_transaction_records.pop
+ unless save_point_records.blank?
+ @_current_transaction_records.push([]) if @_current_transaction_records.empty?
+ @_current_transaction_records.last.concat(save_point_records)
+ end
end
rescue Exception => database_transaction_rollback
if open_transactions == 0
rollback_db_transaction
+ rollback_transaction_records(true)
else
rollback_to_savepoint
+ rollback_transaction_records(false)
end
raise
end
end
end
+ # Register a record with the current transaction so that its after_commit and after_rollback callbacks
+ # can be called.
+ def add_transaction_record(record)
+ last_batch = @_current_transaction_records.last
+ last_batch << record if last_batch
+ end
+
# Begins the transaction (and turns off auto-committing).
def begin_db_transaction() end
@@ -268,6 +288,42 @@ module ActiveRecord
limit.to_i
end
end
+
+ # Send a rollback message to all records after they have been rolled back. If rollback
+ # is false, only rollback records since the last save point.
+ def rollback_transaction_records(rollback) #:nodoc
+ if rollback
+ records = @_current_transaction_records.flatten
+ @_current_transaction_records.clear
+ else
+ records = @_current_transaction_records.pop
+ end
+
+ unless records.blank?
+ records.uniq.each do |record|
+ begin
+ record.rolledback!(rollback)
+ rescue Exception => e
+ record.logger.error(e) if record.respond_to?(:logger)
+ end
+ end
+ end
+ end
+
+ # Send a commit message to all records after they have been committed.
+ def commit_transaction_records #:nodoc
+ records = @_current_transaction_records.flatten
+ @_current_transaction_records.clear
+ unless records.blank?
+ records.uniq.each do |record|
+ begin
+ record.committed!
+ rescue Exception => e
+ record.logger.error(e) if record.respond_to?(:logger)
+ end
+ end
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
index 8099aaa7f7..82270c56b3 100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -787,16 +787,14 @@ class Fixture #:nodoc:
end
def key_list
- columns = @fixture.keys.collect{ |column_name| @connection.quote_column_name(column_name) }
- columns.join(", ")
+ @fixture.keys.map { |column_name| @connection.quote_column_name(column_name) }.join(', ')
end
def value_list
- list = @fixture.inject([]) do |fixtures, (key, value)|
- col = model_class.columns_hash[key] if model_class.respond_to?(:ancestors) && model_class.ancestors.include?(ActiveRecord::Base)
- fixtures << @connection.quote(value, col).gsub('[^\]\\n', "\n").gsub('[^\]\\r', "\r")
- end
- list * ', '
+ cols = (model_class && model_class < ActiveRecord::Base) ? model_class.columns_hash : {}
+ @fixture.map do |key, value|
+ @connection.quote(value, cols[key]).gsub('[^\]\\n', "\n").gsub('[^\]\\r', "\r")
+ end.join(', ')
end
def find
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
index 3f2c1911e7..620758f5af 100644
--- a/activerecord/lib/active_record/transactions.rb
+++ b/activerecord/lib/active_record/transactions.rb
@@ -8,6 +8,10 @@ module ActiveRecord
class TransactionError < ActiveRecordError # :nodoc:
end
+ included do
+ define_callbacks :commit, :rollback, :terminator => "result == false", :scope => [:kind, :name]
+ end
+
# Transactions are protective blocks where SQL statements are only permanent
# if they can all succeed as one atomic action. The classic example is a
# transfer between two accounts where you can only have a deposit if the
@@ -72,7 +76,7 @@ module ActiveRecord
#
# Both +save+ and +destroy+ come wrapped in a transaction that ensures
# that whatever you do in validations or callbacks will happen under its
- # protected cover. So you can use validations to check for values that
+ # protected cover. So you can use validations to check for values that
# the transaction depends on or you can raise exceptions in the callbacks
# to rollback, including <tt>after_*</tt> callbacks.
#
@@ -158,6 +162,21 @@ 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+.
+ #
+ # +after_commit+ callbacks are called on every record saved or destroyed within a
+ # transaction immediately after the transaction is committed. +after_rollback+ callbacks
+ # are called on every record saved or destroyed within a transaction immediately after the
+ # transaction or savepoint is rolled back.
+ #
+ # 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
@@ -182,6 +201,24 @@ module ActiveRecord
# See the ConnectionAdapters::DatabaseStatements#transaction API docs.
connection.transaction(options, &block)
end
+
+ def after_commit(*args, &block)
+ options = args.last
+ if options.is_a?(Hash) && options[:on]
+ options[:if] = Array.wrap(options[:if])
+ options[:if] << "transaction_include_action?(:#{options[:on]})"
+ end
+ set_callback(:commit, :after, *args, &block)
+ end
+
+ def after_rollback(*args, &block)
+ options = args.last
+ if options.is_a?(Hash) && options[:on]
+ options[:if] = Array.wrap(options[:if])
+ options[:if] << "transaction_include_action?(:#{options[:on]})"
+ end
+ set_callback(:rollback, :after, *args, &block)
+ end
end
# See ActiveRecord::Transactions::ClassMethods for detailed documentation.
@@ -205,19 +242,36 @@ module ActiveRecord
# 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
- else
- @attributes.delete(self.class.primary_key)
- @attributes_cache.delete(self.class.primary_key)
- end
+ restore_transaction_record_state
raise
+ ensure
+ clear_transaction_record_state
+ end
+
+ # Call the after_commit callbacks
+ def committed! #:nodoc:
+ _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:
+ _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
end
# Executes +method+ within a transaction and captures its return value as a
@@ -229,10 +283,71 @@ module ActiveRecord
def with_transaction_returning_status
status = nil
self.class.transaction do
+ add_to_transaction
status = yield
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] = @destroyed
+ 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
+
+ # Determine if a transaction included an action for :create, :update, or :destroy. Used in filtering callbacks.
+ def transaction_include_action?(action) #:nodoc
+ case action
+ when :create
+ transaction_record_state(:new_record)
+ when :destroy
+ destroyed?
+ when :update
+ !(transaction_record_state(:new_record) || destroyed?)
+ end
+ end
end
end