aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib')
-rw-r--r--activerecord/lib/active_record/association_preload.rb30
-rw-r--r--activerecord/lib/active_record/associations.rb72
-rw-r--r--activerecord/lib/active_record/associations/association_collection.rb4
-rw-r--r--activerecord/lib/active_record/associations/association_proxy.rb10
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_association.rb11
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb13
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb1
-rw-r--r--activerecord/lib/active_record/associations/has_one_through_association.rb1
-rw-r--r--activerecord/lib/active_record/associations/through_association_scope.rb30
-rw-r--r--activerecord/lib/active_record/base.rb2
-rw-r--r--activerecord/lib/active_record/nested_attributes.rb13
-rw-r--r--activerecord/lib/active_record/reflection.rb15
-rw-r--r--activerecord/lib/active_record/relation.rb2
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder.rb4
14 files changed, 135 insertions, 73 deletions
diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb
index f2094283a2..6d905fe6fe 100644
--- a/activerecord/lib/active_record/association_preload.rb
+++ b/activerecord/lib/active_record/association_preload.rb
@@ -185,6 +185,9 @@ module ActiveRecord
end
def preload_has_and_belongs_to_many_association(records, reflection, preload_options={})
+
+ 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}
@@ -193,14 +196,28 @@ module ActiveRecord
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(
+ right[reflection.association_foreign_key])
+
+ join = left.create_join(right, left.create_on(condition))
+ select = [
+ # FIXME: options[:select] is always nil in the tests. Do we really
+ # need it?
+ options[:select] || left[Arel.star],
+ right[reflection.primary_key_name].as(
+ Arel.sql('the_parent_record_id'))
+ ]
+
associated_records_proxy = reflection.klass.unscoped.
includes(options[:include]).
- joins("INNER JOIN #{connection.quote_table_name options[:join_table]} t0 ON #{reflection.klass.quoted_table_name}.#{reflection.klass.primary_key} = t0.#{reflection.association_foreign_key}").
- select("#{options[:select] || table_name+'.*'}, t0.#{reflection.primary_key_name} as the_parent_record_id").
order(options[:order])
+ associated_records_proxy.joins_values = [join]
+ associated_records_proxy.select_values = select
+
all_associated_records = associated_records(ids) do |some_ids|
- associated_records_proxy.where([conditions, ids]).to_a
+ associated_records_proxy.where([conditions, some_ids]).to_a
end
set_association_collection_records(id_to_record_map, reflection.name, all_associated_records, 'the_parent_record_id')
@@ -373,14 +390,9 @@ module ActiveRecord
end
end
-
- def interpolate_sql_for_preload(sql)
- instance_eval("%@#{sql.gsub('@', '\@')}@", __FILE__, __LINE__)
- end
-
def append_conditions(reflection, preload_options)
sql = ""
- sql << " AND (#{interpolate_sql_for_preload(reflection.sanitized_conditions)})" if reflection.sanitized_conditions
+ sql << " AND (#{reflection.sanitized_conditions})" if reflection.sanitized_conditions
sql << " AND (#{sanitize_sql preload_options[:conditions]})" if preload_options[:conditions]
sql
end
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index cdc8f25119..1056c51a3d 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -20,18 +20,30 @@ module ActiveRecord
end
end
- class HasManyThroughAssociationPolymorphicError < ActiveRecordError #:nodoc:
+ class HasManyThroughAssociationPolymorphicSourceError < ActiveRecordError #:nodoc:
def initialize(owner_class_name, reflection, source_reflection)
super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' on the polymorphic object '#{source_reflection.class_name}##{source_reflection.name}'.")
end
end
+ class HasManyThroughAssociationPolymorphicThroughError < ActiveRecordError #:nodoc:
+ def initialize(owner_class_name, reflection)
+ super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' which goes through the polymorphic association '#{owner_class_name}##{reflection.through_reflection.name}'.")
+ end
+ end
+
class HasManyThroughAssociationPointlessSourceTypeError < ActiveRecordError #:nodoc:
def initialize(owner_class_name, reflection, source_reflection)
super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' with a :source_type option if the '#{reflection.through_reflection.class_name}##{source_reflection.name}' is not polymorphic. Try removing :source_type on your association.")
end
end
+ class HasOneThroughCantAssociateThroughCollection < ActiveRecordError #:nodoc:
+ def initialize(owner_class_name, reflection, through_reflection)
+ super("Cannot have a has_one :through association '#{owner_class_name}##{reflection.name}' where the :through association '#{owner_class_name}##{through_reflection.name}' is a collection. Specify a has_one or belongs_to association in the :through option instead.")
+ end
+ end
+
class HasManyThroughSourceAssociationNotFoundError < ActiveRecordError #:nodoc:
def initialize(reflection)
through_reflection = reflection.through_reflection
@@ -1223,14 +1235,12 @@ module ActiveRecord
if reflection.options[:polymorphic]
association_accessor_methods(reflection, BelongsToPolymorphicAssociation)
- association_foreign_type_setter_method(reflection)
else
association_accessor_methods(reflection, BelongsToAssociation)
association_constructor_method(:build, reflection, BelongsToAssociation)
association_constructor_method(:create, reflection, BelongsToAssociation)
end
- association_foreign_key_setter_method(reflection)
add_counter_cache_callbacks(reflection) if options[:counter_cache]
add_touch_callbacks(reflection, options[:touch]) if options[:touch]
@@ -1450,7 +1460,7 @@ module ActiveRecord
force_reload = params.first unless params.empty?
association = association_instance_get(reflection.name)
- if association.nil? || force_reload
+ if association.nil? || force_reload || association.stale_target?
association = association_proxy_class.new(self, reflection)
retval = force_reload ? reflection.klass.uncached { association.reload } : association.reload
if retval.nil? and association_proxy_class == BelongsToAssociation
@@ -1497,7 +1507,11 @@ module ActiveRecord
association_instance_set(reflection.name, association)
end
- reflection.klass.uncached { association.reload } if force_reload
+ if force_reload
+ reflection.klass.uncached { association.reload }
+ elsif association.stale_target?
+ association.reload
+ end
association
end
@@ -1506,16 +1520,9 @@ module ActiveRecord
if send(reflection.name).loaded? || reflection.options[:finder_sql]
send(reflection.name).map { |r| r.id }
else
- if reflection.through_reflection && reflection.source_reflection.belongs_to?
- through = reflection.through_reflection
- primary_key = reflection.source_reflection.primary_key_name
- send(through.name).select("DISTINCT #{through.quoted_table_name}.#{primary_key}").map! { |r| r.send(primary_key) }
- else
- send(reflection.name).select("#{reflection.quoted_table_name}.#{reflection.klass.primary_key}").except(:includes).map! { |r| r.id }
- end
+ send(reflection.name).select("#{reflection.quoted_table_name}.#{reflection.klass.primary_key}").except(:includes).map! { |r| r.id }
end
end
-
end
def collection_accessor_methods(reflection, association_proxy_class, writer = true)
@@ -1557,45 +1564,6 @@ module ActiveRecord
end
end
- def association_foreign_key_setter_method(reflection)
- setters = reflect_on_all_associations(:belongs_to).map do |belongs_to_reflection|
- if belongs_to_reflection.primary_key_name == reflection.primary_key_name
- "association_instance_set(:#{belongs_to_reflection.name}, nil);"
- end
- end.compact.join
-
- if method_defined?(:"#{reflection.primary_key_name}=")
- undef_method :"#{reflection.primary_key_name}="
- end
-
- class_eval <<-FILE, __FILE__, __LINE__ + 1
- def #{reflection.primary_key_name}=(new_id)
- write_attribute :#{reflection.primary_key_name}, new_id
- if #{reflection.primary_key_name}_changed?
- #{ setters }
- end
- end
- FILE
- end
-
- def association_foreign_type_setter_method(reflection)
- setters = reflect_on_all_associations(:belongs_to).map do |belongs_to_reflection|
- if belongs_to_reflection.options[:foreign_type] == reflection.options[:foreign_type]
- "association_instance_set(:#{belongs_to_reflection.name}, nil);"
- end
- end.compact.join
-
- field = reflection.options[:foreign_type]
- class_eval <<-FILE, __FILE__, __LINE__ + 1
- def #{field}=(new_id)
- write_attribute :#{field}, new_id
- if #{field}_changed?
- #{ setters }
- end
- end
- FILE
- end
-
def add_counter_cache_callbacks(reflection)
cache_column = reflection.counter_cache_column
diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/association_collection.rb
index c513e8ab08..7964f4fa2b 100644
--- a/activerecord/lib/active_record/associations/association_collection.rb
+++ b/activerecord/lib/active_record/associations/association_collection.rb
@@ -462,10 +462,10 @@ module ActiveRecord
callback(:before_add, record)
yield(record) if block_given?
@target ||= [] unless loaded?
- if index = @target.index(record)
+ if @reflection.options[:uniq] && index = @target.index(record)
@target[index] = record
else
- @target << record
+ @target << record
end
callback(:after_add, record)
set_inverse_instance(record, @owner)
diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb
index 252ff7e7ea..f4eceeed8c 100644
--- a/activerecord/lib/active_record/associations/association_proxy.rb
+++ b/activerecord/lib/active_record/associations/association_proxy.rb
@@ -130,6 +130,16 @@ module ActiveRecord
@loaded = true
end
+ # The target is stale if the target no longer points to the record(s) that the
+ # relevant foreign_key(s) refers to. If stale, the association accessor method
+ # on the owner will reload the target. It's up to subclasses to implement this
+ # method if relevant.
+ #
+ # Note that if the target has not been loaded, it is not considered stale.
+ def stale_target?
+ false
+ end
+
# Returns the target of this proxy, same as +proxy_target+.
def target
@target
diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb
index b438620c8f..bbfe18f9fb 100644
--- a/activerecord/lib/active_record/associations/belongs_to_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_association.rb
@@ -42,6 +42,17 @@ module ActiveRecord
@updated
end
+ def stale_target?
+ if @target && @target.persisted?
+ target_id = @target.send(@reflection.association_primary_key).to_s
+ foreign_key = @owner.send(@reflection.primary_key_name).to_s
+
+ target_id != foreign_key
+ else
+ false
+ end
+ end
+
private
def find_target
find_method = if @reflection.options[:primary_key]
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 a0df860623..c580de7fbe 100644
--- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
@@ -23,6 +23,19 @@ module ActiveRecord
@updated
end
+ def stale_target?
+ if @target && @target.persisted?
+ target_id = @target.send(@reflection.association_primary_key).to_s
+ foreign_key = @owner.send(@reflection.primary_key_name).to_s
+ target_type = @target.class.base_class.name
+ foreign_type = @owner.send(@reflection.options[:foreign_type]).to_s
+
+ target_id != foreign_key || target_type != foreign_type
+ else
+ false
+ end
+ end
+
private
# NOTE - for now, we're only supporting inverse setting from belongs_to back onto
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 f0bc6aedf2..5f4667b4d8 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -59,6 +59,7 @@ module ActiveRecord
def find_target
return [] unless target_reflection_has_associated_record?
+ update_stale_state
scoped.all
end
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 e8cf73976b..eb17935d6a 100644
--- a/activerecord/lib/active_record/associations/has_one_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_through_association.rb
@@ -33,6 +33,7 @@ module ActiveRecord
private
def find_target
+ update_stale_state
scoped.first
end
end
diff --git a/activerecord/lib/active_record/associations/through_association_scope.rb b/activerecord/lib/active_record/associations/through_association_scope.rb
index 5dc5b0c048..e57de84f66 100644
--- a/activerecord/lib/active_record/associations/through_association_scope.rb
+++ b/activerecord/lib/active_record/associations/through_association_scope.rb
@@ -10,6 +10,17 @@ module ActiveRecord
end
end
+ def stale_target?
+ if @target && @reflection.through_reflection.macro == :belongs_to && defined?(@through_foreign_key)
+ previous_key = @through_foreign_key.to_s
+ current_key = @owner.send(@reflection.through_reflection.primary_key_name).to_s
+
+ previous_key != current_key
+ else
+ false
+ end
+ end
+
protected
def construct_find_scope
@@ -63,8 +74,8 @@ module ActiveRecord
end
def construct_select(custom_select = nil)
- distinct = "DISTINCT " if @reflection.options[:uniq]
- custom_select || @reflection.options[:select] || "#{distinct}#{@reflection.quoted_table_name}.*"
+ distinct = "DISTINCT #{@reflection.quoted_table_name}.*" if @reflection.options[:uniq]
+ custom_select || @reflection.options[:select] || distinct
end
def construct_joins
@@ -106,7 +117,12 @@ 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).merge(@reflection.source_reflection.primary_key_name => associate.id)
+ join_attributes = construct_owner_attributes(@reflection.through_reflection)
+
+ join_attributes.merge!(
+ @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)
@@ -160,6 +176,14 @@ module ActiveRecord
end
alias_method :sql_conditions, :conditions
+
+ def update_stale_state
+ construct_scope if stale_target?
+
+ if @reflection.through_reflection.macro == :belongs_to
+ @through_foreign_key = @owner.send(@reflection.through_reflection.primary_key_name)
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index 858ccebbfa..6aac25ba9b 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -913,7 +913,7 @@ module ActiveRecord #:nodoc:
end
def type_condition
- sti_column = arel_table[inheritance_column]
+ sti_column = arel_table[inheritance_column.to_sym]
condition = sti_column.eq(sti_name)
descendants.each { |subclass| condition = condition.or(sti_column.eq(subclass.sti_name)) }
diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb
index 050b521b6a..16023defe3 100644
--- a/activerecord/lib/active_record/nested_attributes.rb
+++ b/activerecord/lib/active_record/nested_attributes.rb
@@ -405,7 +405,18 @@ module ActiveRecord
end
elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes['id'].to_s }
- association.send(:add_record_to_target_with_callbacks, existing_record) if !association.loaded? && !call_reject_if(association_name, attributes)
+ unless association.loaded? || call_reject_if(association_name, attributes)
+ # Make sure we are operating on the actual object which is in the association's
+ # proxy_target array (either by finding it, or adding it if not found)
+ target_record = association.proxy_target.detect { |record| record == existing_record }
+
+ if target_record
+ existing_record = target_record
+ else
+ association.send(:add_record_to_target_with_callbacks, existing_record)
+ end
+ end
+
assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
else
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index 70165c600e..0310e7050d 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -209,7 +209,10 @@ module ActiveRecord
end
def association_primary_key
- @association_primary_key ||= options[:primary_key] || klass.primary_key
+ @association_primary_key ||=
+ options[:primary_key] ||
+ !options[:polymorphic] && klass.primary_key ||
+ 'id'
end
def active_record_primary_key
@@ -372,6 +375,10 @@ module ActiveRecord
raise HasManyThroughAssociationNotFoundError.new(active_record.name, self)
end
+ if through_reflection.options[:polymorphic]
+ raise HasManyThroughAssociationPolymorphicThroughError.new(active_record.name, self)
+ end
+
if source_reflection.nil?
raise HasManyThroughSourceAssociationNotFoundError.new(self)
end
@@ -381,13 +388,17 @@ module ActiveRecord
end
if source_reflection.options[:polymorphic] && options[:source_type].nil?
- raise HasManyThroughAssociationPolymorphicError.new(active_record.name, self, source_reflection)
+ raise HasManyThroughAssociationPolymorphicSourceError.new(active_record.name, self, source_reflection)
end
unless [:belongs_to, :has_many, :has_one].include?(source_reflection.macro) && source_reflection.options[:through].nil?
raise HasManyThroughSourceAssociationMacroError.new(self)
end
+ if macro == :has_one && through_reflection.collection?
+ raise HasOneThroughCantAssociateThroughCollection.new(active_record.name, self, through_reflection)
+ end
+
check_validity_of_inverse!
end
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index 7ecba1c43a..d4c5c85594 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -31,7 +31,7 @@ module ActiveRecord
im = arel.compile_insert values
im.into @table
primary_key_name = @klass.primary_key
- primary_key_value = Hash === values ? values[primary_key_name] : nil
+ primary_key_value = primary_key_name && Hash === values ? values[primary_key] : nil
@klass.connection.insert(
im.to_sql,
diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb
index 70d84619a1..b3f1b9e36a 100644
--- a/activerecord/lib/active_record/relation/predicate_builder.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder.rb
@@ -15,7 +15,7 @@ module ActiveRecord
table = Arel::Table.new(table_name, :engine => engine)
end
- attribute = table[column] || Arel::Attribute.new(table, column)
+ attribute = table[column.to_sym]
case value
when Array, ActiveRecord::Associations::AssociationCollection, ActiveRecord::Relation
@@ -26,7 +26,7 @@ module ActiveRecord
when Range, Arel::Relation
attribute.in(value)
when ActiveRecord::Base
- attribute.eq(Arel.sql(value.quoted_id))
+ attribute.eq(value.id)
when Class
# FIXME: I think we need to deprecate this behavior
attribute.eq(value.name)