aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/associations
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib/active_record/associations')
-rw-r--r--activerecord/lib/active_record/associations/alias_tracker.rb14
-rw-r--r--activerecord/lib/active_record/associations/association.rb114
-rw-r--r--activerecord/lib/active_record/associations/association_scope.rb22
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_association.rb89
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb16
-rw-r--r--activerecord/lib/active_record/associations/builder/association.rb4
-rw-r--r--activerecord/lib/active_record/associations/builder/belongs_to.rb59
-rw-r--r--activerecord/lib/active_record/associations/builder/collection_association.rb2
-rw-r--r--activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb55
-rw-r--r--activerecord/lib/active_record/associations/collection_association.rb65
-rw-r--r--activerecord/lib/active_record/associations/collection_proxy.rb67
-rw-r--r--activerecord/lib/active_record/associations/foreign_association.rb7
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb23
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb69
-rw-r--r--activerecord/lib/active_record/associations/has_one_association.rb67
-rw-r--r--activerecord/lib/active_record/associations/has_one_through_association.rb21
-rw-r--r--activerecord/lib/active_record/associations/join_dependency.rb138
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_association.rb51
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_part.rb11
-rw-r--r--activerecord/lib/active_record/associations/preloader.rb61
-rw-r--r--activerecord/lib/active_record/associations/preloader/association.rb12
-rw-r--r--activerecord/lib/active_record/associations/singular_association.rb32
-rw-r--r--activerecord/lib/active_record/associations/through_association.rb33
23 files changed, 495 insertions, 537 deletions
diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb
index 14881cfe17..272eede824 100644
--- a/activerecord/lib/active_record/associations/alias_tracker.rb
+++ b/activerecord/lib/active_record/associations/alias_tracker.rb
@@ -30,20 +30,12 @@ module ActiveRecord
join.left.scan(
/JOIN(?:\s+\w+)?\s+(?:\S+\s+)?(?:#{quoted_name}|#{name})\sON/i
).size
- elsif join.respond_to? :left
+ elsif join.is_a?(Arel::Nodes::Join)
join.left.name == name ? 1 : 0
elsif join.is_a?(Hash)
- join.fetch(name, 0)
+ join[name]
else
- # this branch is reached by two tests:
- #
- # activerecord/test/cases/associations/cascaded_eager_loading_test.rb:37
- # with :posts
- #
- # activerecord/test/cases/associations/eager_test.rb:1133
- # with :comments
- #
- 0
+ raise ArgumentError, "joins list should be initialized by list of Arel::Nodes::Join"
end
end
diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb
index ca1f9f1650..0bb63b97ae 100644
--- a/activerecord/lib/active_record/associations/association.rb
+++ b/activerecord/lib/active_record/associations/association.rb
@@ -17,9 +17,25 @@ module ActiveRecord
# CollectionAssociation
# HasManyAssociation + ForeignAssociation
# HasManyThroughAssociation + ThroughAssociation
+ #
+ # Associations in Active Record are middlemen between the object that
+ # holds the association, known as the <tt>owner</tt>, and the associated
+ # result set, known as the <tt>target</tt>. Association metadata is available in
+ # <tt>reflection</tt>, which is an instance of <tt>ActiveRecord::Reflection::AssociationReflection</tt>.
+ #
+ # For example, given
+ #
+ # class Blog < ActiveRecord::Base
+ # has_many :posts
+ # end
+ #
+ # blog = Blog.first
+ #
+ # The association of <tt>blog.posts</tt> has the object +blog+ as its
+ # <tt>owner</tt>, the collection of its posts as <tt>target</tt>, and
+ # the <tt>reflection</tt> object represents a <tt>:has_many</tt> macro.
class Association #:nodoc:
attr_reader :owner, :target, :reflection
- attr_accessor :inversed
delegate :options, to: :reflection
@@ -41,7 +57,9 @@ module ActiveRecord
end
# Reloads the \target and returns +self+ on success.
- def reload
+ # The QueryCache is cleared if +force+ is true.
+ def reload(force = false)
+ klass.connection.clear_query_cache if force && klass
reset
reset_scope
load_target
@@ -67,7 +85,7 @@ module ActiveRecord
#
# Note that if the target has not been loaded, it is not considered stale.
def stale_target?
- !inversed && loaded? && @stale_state != stale_state
+ !@inversed && loaded? && @stale_state != stale_state
end
# Sets the target of this association to <tt>\target</tt>, and the \loaded flag to +true+.
@@ -80,53 +98,44 @@ module ActiveRecord
target_scope.merge!(association_scope)
end
- # The scope for this association.
- #
- # Note that the association_scope is merged into the target_scope only when the
- # scope method is called. This is because at that point the call may be surrounded
- # by scope.scoping { ... } or with_scope { ... } etc, which affects the scope which
- # actually gets built.
- def association_scope
- if klass
- @association_scope ||= AssociationScope.scope(self)
- end
- end
-
def reset_scope
@association_scope = nil
end
# Set the inverse association, if possible
def set_inverse_instance(record)
- if invertible_for?(record)
- inverse = record.association(inverse_reflection_for(record).name)
- inverse.target = owner
- inverse.inversed = true
+ if inverse = inverse_association_for(record)
+ inverse.inversed_from(owner)
+ end
+ record
+ end
+
+ def set_inverse_instance_from_queries(record)
+ if inverse = inverse_association_for(record)
+ inverse.inversed_from_queries(owner)
end
record
end
# Remove the inverse association, if possible
def remove_inverse_instance(record)
- if invertible_for?(record)
- inverse = record.association(inverse_reflection_for(record).name)
- inverse.target = nil
- inverse.inversed = false
+ if inverse = inverse_association_for(record)
+ inverse.inversed_from(nil)
end
end
+ def inversed_from(record)
+ self.target = record
+ @inversed = !!record
+ end
+ alias :inversed_from_queries :inversed_from
+
# Returns the class of the target. belongs_to polymorphic overrides this to look at the
# polymorphic_type field on the owner.
def klass
reflection.klass
end
- # Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the
- # through association's scope)
- def target_scope
- AssociationRelation.create(klass, klass.arel_table, klass.predicate_builder, self).merge!(klass.all)
- end
-
def extensions
extensions = klass.default_extensions | reflection.extensions
@@ -156,9 +165,9 @@ module ActiveRecord
reset
end
- # We can't dump @reflection since it contains the scope proc
+ # We can't dump @reflection and @through_reflection since it contains the scope proc
def marshal_dump
- ivars = (instance_variables - [:@reflection]).map { |name| [name, instance_variable_get(name)] }
+ ivars = (instance_variables - [:@reflection, :@through_reflection]).map { |name| [name, instance_variable_get(name)] }
[@reflection.name, ivars]
end
@@ -187,6 +196,38 @@ module ActiveRecord
end
private
+ def find_target
+ scope = self.scope
+ return scope.to_a if skip_statement_cache?(scope)
+
+ conn = klass.connection
+ sc = reflection.association_scope_cache(conn, owner) do |params|
+ as = AssociationScope.create { params.bind }
+ target_scope.merge!(as.scope(self))
+ end
+
+ binds = AssociationScope.get_bind_values(owner, reflection.chain)
+ sc.execute(binds, conn) { |record| set_inverse_instance(record) } || []
+ end
+
+ # The scope for this association.
+ #
+ # Note that the association_scope is merged into the target_scope only when the
+ # scope method is called. This is because at that point the call may be surrounded
+ # by scope.scoping { ... } or unscoped { ... } etc, which affects the scope which
+ # actually gets built.
+ def association_scope
+ if klass
+ @association_scope ||= AssociationScope.scope(self)
+ end
+ end
+
+ # Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the
+ # through association's scope)
+ def target_scope
+ AssociationRelation.create(klass, self).merge!(klass.all)
+ end
+
def scope_for_create
scope.scope_for_create
end
@@ -201,8 +242,8 @@ module ActiveRecord
if (reflection.has_one? || reflection.collection?) && !options[:through]
attributes[reflection.foreign_key] = owner[reflection.active_record_primary_key]
- if reflection.options[:as]
- attributes[reflection.type] = owner.class.base_class.name
+ if reflection.type
+ attributes[reflection.type] = owner.class.polymorphic_name
end
end
@@ -240,6 +281,12 @@ module ActiveRecord
end
end
+ def inverse_association_for(record)
+ if invertible_for?(record)
+ record.association(inverse_reflection_for(record).name)
+ end
+ 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.
@@ -269,6 +316,7 @@ module ActiveRecord
def build_record(attributes)
reflection.build_association(attributes) do |record|
initialize_attributes(record, attributes)
+ yield(record) if block_given?
end
end
diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb
index 11967e0571..9e38380611 100644
--- a/activerecord/lib/active_record/associations/association_scope.rb
+++ b/activerecord/lib/active_record/associations/association_scope.rb
@@ -26,7 +26,9 @@ module ActiveRecord
chain = get_chain(reflection, association, scope.alias_tracker)
scope.extending! reflection.extensions
- add_constraints(scope, owner, chain)
+ scope = add_constraints(scope, owner, chain)
+ scope.limit!(1) unless reflection.collection?
+ scope
end
def self.get_bind_values(owner, chain)
@@ -35,24 +37,20 @@ module ActiveRecord
binds << last_reflection.join_id_for(owner)
if last_reflection.type
- binds << owner.class.base_class.name
+ binds << owner.class.polymorphic_name
end
chain.each_cons(2).each do |reflection, next_reflection|
if reflection.type
- binds << next_reflection.klass.base_class.name
+ binds << next_reflection.klass.polymorphic_name
end
end
binds
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
+ private
attr_reader :value_transformation
- private
def join(table, constraint)
table.create_join(table, table.create_on(constraint))
end
@@ -67,7 +65,7 @@ module ActiveRecord
scope = apply_scope(scope, table, key, value)
if reflection.type
- polymorphic_type = transform_value(owner.class.base_class.name)
+ polymorphic_type = transform_value(owner.class.polymorphic_name)
scope = apply_scope(scope, table, reflection.type, polymorphic_type)
end
@@ -88,7 +86,7 @@ module ActiveRecord
constraint = table[key].eq(foreign_table[foreign_key])
if reflection.type
- value = transform_value(next_reflection.klass.base_class.name)
+ value = transform_value(next_reflection.klass.polymorphic_name)
scope = apply_scope(scope, table, reflection.type, value)
end
@@ -135,7 +133,7 @@ module ActiveRecord
item = eval_scope(reflection, scope_chain_item, owner)
if scope_chain_item == chain_head.scope
- scope.merge! item.except(:where, :includes)
+ scope.merge! item.except(:where, :includes, :unscope, :order)
end
reflection.all_includes do
@@ -144,7 +142,7 @@ module ActiveRecord
scope.unscope!(*item.unscope_values)
scope.where_clause += item.where_clause
- scope.order_values |= item.order_values
+ scope.order_values = item.order_values | scope.order_values
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 68c608df13..3346725f2d 100644
--- a/activerecord/lib/active_record/associations/belongs_to_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_association.rb
@@ -5,22 +5,20 @@ module ActiveRecord
# = Active Record Belongs To Association
class BelongsToAssociation < SingularAssociation #:nodoc:
def handle_dependency
- target.send(options[:dependent]) if load_target
- end
+ return unless load_target
- def replace(record)
- if record
- raise_on_type_mismatch!(record)
- update_counters_on_replace(record)
- replace_keys(record)
- set_inverse_instance(record)
- @updated = true
+ case options[:dependent]
+ when :destroy
+ target.destroy
+ raise ActiveRecord::Rollback unless target.destroyed?
else
- decrement_counters
- remove_keys
+ target.send(options[:dependent])
end
+ end
- self.target = record
+ def inversed_from(record)
+ replace_keys(record)
+ super
end
def default(&block)
@@ -36,26 +34,60 @@ module ActiveRecord
@updated
end
- def decrement_counters # :nodoc:
+ def decrement_counters
update_counters(-1)
end
- def increment_counters # :nodoc:
+ def increment_counters
update_counters(1)
end
+ def decrement_counters_before_last_save
+ if reflection.polymorphic?
+ model_was = owner.attribute_before_last_save(reflection.foreign_type).try(:constantize)
+ else
+ model_was = klass
+ end
+
+ foreign_key_was = owner.attribute_before_last_save(reflection.foreign_key)
+
+ if foreign_key_was && model_was < ActiveRecord::Base
+ update_counters_via_scope(model_was, foreign_key_was, -1)
+ end
+ end
+
+ def target_changed?
+ owner.saved_change_to_attribute?(reflection.foreign_key)
+ end
+
private
+ def replace(record)
+ if record
+ raise_on_type_mismatch!(record)
+ set_inverse_instance(record)
+ @updated = true
+ end
+
+ replace_keys(record)
+
+ self.target = record
+ end
def update_counters(by)
if require_counter_update? && foreign_key_present?
if target && !stale_target?
target.increment!(reflection.counter_cache_column, by, touch: reflection.options[:touch])
else
- klass.update_counters(target_id, reflection.counter_cache_column => by, touch: reflection.options[:touch])
+ update_counters_via_scope(klass, owner._read_attribute(reflection.foreign_key), by)
end
end
end
+ def update_counters_via_scope(klass, foreign_key, by)
+ scope = klass.unscoped.where!(primary_key(klass) => foreign_key)
+ scope.update_counters(reflection.counter_cache_column => by, touch: reflection.options[:touch])
+ end
+
def find_target?
!loaded? && foreign_key_present? && klass
end
@@ -64,25 +96,12 @@ module ActiveRecord
reflection.counter_cache_column && owner.persisted?
end
- def update_counters_on_replace(record)
- if require_counter_update? && different_target?(record)
- owner.instance_variable_set :@_after_replace_counter_called, true
- record.increment!(reflection.counter_cache_column)
- decrement_counters
- end
- end
-
- # Checks whether record is different to the current target, without loading it
- def different_target?(record)
- record.id != owner._read_attribute(reflection.foreign_key)
- end
-
def replace_keys(record)
- owner[reflection.foreign_key] = record._read_attribute(reflection.association_primary_key(record.class))
+ owner[reflection.foreign_key] = record ? record._read_attribute(primary_key(record.class)) : nil
end
- def remove_keys
- owner[reflection.foreign_key] = nil
+ def primary_key(klass)
+ reflection.association_primary_key(klass)
end
def foreign_key_present?
@@ -96,14 +115,6 @@ module ActiveRecord
inverse && inverse.has_one?
end
- def target_id
- if options[:primary_key]
- owner.send(reflection.name).try(:id)
- else
- owner._read_attribute(reflection.foreign_key)
- end
- end
-
def stale_state
result = owner._read_attribute(reflection.foreign_key) { |n| owner.send(:missing_attribute, n, caller) }
result && result.to_s
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 4ce3474bd5..9ae452e7a1 100644
--- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
@@ -9,20 +9,14 @@ module ActiveRecord
type.presence && type.constantize
end
- private
+ def target_changed?
+ super || owner.saved_change_to_attribute?(reflection.foreign_type)
+ end
+ private
def replace_keys(record)
super
- owner[reflection.foreign_type] = record.class.base_class.name
- end
-
- def remove_keys
- super
- owner[reflection.foreign_type] = nil
- end
-
- def different_target?(record)
- super || record.class != klass
+ owner[reflection.foreign_type] = record ? record.class.polymorphic_name : nil
end
def inverse_reflection_for(record)
diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb
index ca3032d967..7c69cd65ee 100644
--- a/activerecord/lib/active_record/associations/builder/association.rb
+++ b/activerecord/lib/active_record/associations/builder/association.rb
@@ -104,8 +104,8 @@ module ActiveRecord::Associations::Builder # :nodoc:
def self.define_readers(mixin, name)
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
- def #{name}(*args)
- association(:#{name}).reader(*args)
+ def #{name}
+ association(:#{name}).reader
end
CODE
end
diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb
index c161454c1a..fc00f1e900 100644
--- a/activerecord/lib/active_record/associations/builder/belongs_to.rb
+++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb
@@ -21,50 +21,16 @@ module ActiveRecord::Associations::Builder # :nodoc:
add_default_callbacks(model, reflection) if reflection.options[:default]
end
- def self.define_accessors(mixin, reflection)
- super
- add_counter_cache_methods mixin
- end
-
- def self.add_counter_cache_methods(mixin)
- return if mixin.method_defined? :belongs_to_counter_cache_after_update
-
- mixin.class_eval do
- def belongs_to_counter_cache_after_update(reflection)
- foreign_key = reflection.foreign_key
- cache_column = reflection.counter_cache_column
-
- if (@_after_replace_counter_called ||= false)
- @_after_replace_counter_called = false
- elsif saved_change_to_attribute?(foreign_key) && !new_record?
- if reflection.polymorphic?
- model = attribute_in_database(reflection.foreign_type).try(:constantize)
- model_was = attribute_before_last_save(reflection.foreign_type).try(:constantize)
- else
- model = reflection.klass
- model_was = reflection.klass
- end
-
- foreign_key_was = attribute_before_last_save foreign_key
- foreign_key = attribute_in_database foreign_key
-
- if foreign_key && model.respond_to?(:increment_counter)
- model.increment_counter(cache_column, foreign_key)
- end
-
- if foreign_key_was && model_was.respond_to?(:decrement_counter)
- model_was.decrement_counter(cache_column, foreign_key_was)
- end
- end
- end
- end
- end
-
def self.add_counter_cache_callbacks(model, reflection)
cache_column = reflection.counter_cache_column
model.after_update lambda { |record|
- record.belongs_to_counter_cache_after_update(reflection)
+ association = association(reflection.name)
+
+ if association.target_changed?
+ association.increment_counters
+ association.decrement_counters_before_last_save
+ end
}
klass = reflection.class_name.safe_constantize
@@ -84,7 +50,8 @@ module ActiveRecord::Associations::Builder # :nodoc:
else
klass = association.klass
end
- old_record = klass.find_by(klass.primary_key => old_foreign_id)
+ primary_key = reflection.association_primary_key(klass)
+ old_record = klass.find_by(primary_key => old_foreign_id)
if old_record
if touch != true
@@ -114,12 +81,18 @@ module ActiveRecord::Associations::Builder # :nodoc:
BelongsTo.touch_record(record, record.send(changes_method), foreign_key, n, touch, belongs_to_touch_method)
}}
- unless reflection.counter_cache_column
+ if reflection.counter_cache_column
+ touch_callback = callback.(:saved_changes)
+ update_callback = lambda { |record|
+ instance_exec(record, &touch_callback) unless association(reflection.name).target_changed?
+ }
+ model.after_update update_callback, if: :saved_changes?
+ else
model.after_create callback.(:saved_changes), if: :saved_changes?
+ model.after_update callback.(:saved_changes), if: :saved_changes?
model.after_destroy callback.(:changes_to_save)
end
- model.after_update callback.(:saved_changes), if: :saved_changes?
model.after_touch callback.(:changes_to_save)
end
diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb
index 35a72c3850..ff57c40121 100644
--- a/activerecord/lib/active_record/associations/builder/collection_association.rb
+++ b/activerecord/lib/active_record/associations/builder/collection_association.rb
@@ -24,7 +24,7 @@ module ActiveRecord::Associations::Builder # :nodoc:
if block_given?
extension_module_name = "#{model.name.demodulize}#{name.to_s.camelize}AssociationExtension"
extension = Module.new(&Proc.new)
- model.parent.const_set(extension_module_name, extension)
+ model.module_parent.const_set(extension_module_name, extension)
end
end
diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
index 1981da11a2..0140aa15c8 100644
--- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
+++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
@@ -2,39 +2,6 @@
module ActiveRecord::Associations::Builder # :nodoc:
class HasAndBelongsToMany # :nodoc:
- class JoinTableResolver # :nodoc:
- KnownTable = Struct.new :join_table
-
- class KnownClass # :nodoc:
- def initialize(lhs_class, rhs_class_name)
- @lhs_class = lhs_class
- @rhs_class_name = rhs_class_name
- @join_table = nil
- end
-
- def join_table
- @join_table ||= [@lhs_class.table_name, klass.table_name].sort.join("\0").gsub(/^(.*[._])(.+)\0\1(.+)/, '\1\2_\3').tr("\0", "_")
- end
-
- private
-
- def klass
- @lhs_class.send(:compute_type, @rhs_class_name)
- end
- end
-
- def self.build(lhs_class, name, options)
- if options[:join_table]
- KnownTable.new options[:join_table].to_s
- else
- class_name = options.fetch(:class_name) {
- name.to_s.camelize.singularize
- }
- KnownClass.new lhs_class, class_name.to_s
- end
- end
- end
-
attr_reader :lhs_model, :association_name, :options
def initialize(association_name, lhs_model, options)
@@ -44,8 +11,6 @@ module ActiveRecord::Associations::Builder # :nodoc:
end
def through_model
- habtm = JoinTableResolver.build lhs_model, association_name, options
-
join_model = Class.new(ActiveRecord::Base) {
class << self
attr_accessor :left_model
@@ -56,7 +21,9 @@ module ActiveRecord::Associations::Builder # :nodoc:
end
def self.table_name
- table_name_resolver.join_table
+ # Table name needs to be resolved lazily
+ # because RHS class might not have been loaded
+ @table_name ||= table_name_resolver.call
end
def self.compute_type(class_name)
@@ -86,7 +53,7 @@ module ActiveRecord::Associations::Builder # :nodoc:
}
join_model.name = "HABTM_#{association_name.to_s.camelize}"
- join_model.table_name_resolver = habtm
+ join_model.table_name_resolver = -> { table_name }
join_model.left_model = lhs_model
join_model.add_left_association :left_side, anonymous_class: lhs_model
@@ -96,7 +63,7 @@ module ActiveRecord::Associations::Builder # :nodoc:
def middle_reflection(join_model)
middle_name = [lhs_model.name.downcase.pluralize,
- association_name].join("_".freeze).gsub("::".freeze, "_".freeze).to_sym
+ association_name].join("_").gsub("::", "_").to_sym
middle_options = middle_options join_model
HasMany.create_reflection(lhs_model,
@@ -117,6 +84,18 @@ module ActiveRecord::Associations::Builder # :nodoc:
middle_options
end
+ def table_name
+ if options[:join_table]
+ options[:join_table].to_s
+ else
+ class_name = options.fetch(:class_name) {
+ association_name.to_s.camelize.singularize
+ }
+ klass = lhs_model.send(:compute_type, class_name.to_s)
+ [lhs_model.table_name, klass.table_name].sort.join("\0").gsub(/^(.*[._])(.+)\0\1(.+)/, '\1\2_\3').tr("\0", "_")
+ end
+ end
+
def belongs_to_options(options)
rhs_options = {}
diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb
index 921237a735..c3d4eab562 100644
--- a/activerecord/lib/active_record/associations/collection_association.rb
+++ b/activerecord/lib/active_record/associations/collection_association.rb
@@ -45,6 +45,8 @@ module ActiveRecord
def ids_reader
if loaded?
target.pluck(reflection.association_primary_key)
+ elsif !target.empty?
+ load_target.pluck(reflection.association_primary_key)
else
@association_ids ||= scope.pluck(reflection.association_primary_key)
end
@@ -52,11 +54,11 @@ module ActiveRecord
# Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items
def ids_writer(ids)
- pk_type = reflection.association_primary_key_type
+ primary_key = reflection.association_primary_key
+ pk_type = klass.type_for_attribute(primary_key)
ids = Array(ids).reject(&:blank?)
ids.map! { |i| pk_type.cast(i) }
- primary_key = reflection.association_primary_key
records = klass.where(primary_key => ids).index_by do |r|
r.public_send(primary_key)
end.values_at(*ids).compact
@@ -103,15 +105,12 @@ module ActiveRecord
if attributes.is_a?(Array)
attributes.collect { |attr| build(attr, &block) }
else
- add_to_target(build_record(attributes)) do |record|
- yield(record) if block_given?
- end
+ add_to_target(build_record(attributes, &block))
end
end
- # Add +records+ to this association. Returns +self+ so method calls may
- # be chained. Since << flattens its argument list and inserts each record,
- # +push+ and +concat+ behave identically.
+ # Add +records+ to this association. Since +<<+ flattens its argument list
+ # and inserts each record, +push+ and +concat+ behave identically.
def concat(*records)
records = records.flatten
if owner.new_record?
@@ -212,9 +211,11 @@ module ActiveRecord
def size
if !find_target? || loaded?
target.size
+ elsif @association_ids
+ @association_ids.size
elsif !association_scope.group_values.empty?
load_target.size
- elsif !association_scope.distinct_value && target.is_a?(Array)
+ elsif !association_scope.distinct_value && !target.empty?
unsaved_records = target.select(&:new_record?)
unsaved_records.size + count_records
else
@@ -231,10 +232,10 @@ module ActiveRecord
# loaded and you are going to fetch the records anyway it is better to
# check <tt>collection.length.zero?</tt>.
def empty?
- if loaded?
+ if loaded? || @association_ids || reflection.has_cached_counter?
size.zero?
else
- @target.blank? && !scope.exists?
+ target.empty? && !scope.exists?
end
end
@@ -301,23 +302,6 @@ module ActiveRecord
end
private
-
- def find_target
- scope = self.scope
- return scope.to_a if skip_statement_cache?(scope)
-
- conn = klass.connection
- sc = reflection.association_scope_cache(conn, owner) do |params|
- as = AssociationScope.create { params.bind }
- target_scope.merge!(as.scope(self))
- end
-
- binds = AssociationScope.get_bind_values(owner, reflection.chain)
- sc.execute(binds, conn) do |record|
- set_inverse_instance(record)
- end
- end
-
# We have some records loaded from the database (persisted) and some that are
# in-memory (memory). The same record may be represented in the persisted array
# and in the memory array.
@@ -356,15 +340,17 @@ module ActiveRecord
if attributes.is_a?(Array)
attributes.collect { |attr| _create_record(attr, raise, &block) }
else
+ record = build_record(attributes, &block)
transaction do
- add_to_target(build_record(attributes)) do |record|
- yield(record) if block_given?
- insert_record(record, true, raise) {
+ result = nil
+ add_to_target(record) do
+ result = insert_record(record, true, raise) {
@_was_loaded = loaded?
- @association_ids = nil
}
end
+ raise ActiveRecord::Rollback unless result
end
+ record
end
end
@@ -395,7 +381,8 @@ module ActiveRecord
records.each { |record| callback(:before_remove, record) }
delete_records(existing_records, method) if existing_records.any?
- records.each { |record| target.delete(record) }
+ @target -= records
+ @association_ids = nil
records.each { |record| callback(:after_remove, record) }
end
@@ -408,9 +395,9 @@ module ActiveRecord
end
def replace_records(new_target, original_target)
- delete(target - new_target)
+ delete(difference(target, new_target))
- unless concat(new_target - target)
+ unless concat(difference(new_target, target))
@target = original_target
raise RecordNotSaved, "Failed to replace #{reflection.name} because one or more of the " \
"new records could not be saved."
@@ -420,7 +407,7 @@ module ActiveRecord
end
def replace_common_records_in_memory(new_target, original_target)
- common_records = new_target & original_target
+ common_records = intersection(new_target, original_target)
common_records.each do |record|
skip_callbacks = true
replace_on_target(record, @target.index(record), skip_callbacks)
@@ -436,13 +423,14 @@ module ActiveRecord
unless owner.new_record?
result &&= insert_record(record, true, raise) {
@_was_loaded = loaded?
- @association_ids = nil
}
end
end
end
- result && records
+ raise ActiveRecord::Rollback unless result
+
+ records
end
def replace_on_target(record, index, skip_callbacks)
@@ -457,6 +445,7 @@ module ActiveRecord
if index
target[index] = record
elsif @_was_loaded || !loaded?
+ @association_ids = nil
target << record
end
diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb
index 8b4a48a38c..edcb44f0fc 100644
--- a/activerecord/lib/active_record/associations/collection_proxy.rb
+++ b/activerecord/lib/active_record/associations/collection_proxy.rb
@@ -2,11 +2,8 @@
module ActiveRecord
module Associations
- # Association proxies in Active Record are middlemen between the object that
- # holds the association, known as the <tt>@owner</tt>, and the actual associated
- # object, known as the <tt>@target</tt>. The kind of association any proxy is
- # about is available in <tt>@reflection</tt>. That's an instance of the class
- # ActiveRecord::Reflection::AssociationReflection.
+ # Collection proxies in Active Record are middlemen between an
+ # <tt>association</tt>, and its <tt>target</tt> result set.
#
# For example, given
#
@@ -16,14 +13,14 @@ module ActiveRecord
#
# blog = Blog.first
#
- # the association proxy in <tt>blog.posts</tt> has the object in +blog+ as
- # <tt>@owner</tt>, the collection of its posts as <tt>@target</tt>, and
- # the <tt>@reflection</tt> object represents a <tt>:has_many</tt> macro.
+ # The collection proxy returned by <tt>blog.posts</tt> is built from a
+ # <tt>:has_many</tt> <tt>association</tt>, and delegates to a collection
+ # of posts as the <tt>target</tt>.
#
- # This class delegates unknown methods to <tt>@target</tt> via
- # <tt>method_missing</tt>.
+ # This class delegates unknown methods to the <tt>association</tt>'s
+ # relation class via a delegate cache.
#
- # The <tt>@target</tt> object is not \loaded until needed. For example,
+ # The <tt>target</tt> result set is not loaded until needed. For example,
#
# blog.posts.count
#
@@ -32,7 +29,7 @@ module ActiveRecord
class CollectionProxy < Relation
def initialize(klass, association) #:nodoc:
@association = association
- super klass, klass.arel_table, klass.predicate_builder
+ super klass
extensions = association.extensions
extend(*extensions) if extensions.any?
@@ -366,34 +363,6 @@ module ActiveRecord
@association.create!(attributes, &block)
end
- # Add one or more records to the collection by setting their foreign keys
- # to the association's primary key. Since #<< flattens its argument list and
- # inserts each record, +push+ and #concat behave identically. Returns +self+
- # so method calls may be chained.
- #
- # class Person < ActiveRecord::Base
- # has_many :pets
- # end
- #
- # person.pets.size # => 0
- # person.pets.concat(Pet.new(name: 'Fancy-Fancy'))
- # person.pets.concat(Pet.new(name: 'Spook'), Pet.new(name: 'Choo-Choo'))
- # person.pets.size # => 3
- #
- # person.id # => 1
- # person.pets
- # # => [
- # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
- # # #<Pet id: 2, name: "Spook", person_id: 1>,
- # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
- # # ]
- #
- # person.pets.concat([Pet.new(name: 'Brain'), Pet.new(name: 'Benny')])
- # person.pets.size # => 5
- def concat(*records)
- @association.concat(*records)
- end
-
# Replaces this collection with +other_array+. This will perform a diff
# and delete/add only records that have changed.
#
@@ -500,7 +469,7 @@ module ActiveRecord
# Pet.find(1, 2, 3)
# # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 2, 3)
def delete_all(dependent = nil)
- @association.delete_all(dependent)
+ @association.delete_all(dependent).tap { reset_scope }
end
# Deletes the records of the collection directly from the database
@@ -527,7 +496,7 @@ module ActiveRecord
#
# Pet.find(1) # => Couldn't find Pet with id=1
def destroy_all
- @association.destroy_all
+ @association.destroy_all.tap { reset_scope }
end
# Deletes the +records+ supplied from the collection according to the strategy
@@ -646,7 +615,7 @@ module ActiveRecord
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
# # ]
def delete(*records)
- @association.delete(*records)
+ @association.delete(*records).tap { reset_scope }
end
# Destroys the +records+ supplied and removes them from the collection.
@@ -718,7 +687,7 @@ module ActiveRecord
#
# Pet.find(4, 5, 6) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (4, 5, 6)
def destroy(*records)
- @association.destroy(*records)
+ @association.destroy(*records).tap { reset_scope }
end
##
@@ -1033,8 +1002,9 @@ module ActiveRecord
end
# Adds one or more +records+ to the collection by setting their foreign keys
- # to the association's primary key. Returns +self+, so several appends may be
- # chained together.
+ # to the association's primary key. Since +<<+ flattens its argument list and
+ # inserts each record, +push+ and +concat+ behave identically. Returns +self+
+ # so several appends may be chained together.
#
# class Person < ActiveRecord::Base
# has_many :pets
@@ -1057,6 +1027,7 @@ module ActiveRecord
end
alias_method :push, :<<
alias_method :append, :<<
+ alias_method :concat, :<<
def prepend(*args)
raise NoMethodError, "prepend on association is not defined. Please use <<, push or append"
@@ -1088,7 +1059,7 @@ module ActiveRecord
# person.pets.reload # fetches pets from the database
# # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
def reload
- proxy_association.reload
+ proxy_association.reload(true)
reset_scope
end
@@ -1125,7 +1096,7 @@ module ActiveRecord
SpawnMethods,
].flat_map { |klass|
klass.public_instance_methods(false)
- } - self.public_instance_methods(false) - [:select] + [:scoping]
+ } - self.public_instance_methods(false) - [:select] + [:scoping, :values]
delegate(*delegate_methods, to: :scope)
diff --git a/activerecord/lib/active_record/associations/foreign_association.rb b/activerecord/lib/active_record/associations/foreign_association.rb
index 40010cde03..59af6f54c3 100644
--- a/activerecord/lib/active_record/associations/foreign_association.rb
+++ b/activerecord/lib/active_record/associations/foreign_association.rb
@@ -9,5 +9,12 @@ module ActiveRecord::Associations
false
end
end
+
+ def nullified_owner_attributes
+ Hash.new.tap do |attrs|
+ attrs[reflection.foreign_key] = nil
+ attrs[reflection.type] = nil if reflection.type.present?
+ 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 07c7f28d2d..5972846940 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -36,14 +36,6 @@ module ActiveRecord
super
end
- def empty?
- if reflection.has_cached_counter?
- size.zero?
- else
- super
- end
- end
-
private
# Returns the number of records in this collection.
@@ -63,13 +55,13 @@ module ActiveRecord
count = if reflection.has_cached_counter?
owner._read_attribute(reflection.counter_cache_column).to_i
else
- scope.count
+ scope.count(:all)
end
# If there's nothing in the database and @target has no new records
# we are certain the current target is an empty array. This is a
# documented side-effect of the method that may avoid an extra SELECT.
- (@target ||= []) && loaded! if count == 0
+ loaded! if count == 0
[association_scope.limit_value, count].compact.min
end
@@ -92,13 +84,14 @@ module ActiveRecord
if method == :delete_all
scope.delete_all
else
- scope.update_all(reflection.foreign_key => nil)
+ scope.update_all(nullified_owner_attributes)
end
end
def delete_or_nullify_all_records(method)
count = delete_count(method, scope)
update_counter(-count)
+ count
end
# Deletes the records according to the <tt>:dependent</tt> option.
@@ -130,6 +123,14 @@ module ActiveRecord
end
saved_successfully
end
+
+ def difference(a, b)
+ a - b
+ end
+
+ def intersection(a, b)
+ a & b
+ end
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 adbf52b87c..84a9797aa5 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -8,9 +8,7 @@ module ActiveRecord
def initialize(owner, reflection)
super
-
- @through_records = {}
- @through_association = nil
+ @through_records = {}
end
def concat(*records)
@@ -23,20 +21,6 @@ module ActiveRecord
super
end
- def concat_records(records)
- ensure_not_nested
-
- records = super(records, true)
-
- if owner.new_record? && records
- records.flatten.each do |record|
- build_through_record(record)
- end
- end
-
- records
- end
-
def insert_record(record, validate = true, raise = false)
ensure_not_nested
@@ -50,9 +34,18 @@ module ActiveRecord
end
private
+ def concat_records(records)
+ ensure_not_nested
+
+ records = super(records, true)
+
+ if owner.new_record? && records
+ records.flatten.each do |record|
+ build_through_record(record)
+ end
+ end
- def through_association
- @through_association ||= owner.association(through_reflection.name)
+ records
end
# The through record (built with build_record) is temporarily cached
@@ -97,7 +90,7 @@ module ActiveRecord
def build_record(attributes)
ensure_not_nested
- record = super(attributes)
+ record = super
inverse = source_reflection.inverse_of
if inverse
@@ -140,21 +133,15 @@ module ActiveRecord
scope = through_association.scope
scope.where! construct_join_attributes(*records)
+ scope = scope.where(through_scope_attributes)
case method
when :destroy
if scope.klass.primary_key
- count = scope.destroy_all.length
+ count = scope.destroy_all.count(&:destroyed?)
else
scope.each(&:_run_destroy_callbacks)
-
- arel = scope.arel
-
- stmt = Arel::DeleteManager.new
- stmt.from scope.klass.arel_table
- stmt.wheres = arel.constraints
-
- count = scope.klass.connection.delete(stmt, "SQL")
+ count = scope.delete_all
end
when :nullify
count = scope.update_all(source_reflection.foreign_key => nil)
@@ -174,6 +161,30 @@ module ActiveRecord
else
update_counter(-count)
end
+
+ count
+ end
+
+ def difference(a, b)
+ distribution = distribution(b)
+
+ a.reject { |record| mark_occurrence(distribution, record) }
+ end
+
+ def intersection(a, b)
+ distribution = distribution(b)
+
+ a.select { |record| mark_occurrence(distribution, record) }
+ end
+
+ def mark_occurrence(distribution, record)
+ distribution[record] > 0 && distribution[record] -= 1
+ end
+
+ def distribution(array)
+ array.each_with_object(Hash.new(0)) do |record, distribution|
+ distribution[record] += 1
+ end
end
def through_records_for(record)
diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb
index 7953b89f61..99971286a3 100644
--- a/activerecord/lib/active_record/associations/has_one_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_association.rb
@@ -23,35 +23,6 @@ module ActiveRecord
end
end
- def replace(record, save = true)
- raise_on_type_mismatch!(record) if record
- load_target
-
- return target unless target || record
-
- assigning_another_record = target != record
- if assigning_another_record || record.has_changes_to_save?
- save &&= owner.persisted?
-
- transaction_if(save) do
- remove_target!(options[:dependent]) if target && !target.destroyed? && assigning_another_record
-
- if record
- set_owner_attributes(record)
- set_inverse_instance(record)
-
- if save && !record.save
- nullify_owner_attributes(record)
- set_owner_attributes(target) if target
- raise RecordNotSaved, "Failed to save the new associated #{reflection.name}."
- end
- end
- end
- end
-
- self.target = record
- end
-
def delete(method = options[:dependent])
if load_target
case method
@@ -60,13 +31,41 @@ module ActiveRecord
when :destroy
target.destroyed_by_association = reflection
target.destroy
+ throw(:abort) unless target.destroyed?
when :nullify
- target.update_columns(reflection.foreign_key => nil) if target.persisted?
+ target.update_columns(nullified_owner_attributes) if target.persisted?
end
end
end
private
+ def replace(record, save = true)
+ raise_on_type_mismatch!(record) if record
+
+ return target unless load_target || record
+
+ assigning_another_record = target != record
+ if assigning_another_record || record.has_changes_to_save?
+ save &&= owner.persisted?
+
+ transaction_if(save) do
+ remove_target!(options[:dependent]) if target && !target.destroyed? && assigning_another_record
+
+ if record
+ set_owner_attributes(record)
+ set_inverse_instance(record)
+
+ if save && !record.save
+ nullify_owner_attributes(record)
+ set_owner_attributes(target) if target
+ raise RecordNotSaved, "Failed to save the new associated #{reflection.name}."
+ end
+ end
+ end
+ end
+
+ self.target = record
+ end
# The reason that the save param for replace is false, if for create (not just build),
# is because the setting of the foreign keys is actually handled by the scoping when
@@ -106,6 +105,14 @@ module ActiveRecord
yield
end
end
+
+ def _create_record(attributes, raise_error = false, &block)
+ unless owner.persisted?
+ raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved"
+ end
+
+ super
+ end
end
end
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 36746f9115..10978b2d93 100644
--- a/activerecord/lib/active_record/associations/has_one_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_through_association.rb
@@ -6,17 +6,16 @@ module ActiveRecord
class HasOneThroughAssociation < HasOneAssociation #:nodoc:
include ThroughAssociation
- def replace(record)
- create_through_record(record)
- self.target = record
- end
-
private
+ def replace(record, save = true)
+ create_through_record(record, save)
+ self.target = record
+ end
- def create_through_record(record)
+ def create_through_record(record, save)
ensure_not_nested
- through_proxy = owner.association(through_reflection.name)
+ through_proxy = through_association
through_record = through_proxy.load_target
if through_record && !record
@@ -29,8 +28,12 @@ module ActiveRecord
end
if through_record
- through_record.update(attributes)
- elsif owner.new_record?
+ if through_record.new_record?
+ through_record.assign_attributes(attributes)
+ else
+ through_record.update(attributes)
+ end
+ elsif owner.new_record? || !save
through_proxy.build(attributes)
else
through_proxy.create(attributes)
diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb
index df4bf07999..b76005b587 100644
--- a/activerecord/lib/active_record/associations/join_dependency.rb
+++ b/activerecord/lib/active_record/associations/join_dependency.rb
@@ -14,10 +14,8 @@ module ActiveRecord
i[column.name] = column.alias
}
}
- @name_and_alias_cache = tables.each_with_object({}) { |table, h|
- h[table.node] = table.columns.map { |column|
- [column.name, column.alias]
- }
+ @columns_cache = tables.each_with_object({}) { |table, h|
+ h[table.node] = table.columns
}
end
@@ -25,9 +23,8 @@ module ActiveRecord
@tables.flat_map(&:column_aliases)
end
- # An array of [column_name, alias] pairs for the table
def column_aliases(node)
- @name_and_alias_cache[node]
+ @columns_cache[node]
end
def column_alias(node, column)
@@ -67,64 +64,31 @@ module ActiveRecord
end
end
- # base is the base class on which operation is taking place.
- # associations is the list of associations which are joined using hash, symbol or array.
- # joins is the list of all string join commands and arel nodes.
- #
- # Example :
- #
- # class Physician < ActiveRecord::Base
- # has_many :appointments
- # has_many :patients, through: :appointments
- # end
- #
- # If I execute `@physician.patients.to_a` then
- # base # => Physician
- # associations # => []
- # joins # => [#<Arel::Nodes::InnerJoin: ...]
- #
- # However if I execute `Physician.joins(:appointments).to_a` then
- # base # => Physician
- # associations # => [:appointments]
- # joins # => []
- #
- def initialize(base, table, associations, alias_tracker, eager_loading: true)
- @alias_tracker = alias_tracker
- @eager_loading = eager_loading
+ def initialize(base, table, associations)
tree = self.class.make_tree associations
@join_root = JoinBase.new(base, table, build(tree, base))
- @join_root.children.each { |child| construct_tables! @join_root, child }
end
def reflections
join_root.drop(1).map!(&:reflection)
end
- def join_constraints(joins_to_add, join_type)
- joins = join_root.children.flat_map { |child|
- make_join_constraints(join_root, child, join_type)
- }
+ def join_constraints(joins_to_add, join_type, alias_tracker)
+ @alias_tracker = alias_tracker
+
+ construct_tables!(join_root)
+ joins = make_join_constraints(join_root, join_type)
joins.concat joins_to_add.flat_map { |oj|
+ construct_tables!(oj.join_root)
if join_root.match? oj.join_root
walk join_root, oj.join_root
else
- oj.join_root.children.flat_map { |child|
- make_join_constraints(oj.join_root, child, join_type)
- }
+ make_join_constraints(oj.join_root, join_type)
end
}
end
- def aliases
- @aliases ||= Aliases.new join_root.each_with_index.map { |join_part, i|
- columns = join_part.column_names.each_with_index.map { |column_name, j|
- Aliases::Column.new column_name, "t#{i}_r#{j}"
- }
- Aliases::Table.new(join_part, columns)
- }
- end
-
def instantiate(result_set, &block)
primary_key = aliases.column_alias(join_root, join_root.primary_key)
@@ -149,35 +113,49 @@ module ActiveRecord
result_set.each { |row_hash|
parent_key = primary_key ? row_hash[primary_key] : row_hash
parent = parents[parent_key] ||= join_root.instantiate(row_hash, column_aliases, &block)
- construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases)
+ construct(parent, join_root, row_hash, seen, model_cache)
}
end
parents.values
end
+ def apply_column_aliases(relation)
+ relation._select!(-> { aliases.columns })
+ end
+
protected
- attr_reader :alias_tracker, :base_klass, :join_root
+ attr_reader :join_root
private
+ attr_reader :alias_tracker
- def make_constraints(parent, child, tables, join_type)
- chain = child.reflection.chain
- foreign_table = parent.table
- foreign_klass = parent.base_klass
- child.join_constraints(foreign_table, foreign_klass, join_type, tables, chain)
+ def aliases
+ @aliases ||= Aliases.new join_root.each_with_index.map { |join_part, i|
+ columns = join_part.column_names.each_with_index.map { |column_name, j|
+ Aliases::Column.new column_name, "t#{i}_r#{j}"
+ }
+ Aliases::Table.new(join_part, columns)
+ }
end
- def make_outer_joins(parent, child)
- join_type = Arel::Nodes::OuterJoin
- make_join_constraints(parent, child, join_type, true)
+ def construct_tables!(join_root)
+ join_root.each_children do |parent, child|
+ child.tables = table_aliases_for(parent, child)
+ end
end
- def make_join_constraints(parent, child, join_type, aliasing = false)
- tables = aliasing ? table_aliases_for(parent, child) : child.tables
- joins = make_constraints(parent, child, tables, join_type)
+ def make_join_constraints(join_root, join_type)
+ join_root.children.flat_map do |child|
+ make_constraints(join_root, child, join_type)
+ end
+ end
- joins.concat child.children.flat_map { |c| make_join_constraints(child, c, join_type, aliasing) }
+ def make_constraints(parent, child, join_type = Arel::Nodes::OuterJoin)
+ foreign_table = parent.table
+ foreign_klass = parent.base_klass
+ joins = child.join_constraints(foreign_table, foreign_klass, join_type, alias_tracker)
+ joins.concat child.children.flat_map { |c| make_constraints(child, c, join_type) }
end
def table_aliases_for(parent, node)
@@ -190,13 +168,8 @@ module ActiveRecord
}
end
- def construct_tables!(parent, node)
- node.tables = table_aliases_for(parent, node)
- node.children.each { |child| construct_tables! node, child }
- end
-
def table_alias_for(reflection, parent, join)
- name = "#{reflection.plural_name}_#{parent.table_name}"
+ name = reflection.alias_candidate(parent.table_name)
join ? "#{name}_join" : name
end
@@ -205,8 +178,8 @@ module ActiveRecord
[left.children.find { |node2| node1.match? node2 }, node1]
}.partition(&:first)
- ojs = missing.flat_map { |_, n| make_outer_joins left, n }
- intersection.flat_map { |l, r| walk l, r }.concat ojs
+ joins = intersection.flat_map { |l, r| r.table = l.table; walk(l, r) }
+ joins.concat missing.flat_map { |_, n| make_constraints(left, n) }
end
def find_reflection(klass, name)
@@ -221,15 +194,14 @@ module ActiveRecord
reflection.check_eager_loadable!
if reflection.polymorphic?
- next unless @eager_loading
raise EagerLoadPolymorphicError.new(reflection)
end
- JoinAssociation.new(reflection, build(right, reflection.klass), alias_tracker)
- end.compact
+ JoinAssociation.new(reflection, build(right, reflection.klass))
+ end
end
- def construct(ar_parent, parent, row, rs, seen, model_cache, aliases)
+ def construct(ar_parent, parent, row, seen, model_cache)
return if ar_parent.nil?
parent.children.each do |node|
@@ -238,7 +210,7 @@ module ActiveRecord
other.loaded!
elsif ar_parent.association_cached?(node.reflection.name)
model = ar_parent.association(node.reflection.name).target
- construct(model, node, row, rs, seen, model_cache, aliases)
+ construct(model, node, row, seen, model_cache)
next
end
@@ -250,25 +222,20 @@ module ActiveRecord
next
end
- model = seen[ar_parent.object_id][node.base_klass][id]
+ model = seen[ar_parent.object_id][node][id]
if model
- construct(model, node, row, rs, seen, model_cache, aliases)
+ construct(model, node, row, seen, model_cache)
else
- model = construct_model(ar_parent, node, row, model_cache, id, aliases)
-
- if node.reflection.scope &&
- node.reflection.scope_for(node.base_klass.unscoped).readonly_value
- model.readonly!
- end
+ model = construct_model(ar_parent, node, row, model_cache, id)
- seen[ar_parent.object_id][node.base_klass][id] = model
- construct(model, node, row, rs, seen, model_cache, aliases)
+ seen[ar_parent.object_id][node][id] = model
+ construct(model, node, row, seen, model_cache)
end
end
end
- def construct_model(record, node, row, model_cache, id, aliases)
+ def construct_model(record, node, row, model_cache, id)
other = record.association(node.reflection.name)
model = model_cache[node][id] ||=
@@ -282,6 +249,7 @@ module ActiveRecord
other.target = model
end
+ model.readonly! if node.readonly?
model
end
end
diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
index 221c791bf8..ca0305abbb 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
@@ -1,22 +1,20 @@
# frozen_string_literal: true
require "active_record/associations/join_dependency/join_part"
+require "active_support/core_ext/array/extract"
module ActiveRecord
module Associations
class JoinDependency # :nodoc:
class JoinAssociation < JoinPart # :nodoc:
- # The reflection of the association represented
- attr_reader :reflection
+ attr_reader :reflection, :tables
+ attr_accessor :table
- attr_accessor :tables
-
- def initialize(reflection, children, alias_tracker)
+ def initialize(reflection, children)
super(reflection.klass, children)
- @alias_tracker = alias_tracker
- @reflection = reflection
- @tables = nil
+ @reflection = reflection
+ @tables = nil
end
def match?(other)
@@ -24,27 +22,30 @@ module ActiveRecord
super && reflection == other.reflection
end
- def join_constraints(foreign_table, foreign_klass, join_type, tables, chain)
- joins = []
- tables = tables.reverse
+ def join_constraints(foreign_table, foreign_klass, join_type, alias_tracker)
+ joins = []
# The chain starts with the target table, but we want to end with it here (makes
# more sense in this context), so we reverse
- chain.reverse_each do |reflection|
- table = tables.shift
+ reflection.chain.reverse_each.with_index(1) do |reflection, i|
+ table = tables[-i]
klass = reflection.klass
- constraint = reflection.build_join_constraint(table, foreign_table)
-
- joins << table.create_join(table, table.create_on(constraint), join_type)
+ join_scope = reflection.join_scope(table, foreign_table, foreign_klass)
- join_scope = reflection.join_scope(table, foreign_klass)
arel = join_scope.arel(alias_tracker.aliases)
+ nodes = arel.constraints.first
+
+ others = nodes.children.extract! do |node|
+ Arel.fetch_attribute(node) { |attr| attr.relation.name != table.name }
+ end
- if arel.constraints.any?
+ joins << table.create_join(table, table.create_on(nodes), join_type)
+
+ unless others.empty?
joins.concat arel.join_sources
right = joins.last.right
- right.expr = right.expr.and(arel.constraints)
+ right.expr.children.concat(others)
end
# The current table in this iteration becomes the foreign table in the next
@@ -54,12 +55,16 @@ module ActiveRecord
joins
end
- def table
- tables.first
+ def tables=(tables)
+ @tables = tables
+ @table = tables.first
end
- protected
- attr_reader :alias_tracker
+ def readonly?
+ return @readonly if defined?(@readonly)
+
+ @readonly = reflection.scope && reflection.scope_for(base_klass.unscoped).readonly_value
+ end
end
end
end
diff --git a/activerecord/lib/active_record/associations/join_dependency/join_part.rb b/activerecord/lib/active_record/associations/join_dependency/join_part.rb
index 2181f308bf..3ad72a3646 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_part.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_part.rb
@@ -33,6 +33,13 @@ module ActiveRecord
children.each { |child| child.each(&block) }
end
+ def each_children(&block)
+ children.each do |child|
+ yield self, child
+ child.each_children(&block)
+ end
+ end
+
# An Arel::Table for the active_record
def table
raise NotImplementedError
@@ -47,8 +54,8 @@ module ActiveRecord
length = column_names_with_alias.length
while index < length
- column_name, alias_name = column_names_with_alias[index]
- hash[column_name] = row[alias_name]
+ column = column_names_with_alias[index]
+ hash[column.name] = row[column.alias]
index += 1
end
diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb
index e1087be9b3..8997579527 100644
--- a/activerecord/lib/active_record/associations/preloader.rb
+++ b/activerecord/lib/active_record/associations/preloader.rb
@@ -83,12 +83,11 @@ module ActiveRecord
# { author: :avatar }
# [ :books, { author: :avatar } ]
def preload(records, associations, preload_scope = nil)
- records = records.compact
+ records = Array.wrap(records).compact
if records.empty?
[]
else
- records.uniq!
Array.wrap(associations).flat_map { |association|
preloaders_on association, records, preload_scope
}
@@ -98,34 +97,34 @@ module ActiveRecord
private
# Loads all the given data into +records+ for the +association+.
- def preloaders_on(association, records, scope)
+ def preloaders_on(association, records, scope, polymorphic_parent = false)
case association
when Hash
- preloaders_for_hash(association, records, scope)
- when Symbol
- preloaders_for_one(association, records, scope)
- when String
- preloaders_for_one(association.to_sym, records, scope)
+ preloaders_for_hash(association, records, scope, polymorphic_parent)
+ when Symbol, String
+ preloaders_for_one(association, records, scope, polymorphic_parent)
else
raise ArgumentError, "#{association.inspect} was not recognized for preload"
end
end
- def preloaders_for_hash(association, records, scope)
+ def preloaders_for_hash(association, records, scope, polymorphic_parent)
association.flat_map { |parent, child|
- loaders = preloaders_for_one parent, records, scope
-
- recs = loaders.flat_map(&:preloaded_records).uniq
- loaders.concat Array.wrap(child).flat_map { |assoc|
- preloaders_on assoc, recs, scope
- }
- loaders
+ grouped_records(parent, records, polymorphic_parent).flat_map do |reflection, reflection_records|
+ loaders = preloaders_for_reflection(reflection, reflection_records, scope)
+ recs = loaders.flat_map(&:preloaded_records)
+ child_polymorphic_parent = reflection && reflection.options[:polymorphic]
+ loaders.concat Array.wrap(child).flat_map { |assoc|
+ preloaders_on assoc, recs, scope, child_polymorphic_parent
+ }
+ loaders
+ end
}
end
# Loads all the given data into +records+ for a singular +association+.
#
- # Functions by instantiating a preloader class such as Preloader::HasManyThrough and
+ # Functions by instantiating a preloader class such as Preloader::Association and
# call the +run+ method for each passed in class in the +records+ argument.
#
# Not all records have the same class, so group then preload group on the reflection
@@ -135,24 +134,28 @@ module ActiveRecord
# Additionally, polymorphic belongs_to associations can have multiple associated
# classes, depending on the polymorphic_type field. So we group by the classes as
# well.
- def preloaders_for_one(association, records, scope)
- grouped_records(association, records).flat_map do |reflection, klasses|
- klasses.map do |rhs_klass, rs|
- loader = preloader_for(reflection, rs).new(rhs_klass, rs, reflection, scope)
- loader.run self
- loader
+ def preloaders_for_one(association, records, scope, polymorphic_parent)
+ grouped_records(association, records, polymorphic_parent)
+ .flat_map do |reflection, reflection_records|
+ preloaders_for_reflection reflection, reflection_records, scope
end
+ end
+
+ def preloaders_for_reflection(reflection, records, scope)
+ records.group_by { |record| record.association(reflection.name).klass }.map do |rhs_klass, rs|
+ loader = preloader_for(reflection, rs).new(rhs_klass, rs, reflection, scope)
+ loader.run self
+ loader
end
end
- def grouped_records(association, records)
+ def grouped_records(association, records, polymorphic_parent)
h = {}
records.each do |record|
next unless record
- assoc = record.association(association)
- next unless assoc.klass
- klasses = h[assoc.reflection] ||= {}
- (klasses[assoc.klass] ||= []) << record
+ reflection = record.class._reflect_on_association(association)
+ next if polymorphic_parent && !reflection || !record.association(association).klass
+ (h[reflection] ||= []) << record
end
h
end
@@ -169,7 +172,7 @@ module ActiveRecord
owners.flat_map { |owner| owner.association(reflection.name).target }
end
- protected
+ private
attr_reader :owners, :reflection
end
diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb
index 735da152b7..041e62077c 100644
--- a/activerecord/lib/active_record/associations/preloader/association.rb
+++ b/activerecord/lib/active_record/associations/preloader/association.rb
@@ -27,10 +27,9 @@ module ActiveRecord
end
end
- protected
+ private
attr_reader :owners, :reflection, :preload_scope, :model, :klass
- private
# The name of the key on the associated records
def association_key_name
reflection.join_primary_key(klass)
@@ -44,8 +43,7 @@ module ActiveRecord
def associate_records_to_owner(owner, records)
association = owner.association(reflection.name)
if reflection.collection?
- association.loaded!
- association.target.concat(records)
+ association.target = records
else
association.target = records.first
end
@@ -82,11 +80,11 @@ module ActiveRecord
end
def association_key_type
- @klass.type_for_attribute(association_key_name.to_s).type
+ @klass.type_for_attribute(association_key_name).type
end
def owner_key_type
- @model.type_for_attribute(owner_key_name.to_s).type
+ @model.type_for_attribute(owner_key_name).type
end
def load_records(&block)
@@ -118,7 +116,7 @@ module ActiveRecord
scope = klass.scope_for_association
if reflection.type
- scope.where!(reflection.type => model.base_class.sti_name)
+ scope.where!(reflection.type => model.polymorphic_name)
end
scope.merge!(reflection_scope) if reflection.scope
diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb
index 441bd715e4..a92932fa4b 100644
--- a/activerecord/lib/active_record/associations/singular_association.rb
+++ b/activerecord/lib/active_record/associations/singular_association.rb
@@ -17,9 +17,8 @@ module ActiveRecord
replace(record)
end
- def build(attributes = {})
- record = build_record(attributes)
- yield(record) if block_given?
+ def build(attributes = {}, &block)
+ record = build_record(attributes, &block)
set_new_record(record)
record
end
@@ -27,7 +26,7 @@ module ActiveRecord
# Implements the reload reader method, e.g. foo.reload_bar for
# Foo.has_one :bar
def force_reload_reader
- klass.uncached { reload }
+ reload(true)
target
end
@@ -37,21 +36,7 @@ module ActiveRecord
end
def find_target
- scope = self.scope
- return scope.take if skip_statement_cache?(scope)
-
- conn = klass.connection
- sc = reflection.association_scope_cache(conn, owner) do |params|
- as = AssociationScope.create { params.bind }
- target_scope.merge!(as.scope(self)).limit(1)
- end
-
- binds = AssociationScope.get_bind_values(owner, reflection.chain)
- sc.execute(binds, conn) do |record|
- set_inverse_instance record
- end.first
- rescue ::RangeError
- nil
+ super.first
end
def replace(record)
@@ -62,13 +47,8 @@ module ActiveRecord
replace(record)
end
- def _create_record(attributes, raise_error = false)
- unless owner.persisted?
- raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved"
- end
-
- record = build_record(attributes)
- yield(record) if block_given?
+ def _create_record(attributes, raise_error = false, &block)
+ record = build_record(attributes, &block)
saved = record.save
set_new_record(record)
raise RecordInvalid.new(record) if !saved && raise_error
diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb
index bce2a95ce1..15e6565e69 100644
--- a/activerecord/lib/active_record/associations/through_association.rb
+++ b/activerecord/lib/active_record/associations/through_association.rb
@@ -4,9 +4,24 @@ module ActiveRecord
module Associations
# = Active Record Through Association
module ThroughAssociation #:nodoc:
- delegate :source_reflection, :through_reflection, to: :reflection
+ delegate :source_reflection, to: :reflection
private
+ def through_reflection
+ @through_reflection ||= begin
+ refl = reflection.through_reflection
+
+ while refl.through_reflection?
+ refl = refl.through_reflection
+ end
+
+ refl
+ end
+ end
+
+ def through_association
+ @through_association ||= owner.association(through_reflection.name)
+ end
# We merge in these scopes for two reasons:
#
@@ -38,24 +53,22 @@ module ActiveRecord
def construct_join_attributes(*records)
ensure_mutable
- if source_reflection.association_primary_key(reflection.klass) == reflection.klass.primary_key
+ association_primary_key = source_reflection.association_primary_key(reflection.klass)
+
+ if association_primary_key == reflection.klass.primary_key && !options[:source_type]
join_attributes = { source_reflection.name => records }
else
join_attributes = {
- source_reflection.foreign_key =>
- records.map { |record|
- record.send(source_reflection.association_primary_key(reflection.klass))
- }
+ source_reflection.foreign_key => records.map(&association_primary_key.to_sym)
}
end
if options[:source_type]
- join_attributes[source_reflection.foreign_type] =
- records.map { |record| record.class.base_class.name }
+ join_attributes[source_reflection.foreign_type] = [ options[:source_type] ]
end
if records.count == 1
- Hash[join_attributes.map { |k, v| [k, v.first] }]
+ join_attributes.transform_values!(&:first)
else
join_attributes
end
@@ -101,7 +114,7 @@ module ActiveRecord
attributes[inverse.foreign_key] = target.id
end
- super(attributes)
+ super
end
end
end