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/associations.rb88
-rwxr-xr-xactiverecord/lib/active_record/base.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb56
-rw-r--r--activerecord/lib/active_record/locking/optimistic.rb35
-rw-r--r--activerecord/lib/active_record/migration.rb10
-rw-r--r--activerecord/lib/active_record/railtie.rb9
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb1
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb73
-rw-r--r--activerecord/lib/active_record/serializers/xml_serializer.rb85
-rw-r--r--activerecord/lib/active_record/transactions.rb126
10 files changed, 362 insertions, 125 deletions
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index d94cc03938..6c64210c92 100755
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -1500,7 +1500,16 @@ module ActiveRecord
when :destroy
method_name = "has_many_dependent_destroy_for_#{reflection.name}".to_sym
define_method(method_name) do
- send(reflection.name).each { |o| o.destroy }
+ send(reflection.name).each do |o|
+ # No point in executing the counter update since we're going to destroy the parent anyway
+ counter_method = ('belongs_to_counter_cache_before_destroy_for_' + self.class.name.downcase).to_sym
+ if(o.respond_to? counter_method) then
+ class << o
+ self
+ end.send(:define_method, counter_method, Proc.new {})
+ end
+ o.destroy
+ end
end
before_destroy method_name
when :delete_all
@@ -1728,6 +1737,14 @@ module ActiveRecord
build(associations)
end
+ def graft(*associations)
+ associations.each do |association|
+ join_associations.detect {|a| association == a} ||
+ build(association.reflection.name, association.find_parent_in(self), association.join_class)
+ end
+ self
+ end
+
def join_associations
@joins[1..-1].to_a
end
@@ -1736,6 +1753,16 @@ module ActiveRecord
@joins[0]
end
+ def count_aliases_from_table_joins(name)
+ quoted_name = join_base.active_record.connection.quote_table_name(name.downcase)
+ join_sql = join_base.table_joins.to_s.downcase
+ join_sql.blank? ? 0 :
+ # Table names
+ join_sql.scan(/join(?:\s+\w+)?\s+#{quoted_name}\son/).size +
+ # Table aliases
+ join_sql.scan(/join(?:\s+\w+)?\s+\S+\s+#{quoted_name}\son/).size
+ end
+
def instantiate(rows)
rows.each_with_index do |row, i|
primary_id = join_base.record_id(row)
@@ -1780,22 +1807,22 @@ module ActiveRecord
end
protected
- def build(associations, parent = nil)
+ def build(associations, parent = nil, join_class = Arel::InnerJoin)
parent ||= @joins.last
case associations
when Symbol, String
reflection = parent.reflections[associations.to_s.intern] or
raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?"
@reflections << reflection
- @joins << build_join_association(reflection, parent)
+ @joins << build_join_association(reflection, parent).with_join_class(join_class)
when Array
associations.each do |association|
- build(association, parent)
+ build(association, parent, join_class)
end
when Hash
associations.keys.sort{|a,b|a.to_s<=>b.to_s}.each do |name|
- build(name, parent)
- build(associations[name])
+ build(name, parent, join_class)
+ build(associations[name], nil, join_class)
end
else
raise ConfigurationError, associations.inspect
@@ -1872,6 +1899,12 @@ module ActiveRecord
@table_joins = joins
end
+ def ==(other)
+ other.is_a?(JoinBase) &&
+ other.active_record == active_record &&
+ other.table_joins == table_joins
+ end
+
def aliased_prefix
"t0"
end
@@ -1937,6 +1970,27 @@ module ActiveRecord
end
end
+ def ==(other)
+ other.is_a?(JoinAssociation) &&
+ other.reflection == reflection &&
+ other.parent == parent
+ end
+
+ def find_parent_in(other_join_dependency)
+ other_join_dependency.joins.detect do |join|
+ self.parent == join
+ end
+ end
+
+ def join_class
+ @join_class ||= Arel::InnerJoin
+ end
+
+ def with_join_class(join_class)
+ @join_class = join_class
+ self
+ end
+
def association_join
return @join if @join
@@ -2036,27 +2090,25 @@ module ActiveRecord
end
def join_relation(joining_relation, join = nil)
- if (relations = relation).is_a?(Array)
- joining_relation.joins(Relation::JoinOperation.new(relations.first, Arel::OuterJoin, association_join.first)).
- joins(Relation::JoinOperation.new(relations.last, Arel::OuterJoin, association_join.last))
- else
- joining_relation.joins(Relation::JoinOperation.new(relations, Arel::OuterJoin, association_join))
- end
+ joining_relation.joins(self.with_join_class(Arel::OuterJoin))
end
protected
def aliased_table_name_for(name, suffix = nil)
- if !parent.table_joins.blank? && parent.table_joins.to_s.downcase =~ %r{join(\s+\w+)?\s+#{active_record.connection.quote_table_name name.downcase}\son}
- @join_dependency.table_aliases[name] += 1
+ if @join_dependency.table_aliases[name].zero?
+ @join_dependency.table_aliases[name] = @join_dependency.count_aliases_from_table_joins(name)
end
- unless @join_dependency.table_aliases[name].zero?
- # if the table name has been used, then use an alias
+ if !@join_dependency.table_aliases[name].zero? # We need an alias
name = active_record.connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}#{suffix}"
- table_index = @join_dependency.table_aliases[name]
@join_dependency.table_aliases[name] += 1
- name = name[0..active_record.connection.table_alias_length-3] + "_#{table_index+1}" if table_index > 0
+ if @join_dependency.table_aliases[name] == 1 # First time we've seen this name
+ # Also need to count the aliases from the table_aliases to avoid incorrect count
+ @join_dependency.table_aliases[name] += @join_dependency.count_aliases_from_table_joins(name)
+ end
+ table_index = @join_dependency.table_aliases[name]
+ name = name[0..active_record.connection.table_alias_length-3] + "_#{table_index}" if table_index > 1
else
@join_dependency.table_aliases[name] += 1
end
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index 2d7cfad80d..9ed53cc4af 100755
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -931,6 +931,10 @@ module ActiveRecord #:nodoc:
subclasses.each { |klass| klass.reset_inheritable_attributes; klass.reset_column_information }
end
+ def attribute_method?(attribute)
+ super || (table_exists? && column_names.include?(attribute.to_s.sub(/=$/, '')))
+ end
+
# Set the lookup ancestors for ActiveModel.
def lookup_ancestors #:nodoc:
klass = self
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/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb
index 9044ca418b..60ad23f38c 100644
--- a/activerecord/lib/active_record/locking/optimistic.rb
+++ b/activerecord/lib/active_record/locking/optimistic.rb
@@ -23,6 +23,16 @@ module ActiveRecord
# p2.first_name = "should fail"
# p2.save # Raises a ActiveRecord::StaleObjectError
#
+ # Optimistic locking will also check for stale data when objects are destroyed. Example:
+ #
+ # p1 = Person.find(1)
+ # p2 = Person.find(1)
+ #
+ # p1.first_name = "Michael"
+ # p1.save
+ #
+ # p2.destroy # Raises a ActiveRecord::StaleObjectError
+ #
# You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging,
# or otherwise apply the business logic needed to resolve the conflict.
#
@@ -39,6 +49,7 @@ module ActiveRecord
self.lock_optimistically = true
alias_method_chain :update, :lock
+ alias_method_chain :destroy, :lock
alias_method_chain :attributes_from_column_definition, :lock
class << self
@@ -88,7 +99,7 @@ module ActiveRecord
unless affected_rows == 1
- raise ActiveRecord::StaleObjectError, "Attempted to update a stale object"
+ raise ActiveRecord::StaleObjectError, "Attempted to update a stale object: #{self.class.name}"
end
affected_rows
@@ -100,6 +111,28 @@ module ActiveRecord
end
end
+ def destroy_with_lock #:nodoc:
+ return destroy_without_lock unless locking_enabled?
+
+ unless new_record?
+ lock_col = self.class.locking_column
+ previous_value = send(lock_col).to_i
+
+ affected_rows = connection.delete(
+ "DELETE FROM #{self.class.quoted_table_name} " +
+ "WHERE #{connection.quote_column_name(self.class.primary_key)} = #{quoted_id} " +
+ "AND #{self.class.quoted_locking_column} = #{quote_value(previous_value)}",
+ "#{self.class.name} Destroy"
+ )
+
+ unless affected_rows == 1
+ raise ActiveRecord::StaleObjectError, "Attempted to delete a stale object: #{self.class.name}"
+ end
+ end
+
+ freeze
+ end
+
module ClassMethods
DEFAULT_LOCKING_COLUMN = 'lock_version'
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index c163fb982a..940f825038 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -384,9 +384,13 @@ module ActiveRecord
class << self
def migrate(migrations_path, target_version = nil)
case
- when target_version.nil? then up(migrations_path, target_version)
- when current_version > target_version then down(migrations_path, target_version)
- else up(migrations_path, target_version)
+ when target_version.nil?
+ up(migrations_path, target_version)
+ when current_version == 0 && target_version == 0
+ when current_version > target_version
+ down(migrations_path, target_version)
+ else
+ up(migrations_path, target_version)
end
end
diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb
index f3d21d4969..898df0a67a 100644
--- a/activerecord/lib/active_record/railtie.rb
+++ b/activerecord/lib/active_record/railtie.rb
@@ -61,13 +61,8 @@ module ActiveRecord
# Setup database middleware after initializers have run
initializer "active_record.initialize_database_middleware", :after => "action_controller.set_configs" do |app|
middleware = app.config.middleware
- if middleware.include?("ActiveRecord::SessionStore")
- middleware.insert_before "ActiveRecord::SessionStore", ActiveRecord::ConnectionAdapters::ConnectionManagement
- middleware.insert_before "ActiveRecord::SessionStore", ActiveRecord::QueryCache
- else
- middleware.use ActiveRecord::ConnectionAdapters::ConnectionManagement
- middleware.use ActiveRecord::QueryCache
- end
+ middleware.insert_after "::ActionDispatch::Callbacks", ActiveRecord::QueryCache
+ middleware.insert_after "::ActionDispatch::Callbacks", ActiveRecord::ConnectionAdapters::ConnectionManagement
end
initializer "active_record.set_dispatch_hooks", :before => :set_clear_dependencies_hook do |app|
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index 3514d0a259..d6144dc206 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -188,7 +188,6 @@ module ActiveRecord
def construct_relation_for_association_calculations
including = (@eager_load_values + @includes_values).uniq
join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, including, arel.joins(arel))
-
relation = except(:includes, :eager_load, :preload)
apply_join_dependency(relation, join_dependency)
end
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index 58af930446..7bca12d85e 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -80,6 +80,26 @@ module ActiveRecord
@arel ||= build_arel
end
+ def custom_join_sql(*joins)
+ arel = table
+ joins.each do |join|
+ next if join.blank?
+
+ @implicit_readonly = true
+
+ case join
+ when Hash, Array, Symbol
+ if array_of_strings?(join)
+ join_string = join.join(' ')
+ arel = arel.join(join_string)
+ end
+ else
+ arel = arel.join(join)
+ end
+ end
+ arel.joins(arel)
+ end
+
def build_arel
arel = table
@@ -88,50 +108,41 @@ module ActiveRecord
joins = @joins_values.map {|j| j.respond_to?(:strip) ? j.strip : j}.uniq
- # Build association joins first
joins.each do |join|
association_joins << join if [Hash, Array, Symbol].include?(join.class) && !array_of_strings?(join)
end
- if association_joins.any?
- join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, association_joins.uniq, nil)
- to_join = []
+ stashed_association_joins = joins.select {|j| j.is_a?(ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation)}
- join_dependency.join_associations.each do |association|
- if (association_relation = association.relation).is_a?(Array)
- to_join << [association_relation.first, association.association_join.first]
- to_join << [association_relation.last, association.association_join.last]
- else
- to_join << [association_relation, association.association_join]
- end
- end
+ non_association_joins = (joins - association_joins - stashed_association_joins).reject {|j| j.blank?}
+ custom_joins = custom_join_sql(*non_association_joins)
- to_join.each do |tj|
- unless joined_associations.detect {|ja| ja[0] == tj[0] && ja[1] == tj[1] }
- joined_associations << tj
- arel = arel.join(tj[0]).on(*tj[1])
- end
- end
- end
+ join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, association_joins, custom_joins)
- joins.each do |join|
- next if join.blank?
+ join_dependency.graft(*stashed_association_joins)
- @implicit_readonly = true
+ @implicit_readonly = true unless association_joins.empty? && stashed_association_joins.empty?
- case join
- when Relation::JoinOperation
- arel = arel.join(join.relation, join.join_class).on(*join.on)
- when Hash, Array, Symbol
- if array_of_strings?(join)
- join_string = join.join(' ')
- arel = arel.join(join_string)
- end
+ to_join = []
+
+ join_dependency.join_associations.each do |association|
+ if (association_relation = association.relation).is_a?(Array)
+ to_join << [association_relation.first, association.join_class, association.association_join.first]
+ to_join << [association_relation.last, association.join_class, association.association_join.last]
else
- arel = arel.join(join)
+ to_join << [association_relation, association.join_class, association.association_join]
end
end
+ to_join.each do |tj|
+ unless joined_associations.detect {|ja| ja[0] == tj[0] && ja[1] == tj[1] && ja[2] == tj[2] }
+ joined_associations << tj
+ arel = arel.join(tj[0], tj[1]).on(*tj[2])
+ end
+ end
+
+ arel = arel.join(custom_joins)
+
@where_values.uniq.each do |where|
next if where.blank?
diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb
index 255b03433d..b2d4a48945 100644
--- a/activerecord/lib/active_record/serializers/xml_serializer.rb
+++ b/activerecord/lib/active_record/serializers/xml_serializer.rb
@@ -182,16 +182,31 @@ module ActiveRecord #:nodoc:
options[:except] |= Array.wrap(@serializable.class.inheritance_column)
end
+ def add_extra_behavior
+ add_includes
+ end
+
+ def add_includes
+ procs = options.delete(:procs)
+ @serializable.send(:serializable_add_includes, options) do |association, records, opts|
+ add_associations(association, records, opts)
+ end
+ options[:procs] = procs
+ end
+
+ # TODO This can likely be cleaned up to simple use ActiveSupport::XmlMini.to_tag as well.
def add_associations(association, records, opts)
+ association_name = association.to_s.singularize
+ merged_options = options.merge(opts).merge!(:root => association_name, :skip_instruct => true)
+
if records.is_a?(Enumerable)
- tag = reformat_name(association.to_s)
- type = options[:skip_types] ? {} : {:type => "array"}
+ tag = ActiveSupport::XmlMini.rename_key(association.to_s, options)
+ type = options[:skip_types] ? { } : {:type => "array"}
if records.empty?
- builder.tag!(tag, type)
+ @builder.tag!(tag, type)
else
- builder.tag!(tag, type) do
- association_name = association.to_s.singularize
+ @builder.tag!(tag, type) do
records.each do |record|
if options[:skip_types]
record_type = {}
@@ -200,60 +215,30 @@ module ActiveRecord #:nodoc:
record_type = {:type => record_class}
end
- record.to_xml opts.merge(:root => association_name).merge(record_type)
+ record.to_xml merged_options.merge(record_type)
end
end
end
- else
- if record = @serializable.send(association)
- record.to_xml(opts.merge(:root => association))
- end
- end
- end
-
- def serialize
- args = [root]
- if options[:namespace]
- args << {:xmlns=>options[:namespace]}
- end
-
- if options[:type]
- args << {:type=>options[:type]}
- end
-
- builder.tag!(*args) do
- add_attributes
- procs = options.delete(:procs)
- @serializable.send(:serializable_add_includes, options) { |association, records, opts|
- add_associations(association, records, opts)
- }
- options[:procs] = procs
- add_procs
- yield builder if block_given?
+ elsif record = @serializable.send(association)
+ record.to_xml(merged_options)
end
end
class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc:
- protected
- def compute_type
- type = @serializable.class.serialized_attributes.has_key?(name) ? :yaml : @serializable.class.columns_hash[name].type
-
- case type
- when :text
- :string
- when :time
- :datetime
- else
- type
- end
- end
- end
+ def compute_type
+ type = @serializable.class.serialized_attributes.has_key?(name) ?
+ super : @serializable.class.columns_hash[name].type
- class MethodAttribute < Attribute #:nodoc:
- protected
- def compute_type
- Hash::XML_TYPE_NAMES[@serializable.send(name).class.name] || :string
+ case type
+ when :text
+ :string
+ when :time
+ :datetime
+ else
+ type
end
+ end
+ protected :compute_type
end
end
end
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
index cf0fe8934d..0a55ef2b53 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,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.
+ #
+ # 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.
+ #
+ # 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 +184,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 +215,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 +275,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