aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib/active_record')
-rw-r--r--activerecord/lib/active_record/association_preload.rb108
-rw-r--r--activerecord/lib/active_record/associations.rb2
-rw-r--r--activerecord/lib/active_record/associations/association_collection.rb16
-rw-r--r--activerecord/lib/active_record/associations/association_proxy.rb69
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_association.rb9
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb24
-rw-r--r--activerecord/lib/active_record/associations/class_methods/join_dependency.rb4
-rw-r--r--activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb14
-rw-r--r--activerecord/lib/active_record/associations/has_association.rb42
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb26
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb29
-rw-r--r--activerecord/lib/active_record/associations/has_one_association.rb31
-rw-r--r--activerecord/lib/active_record/associations/has_one_through_association.rb29
-rw-r--r--activerecord/lib/active_record/associations/through_association.rb (renamed from activerecord/lib/active_record/associations/through_association_scope.rb)48
-rw-r--r--activerecord/lib/active_record/attribute_methods/primary_key.rb15
-rw-r--r--activerecord/lib/active_record/base.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/quoting.rb3
-rw-r--r--activerecord/lib/active_record/persistence.rb6
-rw-r--r--activerecord/lib/active_record/relation.rb14
-rw-r--r--activerecord/lib/active_record/transactions.rb4
20 files changed, 241 insertions, 260 deletions
diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb
index 6d905fe6fe..ecf7b6c210 100644
--- a/activerecord/lib/active_record/association_preload.rb
+++ b/activerecord/lib/active_record/association_preload.rb
@@ -127,8 +127,7 @@ module ActiveRecord
association_proxy = parent_record.send(reflection_name)
association_proxy.loaded
association_proxy.target.push(*Array.wrap(associated_record))
-
- association_proxy.__send__(:set_inverse_instance, associated_record, parent_record)
+ association_proxy.send(:set_inverse_instance, associated_record)
end
end
@@ -148,16 +147,19 @@ module ActiveRecord
def set_association_single_records(id_to_record_map, reflection_name, associated_records, key)
seen_keys = {}
associated_records.each do |associated_record|
+ seen_key = associated_record[key].to_s
+
#this is a has_one or belongs_to: there should only be one record.
#Unfortunately we can't (in portable way) ask the database for
#'all records where foo_id in (x,y,z), but please
# only one row per distinct foo_id' so this where we enforce that
- next if seen_keys[associated_record[key].to_s]
- seen_keys[associated_record[key].to_s] = true
- mapped_records = id_to_record_map[associated_record[key].to_s]
+ next if seen_keys.key? seen_key
+
+ seen_keys[seen_key] = true
+ mapped_records = id_to_record_map[seen_key]
mapped_records.each do |mapped_record|
association_proxy = mapped_record.send("set_#{reflection_name}_target", associated_record)
- association_proxy.__send__(:set_inverse_instance, associated_record, mapped_record)
+ association_proxy.send(:set_inverse_instance, associated_record)
end
end
@@ -188,19 +190,17 @@ module ActiveRecord
left = reflection.klass.arel_table
- table_name = reflection.klass.quoted_table_name
id_to_record_map, ids = construct_id_map(records)
records.each {|record| record.send(reflection.name).loaded}
options = reflection.options
- conditions = "t0.#{reflection.primary_key_name} #{in_or_equals_for_ids(ids)}"
- conditions << append_conditions(reflection, preload_options)
-
right = Arel::Table.new(options[:join_table]).alias('t0')
- condition = left[reflection.klass.primary_key].eq(
+
+
+ join_condition = left[reflection.klass.primary_key].eq(
right[reflection.association_foreign_key])
- join = left.create_join(right, left.create_on(condition))
+ join = left.create_join(right, left.create_on(join_condition))
select = [
# FIXME: options[:select] is always nil in the tests. Do we really
# need it?
@@ -216,8 +216,16 @@ module ActiveRecord
associated_records_proxy.joins_values = [join]
associated_records_proxy.select_values = select
+ custom_conditions = append_conditions(reflection, preload_options)
+
all_associated_records = associated_records(ids) do |some_ids|
- associated_records_proxy.where([conditions, some_ids]).to_a
+ method = in_or_equal(some_ids)
+ conditions = right[reflection.primary_key_name].send(*method)
+ conditions = custom_conditions.inject(conditions) do |ast, cond|
+ ast.and cond
+ end
+
+ associated_records_proxy.where(conditions).to_a
end
set_association_collection_records(id_to_record_map, reflection.name, all_associated_records, 'the_parent_record_id')
@@ -324,61 +332,54 @@ module ActiveRecord
if klass = record.send(polymorph_type)
klass_id = record.send(primary_key_name)
if klass_id
- id_map = klasses_and_ids[klass] ||= {}
+ id_map = klasses_and_ids[klass.constantize] ||= {}
(id_map[klass_id.to_s] ||= []) << record
end
end
end
else
- id_map = {}
- records.each do |record|
+ id_map = records.group_by do |record|
key = record.send(primary_key_name)
- (id_map[key.to_s] ||= []) << record if key
+ key && key.to_s
end
- klasses_and_ids[reflection.klass.name] = id_map unless id_map.empty?
+ id_map.delete nil
+ klasses_and_ids[reflection.klass] = id_map unless id_map.empty?
end
- klasses_and_ids.each do |klass_name, _id_map|
- klass = klass_name.constantize
-
- table_name = klass.quoted_table_name
+ klasses_and_ids.each do |klass, _id_map|
+ table = klass.arel_table
primary_key = (reflection.options[:primary_key] || klass.primary_key).to_s
- column_type = klass.columns.detect{|c| c.name == primary_key}.type
-
- ids = _id_map.keys.map do |id|
- if column_type == :integer
- id.to_i
- elsif column_type == :float
- id.to_f
- else
- id
- end
- end
+ method = in_or_equal(_id_map.keys)
+ conditions = table[primary_key].send(*method)
- conditions = "#{table_name}.#{connection.quote_column_name(primary_key)} #{in_or_equals_for_ids(ids)}"
- conditions << append_conditions(reflection, preload_options)
+ custom_conditions = append_conditions(reflection, preload_options)
+ conditions = custom_conditions.inject(conditions) do |ast, cond|
+ ast.and cond
+ end
- associated_records = klass.unscoped.where([conditions, ids]).apply_finder_options(options.slice(:include, :select, :joins, :order)).to_a
+ associated_records = klass.unscoped.where(conditions).apply_finder_options(options.slice(:include, :select, :joins, :order)).to_a
set_association_single_records(_id_map, reflection.name, associated_records, primary_key)
end
end
def find_associated_records(ids, reflection, preload_options)
- options = reflection.options
- table_name = reflection.klass.quoted_table_name
+ options = reflection.options
+ table = reflection.klass.arel_table
+
+ conditions = []
+
+ key = reflection.primary_key_name
if interface = reflection.options[:as]
- conditions = "#{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_id"} #{in_or_equals_for_ids(ids)} and #{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_type"} = '#{self.base_class.sti_name}'"
- else
- foreign_key = reflection.primary_key_name
- conditions = "#{reflection.klass.quoted_table_name}.#{foreign_key} #{in_or_equals_for_ids(ids)}"
+ key = "#{interface}_id"
+ conditions << table["#{interface}_type"].eq(base_class.sti_name)
end
- conditions << append_conditions(reflection, preload_options)
+ conditions += append_conditions(reflection, preload_options)
find_options = {
- :select => preload_options[:select] || options[:select] || Arel::SqlLiteral.new("#{table_name}.*"),
+ :select => preload_options[:select] || options[:select] || table[Arel.star],
:include => preload_options[:include] || options[:include],
:joins => options[:joins],
:group => preload_options[:group] || options[:group],
@@ -386,19 +387,24 @@ module ActiveRecord
}
associated_records(ids) do |some_ids|
- reflection.klass.scoped.apply_finder_options(find_options.merge(:conditions => [conditions, some_ids])).to_a
+ method = in_or_equal(some_ids)
+ where = conditions.inject(table[key].send(*method)) do |ast, cond|
+ ast.and cond
+ end
+
+ reflection.klass.scoped.apply_finder_options(find_options.merge(:conditions => where)).to_a
end
end
def append_conditions(reflection, preload_options)
- sql = ""
- sql << " AND (#{reflection.sanitized_conditions})" if reflection.sanitized_conditions
- sql << " AND (#{sanitize_sql preload_options[:conditions]})" if preload_options[:conditions]
- sql
+ [
+ ("(#{reflection.sanitized_conditions})" if reflection.sanitized_conditions),
+ ("(#{sanitize_sql preload_options[:conditions]})" if preload_options[:conditions]),
+ ].compact.map { |x| Arel.sql x }
end
- def in_or_equals_for_ids(ids)
- ids.size > 1 ? "IN (?)" : "= ?"
+ def in_or_equal(ids)
+ ids.length == 1 ? ['eq', ids.first] : ['in', ids]
end
# Some databases impose a limit on the number of ids in a list (in Oracle its 1000)
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index 1056c51a3d..b49cf8de95 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -120,6 +120,8 @@ module ActiveRecord
# So there is no need to eager load them.
autoload :AssociationCollection, 'active_record/associations/association_collection'
autoload :AssociationProxy, 'active_record/associations/association_proxy'
+ autoload :HasAssociation, 'active_record/associations/has_association'
+ autoload :ThroughAssociation, 'active_record/associations/through_association'
autoload :BelongsToAssociation, 'active_record/associations/belongs_to_association'
autoload :BelongsToPolymorphicAssociation, 'active_record/associations/belongs_to_polymorphic_association'
autoload :HasAndBelongsToManyAssociation, 'active_record/associations/has_and_belongs_to_many_association'
diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb
index 7964f4fa2b..108e316672 100644
--- a/activerecord/lib/active_record/associations/association_collection.rb
+++ b/activerecord/lib/active_record/associations/association_collection.rb
@@ -18,6 +18,8 @@ module ActiveRecord
# If you need to work on all current children, new and existing records,
# +load_target+ and the +loaded+ flag are your friends.
class AssociationCollection < AssociationProxy #:nodoc:
+ include HasAssociation
+
delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :to => :scoped
def select(select = nil)
@@ -111,7 +113,7 @@ module ActiveRecord
else
build_record(attributes) do |record|
block.call(record) if block_given?
- set_belongs_to_association_for(record)
+ set_owner_attributes(record)
end
end
end
@@ -123,7 +125,7 @@ module ActiveRecord
load_target if @owner.new_record?
transaction do
- flatten_deeper(records).each do |record|
+ records.flatten.each do |record|
raise_on_type_mismatch(record)
add_record_to_target_with_callbacks(record) do |r|
result &&= insert_record(record) unless @owner.new_record?
@@ -452,9 +454,7 @@ module ActiveRecord
end
records = @reflection.options[:uniq] ? uniq(records) : records
- records.each do |record|
- set_inverse_instance(record, @owner)
- end
+ records.each { |record| set_inverse_instance(record) }
records
end
@@ -468,7 +468,7 @@ module ActiveRecord
@target << record
end
callback(:after_add, record)
- set_inverse_instance(record, @owner)
+ set_inverse_instance(record)
record
end
@@ -488,7 +488,7 @@ module ActiveRecord
ensure_owner_is_persisted!
transaction do
- with_scope(:create => @scope[:create].merge(scoped.where_values_hash || {})) do
+ with_scope(:create => @scope[:create].merge(scoped.where_values_hash)) do
build_record(attrs, &block)
end
end
@@ -501,7 +501,7 @@ module ActiveRecord
end
def remove_records(*records)
- records = flatten_deeper(records)
+ records = records.flatten
records.each { |record| raise_on_type_mismatch(record) }
transaction do
diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb
index f4eceeed8c..6720d83199 100644
--- a/activerecord/lib/active_record/associations/association_proxy.rb
+++ b/activerecord/lib/active_record/associations/association_proxy.rb
@@ -4,17 +4,17 @@ module ActiveRecord
module Associations
# = Active Record Associations
#
- # This is the root class of all association proxies:
+ # This is the root class of all association proxies ('+ Foo' signifies an included module Foo):
#
# AssociationProxy
# BelongsToAssociation
- # HasOneAssociation
# BelongsToPolymorphicAssociation
- # AssociationCollection
+ # AssociationCollection + HasAssociation
# HasAndBelongsToManyAssociation
# HasManyAssociation
- # HasManyThroughAssociation
- # HasOneThroughAssociation
+ # HasManyThroughAssociation + ThroughAssociation
+ # HasOneAssociation + HasAssociation
+ # HasOneThroughAssociation + ThroughAssociation
#
# Association proxies in Active Record are middlemen between the object that
# holds the association, known as the <tt>@owner</tt>, and the actual associated
@@ -167,11 +167,6 @@ module ActiveRecord
end
protected
- # Does the association have a <tt>:dependent</tt> option?
- def dependent?
- @reflection.options[:dependent]
- end
-
def interpolate_sql(sql, record = nil)
@owner.send(:interpolate_sql, sql, record)
end
@@ -181,20 +176,6 @@ module ActiveRecord
@reflection.klass.send(:sanitize_sql, sql, table_name)
end
- # Assigns the ID of the owner to the corresponding foreign key in +record+.
- # If the association is polymorphic the type of the owner is also set.
- def set_belongs_to_association_for(record)
- if @reflection.options[:as]
- record["#{@reflection.options[:as]}_id"] = @owner.id if @owner.persisted?
- record["#{@reflection.options[:as]}_type"] = @owner.class.base_class.name.to_s
- else
- if @owner.persisted?
- primary_key = @reflection.options[:primary_key] || :id
- record[@reflection.primary_key_name] = @owner.send(primary_key)
- end
- end
- end
-
# Merges into +options+ the ones coming from the reflection.
def merge_options_from_reflection!(options)
options.reverse_merge!(
@@ -232,6 +213,17 @@ module ActiveRecord
{}
end
+ def aliased_table
+ @reflection.klass.arel_table
+ end
+
+ # Set the inverse association, if possible
+ def set_inverse_instance(record)
+ if record && invertible_for?(record)
+ record.send("set_#{inverse_reflection_for(record).name}_target", @owner)
+ end
+ end
+
private
# Forwards any missing method call to the \target.
def method_missing(method, *args)
@@ -290,34 +282,21 @@ module ActiveRecord
end
end
- if RUBY_VERSION < '1.9.2'
- # Array#flatten has problems with recursive arrays before Ruby 1.9.2.
- # Going one level deeper solves the majority of the problems.
- def flatten_deeper(array)
- array.collect { |element| (element.respond_to?(:flatten) && !element.is_a?(Hash)) ? element.flatten : element }.flatten
- end
- else
- def flatten_deeper(array)
- array.flatten
- end
- end
-
# Returns the ID of the owner, quoted if needed.
def owner_quoted_id
@owner.quoted_id
end
- def set_inverse_instance(record, instance)
- return if record.nil? || !we_can_set_the_inverse_on_this?(record)
- inverse_relationship = @reflection.inverse_of
- unless inverse_relationship.nil?
- record.send(:"set_#{inverse_relationship.name}_target", instance)
- end
+ # Can be redefined by subclasses, notably polymorphic belongs_to
+ # The record parameter is necessary to support polymorphic inverses as we must check for
+ # the association in the specific class of the record.
+ def inverse_reflection_for(record)
+ @reflection.inverse_of
end
- # Override in subclasses
- def we_can_set_the_inverse_on_this?(record)
- false
+ # Is this association invertible? Can be redefined by subclasses.
+ def invertible_for?(record)
+ inverse_reflection_for(record)
end
end
end
diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb
index bbfe18f9fb..98c1c13524 100644
--- a/activerecord/lib/active_record/associations/belongs_to_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_association.rb
@@ -32,7 +32,7 @@ module ActiveRecord
@updated = true
end
- set_inverse_instance(record, @owner)
+ set_inverse_instance(record)
loaded
record
@@ -69,7 +69,7 @@ module ActiveRecord
options
) if @owner[@reflection.primary_key_name]
end
- set_inverse_instance(the_target, @owner)
+ set_inverse_instance(the_target)
the_target
end
@@ -83,8 +83,9 @@ module ActiveRecord
# NOTE - for now, we're only supporting inverse setting from belongs_to back onto
# has_one associations.
- def we_can_set_the_inverse_on_this?(record)
- @reflection.has_inverse? && @reflection.inverse_of.macro == :has_one
+ def invertible_for?(record)
+ inverse = inverse_reflection_for(record)
+ inverse && inverse.macro == :has_one
end
def record_id(record)
diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
index c580de7fbe..90eff7399c 100644
--- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
@@ -14,7 +14,7 @@ module ActiveRecord
@updated = true
end
- set_inverse_instance(record, @owner)
+ set_inverse_instance(record)
loaded
record
end
@@ -38,23 +38,13 @@ module ActiveRecord
private
- # NOTE - for now, we're only supporting inverse setting from belongs_to back onto
- # has_one associations.
- def we_can_set_the_inverse_on_this?(record)
- if @reflection.has_inverse?
- inverse_association = @reflection.polymorphic_inverse_of(record.class)
- inverse_association && inverse_association.macro == :has_one
- else
- false
- end
+ def inverse_reflection_for(record)
+ @reflection.polymorphic_inverse_of(record.class)
end
- def set_inverse_instance(record, instance)
- return if record.nil? || !we_can_set_the_inverse_on_this?(record)
- inverse_relationship = @reflection.polymorphic_inverse_of(record.class)
- if inverse_relationship
- record.send(:"set_#{inverse_relationship.name}_target", instance)
- end
+ def invertible_for?(record)
+ inverse = inverse_reflection_for(record)
+ inverse && inverse.macro == :has_one
end
def construct_find_scope
@@ -71,7 +61,7 @@ module ActiveRecord
:include => @reflection.options[:include]
)
end
- set_inverse_instance(target, @owner)
+ set_inverse_instance(target)
target
end
diff --git a/activerecord/lib/active_record/associations/class_methods/join_dependency.rb b/activerecord/lib/active_record/associations/class_methods/join_dependency.rb
index a74d0a4ca8..bb6145656e 100644
--- a/activerecord/lib/active_record/associations/class_methods/join_dependency.rb
+++ b/activerecord/lib/active_record/associations/class_methods/join_dependency.rb
@@ -212,7 +212,7 @@ module ActiveRecord
collection = record.send(join_part.reflection.name)
collection.loaded
collection.target.push(association)
- collection.__send__(:set_inverse_instance, association, record)
+ collection.send(:set_inverse_instance, association)
when :belongs_to
set_target_and_inverse(join_part, association, record)
else
@@ -224,7 +224,7 @@ module ActiveRecord
def set_target_and_inverse(join_part, association, record)
association_proxy = record.send("set_#{join_part.reflection.name}_target", association)
- association_proxy.__send__(:set_inverse_instance, association, record)
+ association_proxy.send(:set_inverse_instance, association)
end
end
end
diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
index e17ac6f2cc..b1d454545f 100644
--- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
@@ -75,10 +75,12 @@ module ActiveRecord
"INNER JOIN #{@owner.connection.quote_table_name @reflection.options[:join_table]} ON #{@reflection.quoted_table_name}.#{@reflection.klass.primary_key} = #{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.association_foreign_key}"
end
- def construct_conditions
- sql = "#{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.primary_key_name} = #{owner_quoted_id} "
- sql << " AND (#{conditions})" if conditions
- sql
+ def join_table
+ Arel::Table.new(@reflection.options[:join_table])
+ end
+
+ def construct_owner_conditions
+ super(join_table)
end
def construct_find_scope
@@ -108,6 +110,10 @@ module ActiveRecord
[]
end
end
+
+ def invertible_for?(record)
+ false
+ end
end
end
end
diff --git a/activerecord/lib/active_record/associations/has_association.rb b/activerecord/lib/active_record/associations/has_association.rb
new file mode 100644
index 0000000000..0ecdb696ea
--- /dev/null
+++ b/activerecord/lib/active_record/associations/has_association.rb
@@ -0,0 +1,42 @@
+module ActiveRecord
+ module Associations
+ # Included in all has_* associations (i.e. everything except belongs_to)
+ module HasAssociation #:nodoc:
+ protected
+ # Sets the owner attributes on the given record
+ def set_owner_attributes(record)
+ if @owner.persisted?
+ construct_owner_attributes.each { |key, value| record[key] = value }
+ end
+ end
+
+ # Returns a hash linking the owner to the association represented by the reflection
+ def construct_owner_attributes(reflection = @reflection)
+ attributes = {}
+ if reflection.macro == :belongs_to
+ attributes[reflection.association_primary_key] = @owner.send(reflection.primary_key_name)
+ else
+ attributes[reflection.primary_key_name] = @owner.send(reflection.active_record_primary_key)
+
+ if reflection.options[:as]
+ attributes["#{reflection.options[:as]}_type"] = @owner.class.base_class.name
+ end
+ end
+ attributes
+ end
+
+ # Builds an array of arel nodes from the owner attributes hash
+ def construct_owner_conditions(table = aliased_table, reflection = @reflection)
+ construct_owner_attributes(reflection).map do |attr, value|
+ table[attr].eq(value)
+ end
+ end
+
+ def construct_conditions
+ conditions = construct_owner_conditions
+ conditions << Arel.sql(sql_conditions) if sql_conditions
+ aliased_table.create_and(conditions)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
index 4b05cf6e81..c3360463cd 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -54,7 +54,7 @@ module ActiveRecord
end
def insert_record(record, force = false, validate = true)
- set_belongs_to_association_for(record)
+ set_owner_attributes(record)
save_record(record, force, validate)
end
@@ -79,22 +79,6 @@ module ActiveRecord
end
end
- def target_obsolete?
- false
- end
-
- def construct_conditions
- if @reflection.options[:as]
- sql =
- "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND " +
- "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}"
- else
- sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}"
- end
- sql << " AND (#{conditions})" if conditions
- sql
- end
-
def construct_find_scope
{
:conditions => construct_conditions,
@@ -106,13 +90,7 @@ module ActiveRecord
end
def construct_create_scope
- create_scoping = {}
- set_belongs_to_association_for(create_scoping)
- create_scoping
- end
-
- def we_can_set_the_inverse_on_this?(record)
- @reflection.inverse_of
+ construct_owner_attributes
end
end
end
diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb
index 5f4667b4d8..e2b008034e 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -1,17 +1,16 @@
-require "active_record/associations/through_association_scope"
require 'active_support/core_ext/object/blank'
module ActiveRecord
# = Active Record Has Many Through Association
module Associations
class HasManyThroughAssociation < HasManyAssociation #:nodoc:
- include ThroughAssociationScope
+ include ThroughAssociation
alias_method :new, :build
def destroy(*records)
transaction do
- delete_records(flatten_deeper(records))
+ delete_records(records.flatten)
super
end
end
@@ -21,9 +20,13 @@ module ActiveRecord
# have a size larger than zero, and you need to fetch that collection afterwards, it'll take one fewer
# SELECT query if you use #length.
def size
- return @owner.send(:read_attribute, cached_counter_attribute_name) if has_cached_counter?
- return @target.size if loaded?
- return count
+ if has_cached_counter?
+ @owner.send(:read_attribute, cached_counter_attribute_name)
+ elsif loaded?
+ @target.size
+ else
+ count
+ end
end
protected
@@ -51,9 +54,9 @@ module ActiveRecord
# TODO - add dependent option support
def delete_records(records)
- klass = @reflection.through_reflection.klass
+ through_association = @owner.send(@reflection.through_reflection.name)
records.each do |associate|
- klass.delete_all(construct_join_attributes(associate))
+ through_association.where(construct_join_attributes(associate)).delete_all
end
end
@@ -63,16 +66,8 @@ module ActiveRecord
scoped.all
end
- def has_cached_counter?
- @owner.attribute_present?(cached_counter_attribute_name)
- end
-
- def cached_counter_attribute_name
- "#{@reflection.name}_count"
- end
-
# NOTE - not sure that we can actually cope with inverses here
- def we_can_set_the_inverse_on_this?(record)
+ def invertible_for?(record)
false
end
end
diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb
index c49fd6e66a..c32aaf986e 100644
--- a/activerecord/lib/active_record/associations/has_one_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_association.rb
@@ -2,6 +2,8 @@ module ActiveRecord
# = Active Record Belongs To Has One Association
module Associations
class HasOneAssociation < AssociationProxy #:nodoc:
+ include HasAssociation
+
def create(attrs = {}, replace_existing = true)
new_record(replace_existing) do |reflection|
attrs = merge_with_conditions(attrs)
@@ -27,7 +29,7 @@ module ActiveRecord
load_target
unless @target.nil? || @target == obj
- if dependent? && !dont_save
+ if @reflection.options[:dependent] && !dont_save
case @reflection.options[:dependent]
when :delete
@target.delete if @target.persisted?
@@ -49,11 +51,11 @@ module ActiveRecord
@target = nil
else
raise_on_type_mismatch(obj)
- set_belongs_to_association_for(obj)
+ set_owner_attributes(obj)
@target = (AssociationProxy === obj ? obj.target : obj)
end
- set_inverse_instance(obj, @owner)
+ set_inverse_instance(obj)
@loaded = true
unless !@owner.persisted? || obj.nil? || dont_save
@@ -79,26 +81,16 @@ module ActiveRecord
the_target = with_scope(:find => @scope[:find]) do
@reflection.klass.find(:first, options)
end
- set_inverse_instance(the_target, @owner)
+ set_inverse_instance(the_target)
the_target
end
def construct_find_scope
- if @reflection.options[:as]
- sql =
- "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_id = #{owner_quoted_id} AND " +
- "#{@reflection.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}"
- else
- sql = "#{@reflection.quoted_table_name}.#{@reflection.primary_key_name} = #{owner_quoted_id}"
- end
- sql << " AND (#{conditions})" if conditions
- { :conditions => sql }
+ { :conditions => construct_conditions }
end
def construct_create_scope
- create_scoping = {}
- set_belongs_to_association_for(create_scoping)
- create_scoping
+ construct_owner_attributes
end
def new_record(replace_existing)
@@ -115,17 +107,12 @@ module ActiveRecord
else
record[@reflection.primary_key_name] = @owner.id if @owner.persisted?
self.target = record
- set_inverse_instance(record, @owner)
+ set_inverse_instance(record)
end
record
end
- def we_can_set_the_inverse_on_this?(record)
- inverse = @reflection.inverse_of
- return !inverse.nil?
- end
-
def merge_with_conditions(attrs={})
attrs ||= {}
attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash)
diff --git a/activerecord/lib/active_record/associations/has_one_through_association.rb b/activerecord/lib/active_record/associations/has_one_through_association.rb
index eb17935d6a..c9ae930e93 100644
--- a/activerecord/lib/active_record/associations/has_one_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_through_association.rb
@@ -1,10 +1,8 @@
-require "active_record/associations/through_association_scope"
-
module ActiveRecord
# = Active Record Has One Through Association
module Associations
class HasOneThroughAssociation < HasOneAssociation
- include ThroughAssociationScope
+ include ThroughAssociation
def replace(new_value)
create_through_record(new_value)
@@ -13,25 +11,26 @@ module ActiveRecord
private
- def create_through_record(new_value) #nodoc:
- klass = @reflection.through_reflection.klass
-
- current_object = @owner.send(@reflection.through_reflection.name)
+ def create_through_record(new_value)
+ proxy = @owner.send(@reflection.through_reflection.name) ||
+ @owner.send(:association_instance_get, @reflection.through_reflection.name)
+ record = proxy.target
- if current_object
- new_value ? current_object.update_attributes(construct_join_attributes(new_value)) : current_object.destroy
+ if record && !new_value
+ record.destroy
elsif new_value
- if @owner.new_record?
- self.target = new_value
- through_association = @owner.send(:association_instance_get, @reflection.through_reflection.name)
- through_association.build(construct_join_attributes(new_value))
+ attributes = construct_join_attributes(new_value)
+
+ if record
+ record.update_attributes(attributes)
+ elsif @owner.new_record?
+ proxy.build(attributes)
else
- @owner.send(@reflection.through_reflection.name, klass.create(construct_join_attributes(new_value)))
+ proxy.create(attributes)
end
end
end
- private
def find_target
update_stale_state
scoped.first
diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association.rb
index e57de84f66..57718285f8 100644
--- a/activerecord/lib/active_record/associations/through_association_scope.rb
+++ b/activerecord/lib/active_record/associations/through_association.rb
@@ -1,7 +1,7 @@
module ActiveRecord
- # = Active Record Through Association Scope
+ # = Active Record Through Association
module Associations
- module ThroughAssociationScope
+ module ThroughAssociation
def scoped
with_scope(@scope) do
@@ -35,8 +35,12 @@ module ActiveRecord
}
end
+ # This scope affects the creation of the associated records (not the join records). At the
+ # moment we only support creating on a :through association when the source reflection is a
+ # belongs_to. Thus it's not necessary to set a foreign key on the associated record(s), so
+ # this scope has can legitimately be empty.
def construct_create_scope
- construct_owner_attributes(@reflection)
+ { }
end
def aliased_through_table
@@ -47,35 +51,13 @@ module ActiveRecord
@reflection.through_reflection.klass.arel_table
end
- # Build SQL conditions from attributes, qualified by table name.
- def construct_conditions
- table = aliased_through_table
- conditions = construct_owner_attributes(@reflection.through_reflection).map do |attr, value|
- table[attr].eq(value)
- end
- conditions << Arel.sql(sql_conditions) if sql_conditions
- table.create_and(conditions)
- end
-
- # Associate attributes pointing to owner
- def construct_owner_attributes(reflection)
- if as = reflection.options[:as]
- { "#{as}_id" => @owner[reflection.active_record_primary_key],
- "#{as}_type" => @owner.class.base_class.name }
- elsif reflection.macro == :belongs_to
- { reflection.klass.primary_key => @owner[reflection.primary_key_name] }
- else
- { reflection.primary_key_name => @owner[reflection.active_record_primary_key] }
- end
+ def construct_owner_conditions
+ super(aliased_through_table, @reflection.through_reflection)
end
- def construct_from
- @reflection.table_name
- end
-
- def construct_select(custom_select = nil)
- distinct = "DISTINCT #{@reflection.quoted_table_name}.*" if @reflection.options[:uniq]
- custom_select || @reflection.options[:select] || distinct
+ def construct_select
+ @reflection.options[:select] ||
+ @reflection.options[:uniq] && "DISTINCT #{@reflection.quoted_table_name}.*"
end
def construct_joins
@@ -117,12 +99,10 @@ module ActiveRecord
# TODO: revisit this to allow it for deletion, supposing dependent option is supported
raise ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(@owner, @reflection) if [:has_one, :has_many].include?(@reflection.source_reflection.macro)
- join_attributes = construct_owner_attributes(@reflection.through_reflection)
-
- join_attributes.merge!(
+ join_attributes = {
@reflection.source_reflection.primary_key_name =>
associate.send(@reflection.source_reflection.association_primary_key)
- )
+ }
if @reflection.options[:source_type]
join_attributes.merge!(@reflection.source_reflection.options[:foreign_type] => associate.class.base_class.name)
diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb
index b891b2c50e..978cd7fbe3 100644
--- a/activerecord/lib/active_record/attribute_methods/primary_key.rb
+++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb
@@ -14,11 +14,13 @@ module ActiveRecord
# Defines the primary key field -- can be overridden in subclasses. Overwriting will negate any effect of the
# primary_key_prefix_type setting, though.
def primary_key
- reset_primary_key
+ @primary_key ||= reset_primary_key
end
def reset_primary_key #:nodoc:
- key = get_primary_key(base_class.name)
+ key = self == base_class ? get_primary_key(base_class.name) :
+ base_class.primary_key
+
set_primary_key(key)
key
end
@@ -40,6 +42,9 @@ module ActiveRecord
end
end
+ attr_accessor :original_primary_key
+ attr_writer :primary_key
+
# Sets the name of the primary key column to use to the given value,
# or (if the value is nil or false) to the value returned by the given
# block.
@@ -48,9 +53,11 @@ module ActiveRecord
# set_primary_key "sysid"
# end
def set_primary_key(value = nil, &block)
- define_attr_method :primary_key, value, &block
+ @primary_key ||= ''
+ self.original_primary_key = @primary_key
+ value &&= value.to_s
+ self.primary_key = block_given? ? instance_eval(&block) : value
end
- alias :primary_key= :set_primary_key
end
end
end
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index 6aac25ba9b..396ab226a9 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -1490,8 +1490,10 @@ MSG
attributes.each do |k, v|
if k.include?("(")
multi_parameter_attributes << [ k, v ]
+ elsif respond_to?("#{k}=")
+ send("#{k}=", v)
else
- respond_to?(:"#{k}=") ? send(:"#{k}=", v) : raise(UnknownAttributeError, "unknown attribute: #{k}")
+ raise(UnknownAttributeError, "unknown attribute: #{k}")
end
end
@@ -1628,7 +1630,7 @@ MSG
# Returns the contents of the record as a nicely formatted string.
def inspect
attributes_as_nice_string = self.class.column_names.collect { |name|
- if has_attribute?(name) || new_record?
+ if has_attribute?(name)
"#{name}: #{attribute_for_inspect(name)}"
end
}.compact.join(", ")
@@ -1816,7 +1818,7 @@ MSG
if scope = self.class.send(:current_scoped_methods)
create_with = scope.scope_for_create
create_with.each { |att,value|
- respond_to?(:"#{att}=") && send("#{att}=", value)
+ respond_to?("#{att}=") && send("#{att}=", value)
}
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
index a7a12faac2..7489e88eef 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
@@ -33,8 +33,9 @@ module ActiveRecord
when BigDecimal then value.to_s('F')
when Numeric then value.to_s
when Date, Time then "'#{quoted_date(value)}'"
+ when Symbol then "'#{quote_string(value.to_s)}'"
else
- "'#{quote_string(value.to_s)}'"
+ "'#{quote_string(value.to_yaml)}'"
end
end
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index 9ac8fcb176..b05957981d 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -258,11 +258,11 @@ module ActiveRecord
# Creates a record with values matching those of the instance attributes
# and returns its id.
def create
- if self.id.nil? && connection.prefetch_primary_key?(self.class.table_name)
+ if id.nil? && connection.prefetch_primary_key?(self.class.table_name)
self.id = connection.next_sequence_value(self.class.sequence_name)
end
- attributes_values = arel_attributes_values
+ attributes_values = arel_attributes_values(!id.nil?)
new_id = if attributes_values.empty?
self.class.unscoped.insert connection.empty_insert_statement_value
@@ -282,7 +282,7 @@ module ActiveRecord
# that instances loaded from the database would.
def attributes_from_column_definition
Hash[self.class.columns.map do |column|
- [column.name, column.default] unless column.name == self.class.primary_key
+ [column.name, column.default]
end]
end
end
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index d4c5c85594..20e983b5f7 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -166,13 +166,19 @@ module ActiveRecord
if conditions || options.present?
where(conditions).apply_finder_options(options.slice(:limit, :order)).update_all(updates)
else
+ limit = nil
+ order = []
# Apply limit and order only if they're both present
if @limit_value.present? == @order_values.present?
- stmt = arel.compile_update(Arel::SqlLiteral.new(@klass.send(:sanitize_sql_for_assignment, updates)))
- @klass.connection.update stmt.to_sql
- else
- except(:limit, :order).update_all(updates)
+ limit = arel.limit
+ order = arel.orders
end
+
+ stmt = arel.compile_update(Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates)))
+ stmt.take limit
+ stmt.order(*order)
+ stmt.key = @klass.arel_table[@klass.primary_key]
+ @klass.connection.update stmt.to_sql
end
end
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
index 443f318067..868f761a33 100644
--- a/activerecord/lib/active_record/transactions.rb
+++ b/activerecord/lib/active_record/transactions.rb
@@ -301,8 +301,8 @@ module ActiveRecord
# 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 ||= {}
+ @_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key)
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)
@@ -329,7 +329,7 @@ module ActiveRecord
@attributes = @attributes.dup if @attributes.frozen?
@new_record = restore_state[:new_record]
@destroyed = restore_state[:destroyed]
- if restore_state[:id]
+ if restore_state.has_key?(:id)
self.id = restore_state[:id]
else
@attributes.delete(self.class.primary_key)