diff options
Diffstat (limited to 'activerecord/lib')
-rwxr-xr-x | activerecord/lib/active_record/base.rb | 4 | ||||
-rw-r--r-- | activerecord/lib/active_record/callbacks.rb | 2 | ||||
-rw-r--r-- | activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb | 56 | ||||
-rw-r--r-- | activerecord/lib/active_record/fixtures.rb | 12 | ||||
-rw-r--r-- | activerecord/lib/active_record/transactions.rb | 137 |
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 |