diff options
Diffstat (limited to 'activerecord/lib/active_record/associations')
35 files changed, 740 insertions, 981 deletions
diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb index 3963008a76..272eede824 100644 --- a/activerecord/lib/active_record/associations/alias_tracker.rb +++ b/activerecord/lib/active_record/associations/alias_tracker.rb @@ -1,51 +1,41 @@ +# frozen_string_literal: true + require "active_support/core_ext/string/conversions" module ActiveRecord module Associations # Keeps track of table aliases for ActiveRecord::Associations::JoinDependency class AliasTracker # :nodoc: - attr_reader :aliases - - def self.create(connection, initial_table, type_caster) - aliases = Hash.new(0) - aliases[initial_table] = 1 - new connection, aliases, type_caster - end - - def self.create_with_joins(connection, initial_table, joins, type_caster) + def self.create(connection, initial_table, joins) if joins.empty? - create(connection, initial_table, type_caster) + aliases = Hash.new(0) else aliases = Hash.new { |h, k| h[k] = initial_count_for(connection, k, joins) } - aliases[initial_table] = 1 - new connection, aliases, type_caster end + aliases[initial_table] = 1 + new(connection, aliases) end def self.initial_count_for(connection, name, table_joins) - # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase - quoted_name = connection.quote_table_name(name).downcase + quoted_name = nil counts = table_joins.map do |join| if join.is_a?(Arel::Nodes::StringJoin) + # quoted_name should be case ignored as some database adapters (Oracle) return quoted name in uppercase + quoted_name ||= connection.quote_table_name(name) + # Table names + table aliases - join.left.downcase.scan( - /join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/ + join.left.scan( + /JOIN(?:\s+\w+)?\s+(?:\S+\s+)?(?:#{quoted_name}|#{name})\sON/i ).size - elsif join.respond_to? :left - join.left.table_name == name ? 1 : 0 + elsif join.is_a?(Arel::Nodes::Join) + join.left.name == name ? 1 : 0 + elsif join.is_a?(Hash) + 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 @@ -53,17 +43,16 @@ module ActiveRecord end # table_joins is an array of arel joins which might conflict with the aliases we assign here - def initialize(connection, aliases, type_caster) + def initialize(connection, aliases) @aliases = aliases @connection = connection - @type_caster = type_caster end - def aliased_table_for(table_name, aliased_name) + def aliased_table_for(table_name, aliased_name, type_caster) if aliases[table_name].zero? # If it's zero, we can have our table_name aliases[table_name] = 1 - Arel::Table.new(table_name, type_caster: @type_caster) + Arel::Table.new(table_name, type_caster: type_caster) else # Otherwise, we need to use an alias aliased_name = @connection.table_alias_for(aliased_name) @@ -76,10 +65,12 @@ module ActiveRecord else aliased_name end - Arel::Table.new(table_name, type_caster: @type_caster).alias(table_alias) + Arel::Table.new(table_name, type_caster: type_caster).alias(table_alias) end end + attr_reader :aliases + private def truncate(name) diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 1cb2b2d7c6..44596f4424 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "active_support/core_ext/array/wrap" module ActiveRecord @@ -17,7 +19,6 @@ module ActiveRecord # HasManyThroughAssociation + ThroughAssociation class Association #:nodoc: attr_reader :owner, :target, :reflection - attr_accessor :inversed delegate :options, to: :reflection @@ -30,14 +31,6 @@ module ActiveRecord reset_scope end - # Returns the name of the table of the associated class: - # - # post.comments.aliased_table_name # => "comments" - # - def aliased_table_name - klass.table_name - end - # Resets the \loaded flag to +false+ and sets the \target to +nil+. def reset @loaded = false @@ -73,7 +66,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+. @@ -94,7 +87,7 @@ module ActiveRecord # actually gets built. def association_scope if klass - @association_scope ||= AssociationScope.scope(self, klass.connection) + @association_scope ||= AssociationScope.scope(self) end end @@ -104,23 +97,24 @@ module ActiveRecord # 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 # 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 + # Returns the class of the target. belongs_to polymorphic overrides this to look at the # polymorphic_type field on the owner. def klass @@ -130,7 +124,17 @@ module ActiveRecord # 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) + AssociationRelation.create(klass, self).merge!(klass.all) + end + + def extensions + extensions = klass.default_extensions | reflection.extensions + + if reflection.scope + extensions |= reflection.scope_for(klass.unscoped, owner).extensions + end + + extensions end # Loads the \target if needed and returns it. @@ -152,17 +156,9 @@ module ActiveRecord reset end - def interpolate(sql, record = nil) - if sql.respond_to?(:to_proc) - owner.instance_exec(record, &sql) - else - sql - end - 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 @@ -177,8 +173,8 @@ module ActiveRecord skip_assign = [reflection.foreign_key, reflection.type].compact assigned_keys = record.changed_attribute_names_to_save assigned_keys += except_from_scope_attributes.keys.map(&:to_s) - attributes = create_scope.except(*(assigned_keys - skip_assign)) - record.assign_attributes(attributes) + attributes = scope_for_create.except!(*(assigned_keys - skip_assign)) + record.send(:_assign_attributes, attributes) if attributes.any? set_inverse_instance(record) end @@ -191,6 +187,9 @@ module ActiveRecord end private + def scope_for_create + scope.scope_for_create + end def find_target? !loaded? && (!owner.new_record? || foreign_key_present?) && klass @@ -202,8 +201,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 @@ -241,6 +240,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. @@ -270,11 +275,12 @@ module ActiveRecord def build_record(attributes) reflection.build_association(attributes) do |record| initialize_attributes(record, attributes) + yield(record) if block_given? end end # Returns true if statement cache should be skipped on the association reader. - def skip_statement_cache? + def skip_statement_cache?(scope) reflection.has_scope? || scope.eager_loading? || klass.scope_attributes? || diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index badde9973f..0a90a6104a 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + module ActiveRecord module Associations class AssociationScope #:nodoc: - def self.scope(association, connection) - INSTANCE.scope(association, connection) + def self.scope(association) + INSTANCE.scope(association) end def self.create(&block) @@ -16,20 +18,15 @@ module ActiveRecord INSTANCE = create - def scope(association, connection) + def scope(association) klass = association.klass reflection = association.reflection scope = klass.unscoped owner = association.owner - alias_tracker = AliasTracker.create connection, association.klass.table_name, klass.type_caster - chain_head, chain_tail = get_chain(reflection, association, alias_tracker) - - scope.extending! Array(reflection.options[:extend]) - add_constraints(scope, owner, klass, reflection, chain_head, chain_tail) - end + chain = get_chain(reflection, association, scope.alias_tracker) - def join_type - Arel::Nodes::InnerJoin + scope.extending! reflection.extensions + add_constraints(scope, owner, chain) end def self.get_bind_values(owner, chain) @@ -38,39 +35,36 @@ 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), join_type) + table.create_join(table, table.create_on(constraint)) end - def last_chain_scope(scope, table, reflection, owner, association_klass) - join_keys = reflection.join_keys(association_klass) + def last_chain_scope(scope, reflection, owner) + join_keys = reflection.join_keys key = join_keys.key foreign_key = join_keys.foreign_key + table = reflection.aliased_table value = transform_value(owner[foreign_key]) - scope = scope.where(table.name => { key => value }) + scope = apply_scope(scope, table, key, value) if reflection.type - polymorphic_type = transform_value(owner.class.base_class.name) - scope = scope.where(table.name => { reflection.type => polymorphic_type }) + polymorphic_type = transform_value(owner.class.polymorphic_name) + scope = apply_scope(scope, table, reflection.type, polymorphic_type) end scope @@ -80,28 +74,29 @@ module ActiveRecord value_transformation.call(value) end - def next_chain_scope(scope, table, reflection, association_klass, foreign_table, next_reflection) - join_keys = reflection.join_keys(association_klass) + def next_chain_scope(scope, reflection, next_reflection) + join_keys = reflection.join_keys key = join_keys.key foreign_key = join_keys.foreign_key + table = reflection.aliased_table + foreign_table = next_reflection.aliased_table constraint = table[key].eq(foreign_table[foreign_key]) if reflection.type - value = transform_value(next_reflection.klass.base_class.name) - scope = scope.where(table.name => { reflection.type => value }) + value = transform_value(next_reflection.klass.polymorphic_name) + scope = apply_scope(scope, table, reflection.type, value) end - scope = scope.joins(join(foreign_table, constraint)) + scope.joins!(join(foreign_table, constraint)) end class ReflectionProxy < SimpleDelegator # :nodoc: - attr_accessor :next - attr_reader :alias_name + attr_reader :aliased_table - def initialize(reflection, alias_name) + def initialize(reflection, aliased_table) super(reflection) - @alias_name = alias_name + @aliased_table = aliased_table end def all_includes; nil; end @@ -109,39 +104,34 @@ module ActiveRecord def get_chain(reflection, association, tracker) name = reflection.name - runtime_reflection = Reflection::RuntimeReflection.new(reflection, association) - previous_reflection = runtime_reflection + chain = [Reflection::RuntimeReflection.new(reflection, association)] reflection.chain.drop(1).each do |refl| - alias_name = tracker.aliased_table_for(refl.table_name, refl.alias_candidate(name)) - proxy = ReflectionProxy.new(refl, alias_name) - previous_reflection.next = proxy - previous_reflection = proxy + aliased_table = tracker.aliased_table_for( + refl.table_name, + refl.alias_candidate(name), + refl.klass.type_caster + ) + chain << ReflectionProxy.new(refl, aliased_table) end - [runtime_reflection, previous_reflection] + chain end - def add_constraints(scope, owner, association_klass, refl, chain_head, chain_tail) - owner_reflection = chain_tail - table = owner_reflection.alias_name - scope = last_chain_scope(scope, table, owner_reflection, owner, association_klass) - - reflection = chain_head - while reflection - table = reflection.alias_name - next_reflection = reflection.next + def add_constraints(scope, owner, chain) + scope = last_chain_scope(scope, chain.last, owner) - unless reflection == chain_tail - foreign_table = next_reflection.alias_name - scope = next_chain_scope(scope, table, reflection, association_klass, foreign_table, next_reflection) - end + chain.each_cons(2) do |reflection, next_reflection| + scope = next_chain_scope(scope, reflection, next_reflection) + end + chain_head = chain.first + chain.reverse_each do |reflection| # Exclude the scope of the association itself, because that # was already merged in the #scope method. reflection.constraints.each do |scope_chain_item| - item = eval_scope(reflection.klass, table, scope_chain_item, owner) + item = eval_scope(reflection, scope_chain_item, owner) - if scope_chain_item == refl.scope - scope.merge! item.except(:where, :includes) + if scope_chain_item == chain_head.scope + scope.merge! item.except(:where, :includes, :unscope, :order) end reflection.all_includes do @@ -150,18 +140,24 @@ 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 - - reflection = next_reflection end scope end - def eval_scope(klass, table, scope, owner) - predicate_builder = PredicateBuilder.new(TableMetadata.new(klass, table)) - ActiveRecord::Relation.create(klass, table, predicate_builder).instance_exec(owner, &scope) + def apply_scope(scope, table, key, value) + if scope.table == table + scope.where!(key => value) + else + scope.where!(table.name => { key => value }) + end + end + + def eval_scope(reflection, scope, owner) + relation = reflection.build_scope(reflection.aliased_table) + relation.instance_exec(owner, &scope) || relation 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 64b2311911..3d4ad1dd5b 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -1,24 +1,28 @@ +# frozen_string_literal: true + module ActiveRecord - # = Active Record Belongs To Association module Associations + # = 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) + writer(owner.instance_exec(&block)) if reader.nil? end def reset @@ -38,14 +42,32 @@ module ActiveRecord update_counters(1) end + def target_changed? + owner.saved_change_to_attribute?(reflection.foreign_key) + end + private + def replace(record) + if record + raise_on_type_mismatch!(record) + update_counters_on_replace(record) + set_inverse_instance(record) + @updated = true + else + decrement_counters + 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) + target.increment!(reflection.counter_cache_column, by, touch: reflection.options[:touch]) else - klass.update_counters(target_id, reflection.counter_cache_column => by) + counter_cache_target.update_counters(reflection.counter_cache_column => by, touch: reflection.options[:touch]) end end end @@ -61,22 +83,22 @@ module ActiveRecord 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) + record.increment!(reflection.counter_cache_column, touch: reflection.options[:touch]) 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) + record._read_attribute(primary_key(record)) != 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)) : nil end - def remove_keys - owner[reflection.foreign_key] = nil + def primary_key(record) + reflection.association_primary_key(record.class) end def foreign_key_present? @@ -90,12 +112,9 @@ 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 + def counter_cache_target + primary_key = reflection.association_primary_key(klass) + klass.unscoped.where!(primary_key => owner._read_attribute(reflection.foreign_key)) end def stale_state 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 b710cf6bdb..3fd2fb5f67 100644 --- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb @@ -1,22 +1,22 @@ +# frozen_string_literal: true + module ActiveRecord - # = Active Record Belongs To Polymorphic Association module Associations + # = Active Record Belongs To Polymorphic Association class BelongsToPolymorphicAssociation < BelongsToAssociation #:nodoc: def klass type = owner[reflection.foreign_type] 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 + owner[reflection.foreign_type] = record ? record.class.polymorphic_name : nil end def different_target?(record) diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb index d0534056d9..7c69cd65ee 100644 --- a/activerecord/lib/active_record/associations/builder/association.rb +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # This is the parent Association class which defines the variables # used by all associations. # @@ -36,11 +38,6 @@ module ActiveRecord::Associations::Builder # :nodoc: def self.create_reflection(model, name, scope, options, extension = nil) raise ArgumentError, "association names must be a Symbol" unless name.kind_of?(Symbol) - if scope.is_a?(Hash) - options = scope - scope = nil - end - validate_options(options) scope = build_scope(scope, extension) @@ -107,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 a1609ab0fb..0166ed98ca 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord::Associations::Builder # :nodoc: class BelongsTo < SingularAssociation #:nodoc: def self.macro @@ -5,7 +7,7 @@ module ActiveRecord::Associations::Builder # :nodoc: end def self.valid_options(options) - super + [:polymorphic, :touch, :counter_cache, :optional] + super + [:polymorphic, :touch, :counter_cache, :optional, :default] end def self.valid_dependent_options @@ -16,6 +18,7 @@ module ActiveRecord::Associations::Builder # :nodoc: super add_counter_cache_callbacks(model, reflection) if reflection.options[:counter_cache] add_touch_callbacks(model, reflection) if reflection.options[:touch] + add_default_callbacks(model, reflection) if reflection.options[:default] end def self.define_accessors(mixin, reflection) @@ -31,11 +34,9 @@ module ActiveRecord::Associations::Builder # :nodoc: foreign_key = reflection.foreign_key cache_column = reflection.counter_cache_column - if (@_after_create_counter_called ||= false) - @_after_create_counter_called = false - elsif (@_after_replace_counter_called ||= false) + if @_after_replace_counter_called ||= false @_after_replace_counter_called = false - elsif saved_change_to_attribute?(foreign_key) && !new_record? + elsif association(reflection.name).target_changed? if reflection.polymorphic? model = attribute_in_database(reflection.foreign_type).try(:constantize) model_was = attribute_before_last_save(reflection.foreign_type).try(:constantize) @@ -47,15 +48,21 @@ module ActiveRecord::Associations::Builder # :nodoc: 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) + if foreign_key && model < ActiveRecord::Base + counter_cache_target(reflection, model, foreign_key).update_counters(cache_column => 1) end - if foreign_key_was && model_was.respond_to?(:decrement_counter) - model_was.decrement_counter(cache_column, foreign_key_was) + if foreign_key_was && model_was < ActiveRecord::Base + counter_cache_target(reflection, model_was, foreign_key_was).update_counters(cache_column => -1) end end end + + private + def counter_cache_target(reflection, model, foreign_key) + primary_key = reflection.association_primary_key(model) + model.unscoped.where!(primary_key => foreign_key) + end end end @@ -83,7 +90,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 @@ -113,9 +121,19 @@ module ActiveRecord::Associations::Builder # :nodoc: BelongsTo.touch_record(record, record.send(changes_method), foreign_key, n, touch, belongs_to_touch_method) }} - model.after_save callback.(:saved_changes), if: :saved_changes? - model.after_touch callback.(:changes_to_save) - model.after_destroy callback.(:changes_to_save) + unless reflection.counter_cache_column + model.after_create 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 + + def self.add_default_callbacks(model, reflection) + model.before_validation lambda { |o| + o.association(reflection.name).default(&reflection.options[:default]) + } end def self.add_destroy_callbacks(model, reflection) diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb index edeb6491bd..35a72c3850 100644 --- a/activerecord/lib/active_record/associations/builder/collection_association.rb +++ b/activerecord/lib/active_record/associations/builder/collection_association.rb @@ -1,4 +1,4 @@ -# This class is inherited by the has_many and has_many_and_belongs_to_many association classes +# frozen_string_literal: true require "active_record/associations" 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 6b71826431..e3070e0472 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 @@ -1,38 +1,7 @@ +# frozen_string_literal: true + 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) @@ -42,10 +11,8 @@ 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; + class << self attr_accessor :left_model attr_accessor :name attr_accessor :table_name_resolver @@ -54,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) @@ -84,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 @@ -115,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/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb index 7864d4c536..5b9617bc6d 100644 --- a/activerecord/lib/active_record/associations/builder/has_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_many.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord::Associations::Builder # :nodoc: class HasMany < CollectionAssociation #:nodoc: def self.macro diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb index 4de846d12b..bfb37d6eee 100644 --- a/activerecord/lib/active_record/associations/builder/has_one.rb +++ b/activerecord/lib/active_record/associations/builder/has_one.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord::Associations::Builder # :nodoc: class HasOne < SingularAssociation #:nodoc: def self.macro diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb index 7732b63af6..0a02ef4cc1 100644 --- a/activerecord/lib/active_record/associations/builder/singular_association.rb +++ b/activerecord/lib/active_record/associations/builder/singular_association.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # This class is inherited by the has_one and belongs_to association classes module ActiveRecord::Associations::Builder # :nodoc: diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 77282e6463..840d900bbc 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module Associations # = Active Record Association Collection @@ -30,7 +32,8 @@ module ActiveRecord reload end - CollectionProxy.create(klass, self) + @proxy ||= CollectionProxy.create(klass, self) + @proxy.reset_scope end # Implements the writer method, e.g. foo.items= for Foo.has_many :items @@ -42,24 +45,28 @@ 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 ||= ( - column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}" - scope.pluck(column) - ) + @association_ids ||= scope.pluck(reflection.association_primary_key) end end # 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) } - records = klass.where(reflection.association_primary_key => ids).index_by do |r| - r.send(reflection.association_primary_key) + + records = klass.where(primary_key => ids).index_by do |r| + r.public_send(primary_key) end.values_at(*ids).compact + if records.size != ids.size - klass.all.raise_record_not_found_exception!(ids, records.size, ids.size, reflection.association_primary_key) + found_ids = records.map { |record| record.public_send(primary_key) } + not_found_ids = ids - found_ids + klass.all.raise_record_not_found_exception!(ids, records.size, ids.size, primary_key, not_found_ids) else replace(records) end @@ -68,26 +75,29 @@ module ActiveRecord def reset super @target = [] + @association_ids = nil end def find(*args) - if block_given? - load_target.find(*args) { |*block_args| yield(*block_args) } - else - if options[:inverse_of] && loaded? - args_flatten = args.flatten - raise RecordNotFound, "Couldn't find #{scope.klass.name} without an ID" if args_flatten.blank? - result = find_by_scan(*args) - - result_size = Array(result).size - if !result || result_size != args_flatten.size - scope.raise_record_not_found_exception!(args_flatten, result_size, args_flatten.size) - else - result - end + if options[:inverse_of] && loaded? + args_flatten = args.flatten + model = scope.klass + + if args_flatten.blank? + error_message = "Couldn't find #{model.name} without an ID" + raise RecordNotFound.new(error_message, model.name, model.primary_key, args) + end + + result = find_by_scan(*args) + + result_size = Array(result).size + if !result || result_size != args_flatten.size + scope.raise_record_not_found_exception!(args_flatten, result_size, args_flatten.size) else - scope.find(*args) + result end + else + scope.find(*args) end end @@ -95,9 +105,7 @@ 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 @@ -179,8 +187,6 @@ module ActiveRecord # are actually removed from the database, that depends precisely on # +delete_records+. They are in any case removed from the collection. def delete(*records) - return if records.empty? - records = find(records) if records.any? { |record| record.kind_of?(Integer) || record.kind_of?(String) } delete_or_destroy(records, options[:dependent]) end @@ -190,8 +196,6 @@ module ActiveRecord # Note that this method removes records from the database ignoring the # +:dependent+ option. def destroy(*records) - return if records.empty? - records = find(records) if records.any? { |record| record.kind_of?(Integer) || record.kind_of?(String) } delete_or_destroy(records, :destroy) end @@ -208,9 +212,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 @@ -227,10 +233,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 size.zero? else - @target.blank? && !scope.exists? + target.empty? && !scope.exists? end end @@ -280,35 +286,6 @@ module ActiveRecord replace_on_target(record, index, skip_callbacks, &block) end - def replace_on_target(record, index, skip_callbacks) - callback(:before_add, record) unless skip_callbacks - - begin - if index - record_was = target[index] - target[index] = record - else - target << record - end - - set_inverse_instance(record) - - yield(record) if block_given? - rescue - if index - target[index] = record_was - else - target.delete(record) - end - - raise - end - - callback(:after_add, record) unless skip_callbacks - - record - end - def scope scope = super scope.none! if null_scope? @@ -328,18 +305,17 @@ module ActiveRecord private def find_target - return scope.to_a if skip_statement_cache? + scope = self.scope + return scope.to_a if skip_statement_cache?(scope) conn = klass.connection - sc = reflection.association_scope_cache(conn, owner) do - StatementCache.create(conn) { |params| - as = AssociationScope.create { params.bind } - target_scope.merge as.scope(self, conn) - } + 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, klass, conn) do |record| + sc.execute(binds, conn) do |record| set_inverse_instance(record) end end @@ -382,25 +358,33 @@ 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 # Do the relevant stuff to insert the given record into the association collection. - def insert_record(record, validate = true, raise = false) - raise NotImplementedError - end - - def create_scope - scope.scope_for_create.stringify_keys + def insert_record(record, validate = true, raise = false, &block) + if raise + record.save!(validate: validate, &block) + else + record.save(validate: validate, &block) + end end def delete_or_destroy(records, method) + return if records.empty? + records = find(records) if records.any? { |record| record.kind_of?(Integer) || record.kind_of?(String) } records = records.flatten records.each { |record| raise_on_type_mismatch!(record) } existing_records = records.reject(&:new_record?) @@ -416,7 +400,7 @@ 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 records.each { |record| callback(:after_remove, record) } end @@ -448,17 +432,46 @@ module ActiveRecord end end - def concat_records(records, should_raise = false) + def concat_records(records, raise = false) result = true records.each do |record| raise_on_type_mismatch!(record) - add_to_target(record) do |rec| - result &&= insert_record(rec, true, should_raise) unless owner.new_record? + add_to_target(record) do + 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) + callback(:before_add, record) unless skip_callbacks + + set_inverse_instance(record) + + @_was_loaded = true + + yield(record) if block_given? + + if index + target[index] = record + elsif @_was_loaded || !loaded? + target << record + end + + callback(:after_add, record) unless skip_callbacks + + record + ensure + @_was_loaded = nil end def callback(method, record) diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 55bf2e0ff0..9a30198b95 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module Associations # Association proxies in Active Record are middlemen between the object that @@ -30,7 +32,10 @@ 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? end def target @@ -78,7 +83,7 @@ module ActiveRecord # # #<Pet id: nil, name: "Choo-Choo"> # # ] # - # person.pets.select(:id, :name ) + # person.pets.select(:id, :name) # # => [ # # #<Pet id: 1, name: "Fancy-Fancy">, # # #<Pet id: 2, name: "Spook">, @@ -130,8 +135,9 @@ module ActiveRecord # # #<Pet id: 2, name: "Spook", person_id: 1>, # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> # # ] - def find(*args, &block) - @association.find(*args, &block) + def find(*args) + return super if block_given? + @association.find(*args) end ## @@ -743,10 +749,6 @@ module ActiveRecord # # ] #-- - def uniq - load_target.uniq - end - def calculate(operation, column_name) null_scope? ? scope.calculate(operation, column_name) : super end @@ -986,6 +988,12 @@ module ActiveRecord load_target == other end + ## + # :method: to_ary + # + # :call-seq: + # to_ary() + # # Returns a new array of objects from the collection. If the collection # hasn't been loaded, it fetches the records from the database. # @@ -1019,10 +1027,6 @@ module ActiveRecord # # #<Pet id: 5, name: "Brain", person_id: 1>, # # #<Pet id: 6, name: "Boss", person_id: 1> # # ] - def to_ary - load_target.dup - end - alias_method :to_a, :to_ary def records # :nodoc: load_target @@ -1070,7 +1074,6 @@ module ActiveRecord end # Reloads the collection from the database. Returns +self+. - # Equivalent to <tt>collection(true)</tt>. # # class Person < ActiveRecord::Base # has_many :pets @@ -1084,13 +1087,9 @@ module ActiveRecord # # person.pets.reload # fetches pets from the database # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>] - # - # person.pets(true) # fetches pets from the database - # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>] def reload - @scope = nil proxy_association.reload - self + reset_scope end # Unloads the association. Returns +self+. @@ -1110,9 +1109,14 @@ module ActiveRecord # person.pets # fetches pets from the database # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>] def reset - @scope = nil proxy_association.reset proxy_association.reset_scope + reset_scope + end + + def reset_scope # :nodoc: + @offsets = {} + @scope = nil self end @@ -1121,7 +1125,7 @@ module ActiveRecord SpawnMethods, ].flat_map { |klass| klass.public_instance_methods(false) - } - self.public_instance_methods(false) + [:scoping] + } - self.public_instance_methods(false) - [:select] + [:scoping] delegate(*delegate_methods, to: :scope) @@ -1148,18 +1152,6 @@ module ActiveRecord def exec_queries load_target end - - def respond_to_missing?(method, _) - scope.respond_to?(method) || super - end - - def method_missing(method, *args, &block) - if scope.respond_to?(method) - scope.public_send(method, *args, &block) - else - super - end - end end end end diff --git a/activerecord/lib/active_record/associations/foreign_association.rb b/activerecord/lib/active_record/associations/foreign_association.rb index 3ceec0ee46..40010cde03 100644 --- a/activerecord/lib/active_record/associations/foreign_association.rb +++ b/activerecord/lib/active_record/associations/foreign_association.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord::Associations module ForeignAssociation # :nodoc: def foreign_key_present? diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index b413eb3f9c..cf85a87fa7 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + module ActiveRecord - # = Active Record Has Many Association module Associations + # = Active Record Has Many Association # This is the proxy that handles a has many association. # # If the association has a <tt>:through</tt> option further specialization @@ -31,12 +33,7 @@ module ActiveRecord def insert_record(record, validate = true, raise = false) set_owner_attributes(record) - - if raise - record.save!(validate: validate) - else - record.save(validate: validate) - end + super end def empty? @@ -66,7 +63,7 @@ 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 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 c4a7fe4432..617956c768 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -1,14 +1,14 @@ +# frozen_string_literal: true + module ActiveRecord - # = Active Record Has Many Through Association module Associations + # = Active Record Has Many Through Association class HasManyThroughAssociation < HasManyAssociation #:nodoc: include ThroughAssociation def initialize(owner, reflection) super - - @through_records = {} - @through_association = nil + @through_records = {} end def concat(*records) @@ -39,11 +39,7 @@ module ActiveRecord ensure_not_nested if record.new_record? || record.has_changes_to_save? - if raise - record.save!(validate: validate) - else - return unless record.save(validate: validate) - end + return unless super end save_through_record(record) @@ -52,11 +48,6 @@ module ActiveRecord end private - - def through_association - @through_association ||= owner.association(through_reflection.name) - end - # The through record (built with build_record) is temporarily cached # so that it may be reused if insert_record is subsequently called. # @@ -99,7 +90,7 @@ module ActiveRecord def build_record(attributes) ensure_not_nested - record = super(attributes) + record = super inverse = source_reflection.inverse_of if inverse @@ -113,6 +104,11 @@ module ActiveRecord record end + def remove_records(existing_records, records, method) + super + delete_through_records(records) + end + def target_reflection_has_associated_record? !(through_reflection.belongs_to? && owner[through_reflection.foreign_key].blank?) end @@ -137,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", scope.bound_attributes) + count = scope.delete_all end when :nullify count = scope.update_all(source_reflection.foreign_key => nil) diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 8458253ff8..390bfd8b08 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + module ActiveRecord - # = Active Record Has One Association module Associations + # = Active Record Has One Association class HasOneAssociation < SingularAssociation #:nodoc: include ForeignAssociation @@ -21,42 +23,15 @@ 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 when :delete target.delete 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? end @@ -64,6 +39,33 @@ module ActiveRecord 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 @@ -78,6 +80,7 @@ module ActiveRecord when :delete target.delete when :destroy + target.destroyed_by_association = reflection target.destroy else nullify_owner_attributes(target) @@ -102,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 1183bdf6f4..10978b2d93 100644 --- a/activerecord/lib/active_record/associations/has_one_through_association.rb +++ b/activerecord/lib/active_record/associations/has_one_through_association.rb @@ -1,20 +1,21 @@ +# frozen_string_literal: true + module ActiveRecord - # = Active Record Has One Through Association module Associations + # = Active Record Has One Through Association 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 @@ -27,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 87e0847ec1..b76005b587 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module Associations class JoinDependency # :nodoc: @@ -12,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 @@ -23,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) @@ -33,20 +32,14 @@ module ActiveRecord end Table = Struct.new(:node, :columns) do # :nodoc: - def table - Arel::Nodes::TableAlias.new node.table, node.aliased_table_name - end - def column_aliases - t = table + t = node.table columns.map { |column| t[column.name].as Arel.sql column.alias } end end Column = Struct.new(:name, :alias) end - attr_reader :alias_tracker, :base_klass, :join_root - def self.make_tree(associations) hash = {} walk_tree associations, hash @@ -71,70 +64,32 @@ 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, associations, joins, eager_loading: true) - @alias_tracker = AliasTracker.create_with_joins(base.connection, base.table_name, joins, base.type_caster) - @eager_loading = eager_loading + def initialize(base, table, associations) tree = self.class.make_tree associations - @join_root = JoinBase.new base, build(tree, base) - @join_root.children.each { |child| construct_tables! @join_root, child } + @join_root = JoinBase.new(base, table, build(tree, base)) end def reflections join_root.drop(1).map!(&:reflection) end - def join_constraints(outer_joins, join_type) - joins = join_root.children.flat_map { |child| + def join_constraints(joins_to_add, join_type, alias_tracker) + @alias_tracker = alias_tracker - if join_type == Arel::Nodes::OuterJoin - make_left_outer_joins join_root, child - else - make_inner_joins join_root, child - end - } + construct_tables!(join_root) + joins = make_join_constraints(join_root, join_type) - joins.concat outer_joins.flat_map { |oj| + 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_outer_joins oj.join_root, child - } + make_join_constraints(oj.join_root, join_type) end } end - def 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, aliases) + def instantiate(result_set, &block) primary_key = aliases.column_alias(join_root, join_root.primary_key) seen = Hash.new { |i, object_id| @@ -157,65 +112,65 @@ module ActiveRecord message_bus.instrument("instantiation.active_record", payload) do 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) - construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases) + parent = parents[parent_key] ||= join_root.instantiate(row_hash, column_aliases, &block) + construct(parent, join_root, row_hash, seen, model_cache) } end parents.values end - private + def apply_column_aliases(relation) + relation._select!(-> { aliases.columns }) + end - 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, child, join_type, tables, chain) - end + protected + attr_reader :join_root - def make_outer_joins(parent, child) - tables = table_aliases_for(parent, child) - join_type = Arel::Nodes::OuterJoin - info = make_constraints parent, child, tables, join_type + private + attr_reader :alias_tracker - [info] + child.children.flat_map { |c| make_outer_joins(child, c) } + 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_left_outer_joins(parent, child) - tables = child.tables - join_type = Arel::Nodes::OuterJoin - info = make_constraints parent, child, tables, join_type - - [info] + child.children.flat_map { |c| make_left_outer_joins(child, c) } + def construct_tables!(join_root) + join_root.each_children do |parent, child| + child.tables = table_aliases_for(parent, child) + end end - def make_inner_joins(parent, child) - tables = child.tables - join_type = Arel::Nodes::InnerJoin - info = 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 - [info] + child.children.flat_map { |c| make_inner_joins(child, c) } + 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) node.reflection.chain.map { |reflection| alias_tracker.aliased_table_for( reflection.table_name, - table_alias_for(reflection, parent, reflection != node.reflection) + table_alias_for(reflection, parent, reflection != node.reflection), + reflection.klass.type_caster ) } 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 << "_join" if join - name + name = reflection.alias_candidate(parent.table_name) + join ? "#{name}_join" : name end def walk(left, right) @@ -223,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) @@ -239,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) - 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| @@ -256,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 @@ -268,24 +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_for(node.base_klass).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] ||= @@ -299,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 f5fcba1236..4583d89cba 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -1,19 +1,19 @@ +# frozen_string_literal: true + require "active_record/associations/join_dependency/join_part" module ActiveRecord module Associations class JoinDependency # :nodoc: class JoinAssociation < JoinPart # :nodoc: - # The reflection of the association represented - attr_reader :reflection - - attr_accessor :tables + attr_reader :reflection, :tables + attr_accessor :table def initialize(reflection, children) super(reflection.klass, children) - @reflection = reflection - @tables = nil + @reflection = reflection + @tables = nil end def match?(other) @@ -21,110 +21,44 @@ module ActiveRecord super && reflection == other.reflection end - JoinInformation = Struct.new :joins, :binds - - def join_constraints(foreign_table, foreign_klass, node, join_type, tables, chain) - joins = [] - binds = [] - 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 - join_keys = reflection.join_keys(klass) - key = join_keys.key - foreign_key = join_keys.foreign_key - - constraint = build_constraint(klass, table, key, foreign_table, foreign_key) - - predicate_builder = PredicateBuilder.new(TableMetadata.new(klass, table)) - scope_chain_items = reflection.scopes.map do |item| - if item.is_a?(Relation) - item - else - ActiveRecord::Relation.create(klass, table, predicate_builder) - .instance_exec(node, &item) - end - end - - klass_scope = - if klass.current_scope - klass.current_scope.clone.tap { |scope| - scope.joins_values = [] - } - else - relation = ActiveRecord::Relation.create( - klass, - table, - predicate_builder, - ) - klass.send(:build_default_scope, relation) - end - scope_chain_items.concat [klass_scope].compact - - rel = scope_chain_items.inject(scope_chain_items.shift) do |left, right| - left.merge right - end + constraint = reflection.build_join_constraint(table, foreign_table) - if rel && !rel.arel.constraints.empty? - binds += rel.bound_attributes - constraint = constraint.and rel.arel.constraints - end + joins << table.create_join(table, table.create_on(constraint), join_type) - if reflection.type - value = foreign_klass.base_class.name - column = klass.columns_hash[reflection.type.to_s] + join_scope = reflection.join_scope(table, foreign_klass) + arel = join_scope.arel(alias_tracker.aliases) - binds << Relation::QueryAttribute.new(column.name, value, klass.type_for_attribute(column.name)) - constraint = constraint.and klass.arel_attribute(reflection.type, table).eq(Arel::Nodes::BindParam.new) + if arel.constraints.any? + joins.concat arel.join_sources + right = joins.last.right + right.expr = right.expr.and(arel.constraints) end - joins << table.create_join(table, table.create_on(constraint), join_type) - # The current table in this iteration becomes the foreign table in the next foreign_table, foreign_klass = table, klass end - JoinInformation.new joins, binds + joins end - # Builds equality condition. - # - # Example: - # - # class Physician < ActiveRecord::Base - # has_many :appointments - # end - # - # If I execute `Physician.joins(:appointments).to_a` then - # klass # => Physician - # table # => #<Arel::Table @name="appointments" ...> - # key # => physician_id - # foreign_table # => #<Arel::Table @name="physicians" ...> - # foreign_key # => id - # - def build_constraint(klass, table, key, foreign_table, foreign_key) - constraint = table[key].eq(foreign_table[foreign_key]) - - if klass.finder_needs_type_condition? - constraint = table.create_and([ - constraint, - klass.send(:type_condition, table) - ]) - end - - constraint + def tables=(tables) + @tables = tables + @table = tables.first end - def table - tables.first - end + def readonly? + return @readonly if defined?(@readonly) - def aliased_table_name - table.table_alias || table.name + @readonly = reflection.scope && reflection.scope_for(base_klass.unscoped).readonly_value end end end diff --git a/activerecord/lib/active_record/associations/join_dependency/join_base.rb b/activerecord/lib/active_record/associations/join_dependency/join_base.rb index fca20514d1..988b4e8fa2 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_base.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_base.rb @@ -1,20 +1,21 @@ +# frozen_string_literal: true + require "active_record/associations/join_dependency/join_part" module ActiveRecord module Associations class JoinDependency # :nodoc: class JoinBase < JoinPart # :nodoc: - def match?(other) - return true if self == other - super && base_klass == other.base_klass - end + attr_reader :table - def table - base_klass.arel_table + def initialize(base_klass, table, children) + super(base_klass, children) + @table = table end - def aliased_table_name - base_klass.table_name + def match?(other) + return true if self == other + super && base_klass == other.base_klass 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 61cec5403a..3ad72a3646 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_part.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_part.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module Associations class JoinDependency # :nodoc: @@ -22,10 +24,6 @@ module ActiveRecord @children = children end - def name - reflection.name - end - def match?(other) self.class == other.class end @@ -35,13 +33,15 @@ module ActiveRecord children.each { |child| child.each(&block) } end - # An Arel::Table for the active_record - def table - raise NotImplementedError + def each_children(&block) + children.each do |child| + yield self, child + child.each_children(&block) + end end - # The alias for the active_record's table - def aliased_table_name + # An Arel::Table for the active_record + def table raise NotImplementedError end @@ -54,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 9f77f38b35..5c2ac5b374 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module Associations # Implements the details of eager loading of Active Record associations. @@ -42,20 +44,10 @@ module ActiveRecord extend ActiveSupport::Autoload eager_autoload do - autoload :Association, "active_record/associations/preloader/association" - autoload :SingularAssociation, "active_record/associations/preloader/singular_association" - autoload :CollectionAssociation, "active_record/associations/preloader/collection_association" - autoload :ThroughAssociation, "active_record/associations/preloader/through_association" - - autoload :HasMany, "active_record/associations/preloader/has_many" - autoload :HasManyThrough, "active_record/associations/preloader/has_many_through" - autoload :HasOne, "active_record/associations/preloader/has_one" - autoload :HasOneThrough, "active_record/associations/preloader/has_one_through" - autoload :BelongsTo, "active_record/associations/preloader/belongs_to" + autoload :Association, "active_record/associations/preloader/association" + autoload :ThroughAssociation, "active_record/associations/preloader/through_association" end - NULL_RELATION = Struct.new(:values, :where_clause, :joins_values).new({}, Relation::WhereClause.empty, []) - # Eager loads the named associations for the given Active Record record(s). # # In this description, 'association name' shall refer to the name passed @@ -91,14 +83,13 @@ module ActiveRecord # { author: :avatar } # [ :books, { author: :avatar } ] def preload(records, associations, preload_scope = nil) - records = Array.wrap(records).compact.uniq - associations = Array.wrap(associations) - preload_scope = preload_scope || NULL_RELATION + records = Array.wrap(records).compact if records.empty? [] else - associations.flat_map { |association| + records.uniq! + Array.wrap(associations).flat_map { |association| preloaders_on association, records, preload_scope } end @@ -107,26 +98,30 @@ 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) + preloaders_for_hash(association, records, scope, polymorphic_parent) when Symbol - preloaders_for_one(association, records, scope) + preloaders_for_one(association, records, scope, polymorphic_parent) when String - preloaders_for_one(association.to_sym, records, scope) + preloaders_for_one(association.to_sym, 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 + loaders = preloaders_for_one parent, records, scope, polymorphic_parent recs = loaders.flat_map(&:preloaded_records).uniq + + reflection = records.first.class._reflect_on_association(parent) + polymorphic_parent = reflection && reflection.options[:polymorphic] + loaders.concat Array.wrap(child).flat_map { |assoc| - preloaders_on assoc, recs, scope + preloaders_on assoc, recs, scope, polymorphic_parent } loaders } @@ -144,21 +139,23 @@ 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| + def preloaders_for_one(association, records, scope, polymorphic_parent) + grouped_records(association, records, polymorphic_parent).flat_map do |reflection, klasses| klasses.map do |rhs_klass, rs| - loader = preloader_for(reflection, rs, rhs_klass).new(rhs_klass, rs, reflection, scope) + loader = preloader_for(reflection, rs).new(rhs_klass, rs, reflection, scope) loader.run self loader end end end - def grouped_records(association, records) + def grouped_records(association, records, polymorphic_parent) h = {} records.each do |record| next unless record + next if polymorphic_parent && !record.class._reflect_on_association(association) assoc = record.association(association) + next unless assoc.klass klasses = h[assoc.reflection] ||= {} (klasses[assoc.klass] ||= []) << record end @@ -166,8 +163,6 @@ module ActiveRecord end class AlreadyLoaded # :nodoc: - attr_reader :owners, :reflection - def initialize(klass, owners, reflection, preload_scope) @owners = owners @reflection = reflection @@ -178,34 +173,24 @@ module ActiveRecord def preloaded_records owners.flat_map { |owner| owner.association(reflection.name).target } end - end - class NullPreloader # :nodoc: - def self.new(klass, owners, reflection, preload_scope); self; end - def self.run(preloader); end - def self.preloaded_records; []; end - def self.owners; []; end + private + attr_reader :owners, :reflection end # Returns a class containing the logic needed to load preload the data - # and attach it to a relation. For example +Preloader::Association+ or - # +Preloader::HasManyThrough+. The class returned implements a `run` method + # and attach it to a relation. The class returned implements a `run` method # that accepts a preloader. - def preloader_for(reflection, owners, rhs_klass) - return NullPreloader unless rhs_klass - + def preloader_for(reflection, owners) if owners.first.association(reflection.name).loaded? return AlreadyLoaded end reflection.check_preloadable! - case reflection.macro - when :has_many - reflection.options[:through] ? HasManyThrough : HasMany - when :has_one - reflection.options[:through] ? HasOneThrough : HasOne - when :belongs_to - BelongsTo + if reflection.options[:through] + ThroughAssociation + else + Association end end end diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 4072d19380..d6f7359055 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -1,8 +1,9 @@ +# frozen_string_literal: true + module ActiveRecord module Associations class Preloader class Association #:nodoc: - attr_reader :owners, :reflection, :preload_scope, :model, :klass attr_reader :preloaded_records def initialize(klass, owners, reflection, preload_scope) @@ -11,86 +12,64 @@ module ActiveRecord @reflection = reflection @preload_scope = preload_scope @model = owners.first && owners.first.class - @scope = nil @preloaded_records = [] end def run(preloader) - preload(preloader) - end - - def preload(preloader) - raise NotImplementedError - end - - def scope - @scope ||= build_scope - end - - def records_for(ids) - scope.where(association_key_name => ids) - end - - def table - klass.arel_table - end - - # The name of the key on the associated records - def association_key_name - raise NotImplementedError - end - - # This is overridden by HABTM as the condition should be on the foreign_key column in - # the join table - def association_key - klass.arel_attribute(association_key_name, table) - end - - # The name of the key on the model which declares the association - def owner_key_name - raise NotImplementedError - end + records = load_records do |record| + owner = owners_by_key[convert_key(record[association_key_name])] + association = owner.association(reflection.name) + association.set_inverse_instance(record) + end - def options - reflection.options + owners.each do |owner| + associate_records_to_owner(owner, records[convert_key(owner[owner_key_name])] || []) + end end private + attr_reader :owners, :reflection, :preload_scope, :model, :klass - def associated_records_by_owner(preloader) - records = load_records do |record| - owner = owners_by_key[convert_key(record[association_key_name])] - association = owner.association(reflection.name) - association.set_inverse_instance(record) - end + # The name of the key on the associated records + def association_key_name + reflection.join_primary_key(klass) + end + + # The name of the key on the model which declares the association + def owner_key_name + reflection.join_foreign_key + end - owners.each_with_object({}) do |owner, result| - result[owner] = records[convert_key(owner[owner_key_name])] || [] + def associate_records_to_owner(owner, records) + association = owner.association(reflection.name) + association.loaded! + if reflection.collection? + association.target.concat(records) + else + association.target = records.first unless records.empty? end end def owner_keys - unless defined?(@owner_keys) - @owner_keys = owners.map do |owner| - owner[owner_key_name] - end - @owner_keys.uniq! - @owner_keys.compact! - end - @owner_keys + @owner_keys ||= owners_by_key.keys end def owners_by_key unless defined?(@owners_by_key) @owners_by_key = owners.each_with_object({}) do |owner, h| - h[convert_key(owner[owner_key_name])] = owner + key = convert_key(owner[owner_key_name]) + h[key] = owner if key end end @owners_by_key end def key_conversion_required? - @key_conversion_required ||= association_key_type != owner_key_type + unless defined?(@key_conversion_required) + @key_conversion_required = (association_key_type != owner_key_type) + end + + @key_conversion_required end def convert_key(key) @@ -102,11 +81,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) @@ -115,54 +94,35 @@ module ActiveRecord # Make several smaller queries if necessary or make one query if the adapter supports it slices = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size) @preloaded_records = slices.flat_map do |slice| - records_for(slice).load(&block) + records_for(slice, &block) end @preloaded_records.group_by do |record| convert_key(record[association_key_name]) end end - def reflection_scope - @reflection_scope ||= reflection.scope_for(klass) + def records_for(ids, &block) + scope.where(association_key_name => ids).load(&block) end - def build_scope - scope = klass.unscoped - - values = reflection_scope.values - preload_values = preload_scope.values - - scope.where_clause = reflection_scope.where_clause + preload_scope.where_clause - scope.references_values = Array(values[:references]) + Array(preload_values[:references]) - - if preload_values[:select] || values[:select] - scope._select!(preload_values[:select] || values[:select]) - end - scope.includes! preload_values[:includes] || values[:includes] - if preload_scope.joins_values.any? - scope.joins!(preload_scope.joins_values) - else - scope.joins!(reflection_scope.joins_values) - end - - if order_values = preload_values[:order] || values[:order] - scope.order!(order_values) - end + def scope + @scope ||= build_scope + end - if preload_values[:reordering] || values[:reordering] - scope.reordering_value = true - end + def reflection_scope + @reflection_scope ||= reflection.scope ? reflection.scope_for(klass.unscoped) : klass.unscoped + end - if preload_values[:readonly] || values[:readonly] - scope.readonly! - end + def build_scope + scope = klass.scope_for_association - if options[:as] - scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name }) + if reflection.type + scope.where!(reflection.type => model.polymorphic_name) end - scope.unscope_values = Array(values[:unscope]) + Array(preload_values[:unscope]) - klass.default_scoped.merge(scope) + scope.merge!(reflection_scope) if reflection.scope + scope.merge!(preload_scope) if preload_scope + scope end end end diff --git a/activerecord/lib/active_record/associations/preloader/belongs_to.rb b/activerecord/lib/active_record/associations/preloader/belongs_to.rb deleted file mode 100644 index 38e231826c..0000000000 --- a/activerecord/lib/active_record/associations/preloader/belongs_to.rb +++ /dev/null @@ -1,15 +0,0 @@ -module ActiveRecord - module Associations - class Preloader - class BelongsTo < SingularAssociation #:nodoc: - def association_key_name - reflection.options[:primary_key] || klass && klass.primary_key - end - - def owner_key_name - reflection.foreign_key - end - end - end - end -end diff --git a/activerecord/lib/active_record/associations/preloader/collection_association.rb b/activerecord/lib/active_record/associations/preloader/collection_association.rb deleted file mode 100644 index 26690bf16d..0000000000 --- a/activerecord/lib/active_record/associations/preloader/collection_association.rb +++ /dev/null @@ -1,17 +0,0 @@ -module ActiveRecord - module Associations - class Preloader - class CollectionAssociation < Association #:nodoc: - private - - def preload(preloader) - associated_records_by_owner(preloader).each do |owner, records| - association = owner.association(reflection.name) - association.loaded! - association.target.concat(records) - end - end - end - end - end -end diff --git a/activerecord/lib/active_record/associations/preloader/has_many.rb b/activerecord/lib/active_record/associations/preloader/has_many.rb deleted file mode 100644 index 20df1cc19a..0000000000 --- a/activerecord/lib/active_record/associations/preloader/has_many.rb +++ /dev/null @@ -1,15 +0,0 @@ -module ActiveRecord - module Associations - class Preloader - class HasMany < CollectionAssociation #:nodoc: - def association_key_name - reflection.foreign_key - end - - def owner_key_name - reflection.active_record_primary_key - end - end - end - end -end diff --git a/activerecord/lib/active_record/associations/preloader/has_many_through.rb b/activerecord/lib/active_record/associations/preloader/has_many_through.rb deleted file mode 100644 index 2029871f39..0000000000 --- a/activerecord/lib/active_record/associations/preloader/has_many_through.rb +++ /dev/null @@ -1,19 +0,0 @@ -module ActiveRecord - module Associations - class Preloader - class HasManyThrough < CollectionAssociation #:nodoc: - include ThroughAssociation - - def associated_records_by_owner(preloader) - records_by_owner = super - - if reflection_scope.distinct_value - records_by_owner.each_value(&:uniq!) - end - - records_by_owner - end - end - end - end -end diff --git a/activerecord/lib/active_record/associations/preloader/has_one.rb b/activerecord/lib/active_record/associations/preloader/has_one.rb deleted file mode 100644 index c4add621ca..0000000000 --- a/activerecord/lib/active_record/associations/preloader/has_one.rb +++ /dev/null @@ -1,15 +0,0 @@ -module ActiveRecord - module Associations - class Preloader - class HasOne < SingularAssociation #:nodoc: - def association_key_name - reflection.foreign_key - end - - def owner_key_name - reflection.active_record_primary_key - end - end - end - end -end diff --git a/activerecord/lib/active_record/associations/preloader/has_one_through.rb b/activerecord/lib/active_record/associations/preloader/has_one_through.rb deleted file mode 100644 index f063f85574..0000000000 --- a/activerecord/lib/active_record/associations/preloader/has_one_through.rb +++ /dev/null @@ -1,9 +0,0 @@ -module ActiveRecord - module Associations - class Preloader - class HasOneThrough < SingularAssociation #:nodoc: - include ThroughAssociation - end - end - end -end diff --git a/activerecord/lib/active_record/associations/preloader/singular_association.rb b/activerecord/lib/active_record/associations/preloader/singular_association.rb deleted file mode 100644 index 5c5828262e..0000000000 --- a/activerecord/lib/active_record/associations/preloader/singular_association.rb +++ /dev/null @@ -1,18 +0,0 @@ -module ActiveRecord - module Associations - class Preloader - class SingularAssociation < Association #:nodoc: - private - - def preload(preloader) - associated_records_by_owner(preloader).each do |owner, associated_records| - record = associated_records.first - - association = owner.association(reflection.name) - association.target = record - end - end - end - end - end -end diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index 9d44a02021..a6b7ab80a2 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -1,106 +1,105 @@ +# frozen_string_literal: true + module ActiveRecord module Associations class Preloader - module ThroughAssociation #:nodoc: - def through_reflection - reflection.through_reflection - end - - def source_reflection - reflection.source_reflection - end - - def associated_records_by_owner(preloader) - preloader.preload(owners, - through_reflection.name, - through_scope) - - through_records = owners.map do |owner| - association = owner.association through_reflection.name - - center = target_records_from_association(association) - [owner, Array(center)] - end - - reset_association owners, through_reflection.name - - middle_records = through_records.flat_map { |(_, rec)| rec } - - preloaders = preloader.preload(middle_records, - source_reflection.name, - reflection_scope) - + class ThroughAssociation < Association # :nodoc: + def run(preloader) + already_loaded = owners.first.association(through_reflection.name).loaded? + through_scope = through_scope() + reflection_scope = target_reflection_scope + through_preloaders = preloader.preload(owners, through_reflection.name, through_scope) + middle_records = through_preloaders.flat_map(&:preloaded_records) + preloaders = preloader.preload(middle_records, source_reflection.name, reflection_scope) @preloaded_records = preloaders.flat_map(&:preloaded_records) - middle_to_pl = preloaders.each_with_object({}) do |pl, h| - pl.owners.each { |middle| - h[middle] = pl - } - end - - through_records.each_with_object({}) do |(lhs, center), records_by_owner| - pl_to_middle = center.group_by { |record| middle_to_pl[record] } - - records_by_owner[lhs] = pl_to_middle.flat_map do |pl, middles| - rhs_records = middles.flat_map { |r| - association = r.association source_reflection.name - - target_records_from_association(association) - }.compact - - # Respect the order on `reflection_scope` if it exists, else use the natural order. - if reflection_scope.values[:order].present? - @id_map ||= id_to_index_map @preloaded_records - rhs_records.sort_by { |rhs| @id_map[rhs] } - else - rhs_records + owners.each do |owner| + through_records = Array(owner.association(through_reflection.name).target) + if already_loaded + if source_type = reflection.options[:source_type] + through_records = through_records.select do |record| + record[reflection.foreign_type] == source_type + end end + else + owner.association(through_reflection.name).reset if through_scope + end + result = through_records.flat_map do |record| + association = record.association(source_reflection.name) + target = association.target + association.reset if preload_scope + target + end + result.compact! + if reflection_scope + result.sort_by! { |rhs| preload_index[rhs] } if reflection_scope.order_values.any? + result.uniq! if reflection_scope.distinct_value end + associate_records_to_owner(owner, result) end end private - - def id_to_index_map(ids) - id_map = {} - ids.each_with_index { |id, index| id_map[id] = index } - id_map + def through_reflection + reflection.through_reflection end - def reset_association(owners, association_name) - should_reset = (through_scope != through_reflection.klass.unscoped) || - (reflection.options[:source_type] && through_reflection.collection?) + def source_reflection + reflection.source_reflection + end - # Don't cache the association - we would only be caching a subset - if should_reset - owners.each { |owner| - owner.association(association_name).reset - } + def preload_index + @preload_index ||= @preloaded_records.each_with_object({}).with_index do |(id, result), index| + result[id] = index end end def through_scope scope = through_reflection.klass.unscoped + options = reflection.options if options[:source_type] scope.where! reflection.foreign_type => options[:source_type] - else - unless reflection_scope.where_clause.empty? - scope.includes_values = Array(reflection_scope.values[:includes] || options[:source]) - scope.where_clause = reflection_scope.where_clause + elsif !reflection_scope.where_clause.empty? + scope.where_clause = reflection_scope.where_clause + values = reflection_scope.values + + if includes = values[:includes] + scope.includes!(source_reflection.name => includes) + else + scope.includes!(source_reflection.name) end - scope.references! reflection_scope.values[:references] - if scope.eager_loading? && order_values = reflection_scope.values[:order] + if values[:references] && !values[:references].empty? + scope.references!(values[:references]) + else + scope.references!(source_reflection.table_name) + end + + if joins = values[:joins] + scope.joins!(source_reflection.name => joins) + end + + if left_outer_joins = values[:left_outer_joins] + scope.left_outer_joins!(source_reflection.name => left_outer_joins) + end + + if scope.eager_loading? && order_values = values[:order] scope = scope.order(order_values) end end - scope + scope unless scope.empty_scope? end - def target_records_from_association(association) - association.loaded? ? association.target : association.reader + def target_reflection_scope + if preload_scope + reflection_scope.merge(preload_scope) + elsif reflection.scope + reflection_scope + else + nil + end end end end diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index 91580a28d0..cfab16a745 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveRecord module Associations class SingularAssociation < Association #:nodoc: @@ -15,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 @@ -30,24 +31,22 @@ module ActiveRecord end private - - def create_scope - scope.scope_for_create.stringify_keys.except(klass.primary_key) + def scope_for_create + super.except!(klass.primary_key) end def find_target - return scope.take if skip_statement_cache? + scope = self.scope + return scope.take if skip_statement_cache?(scope) conn = klass.connection - sc = reflection.association_scope_cache(conn, owner) do - StatementCache.create(conn) { |params| - as = AssociationScope.create { params.bind } - target_scope.merge(as.scope(self, conn)).limit(1) - } + 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, klass, conn) do |record| + sc.execute(binds, conn) do |record| set_inverse_instance record end.first rescue ::RangeError @@ -62,9 +61,8 @@ module ActiveRecord replace(record) end - def _create_record(attributes, raise_error = false) - 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 6b87993ba3..15e6565e69 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -1,10 +1,27 @@ +# frozen_string_literal: true + module ActiveRecord - # = Active Record Through Association 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: # @@ -13,7 +30,7 @@ module ActiveRecord def target_scope scope = super reflection.chain.drop(1).each do |reflection| - relation = reflection.klass.all + relation = reflection.klass.scope_for_association scope.merge!( relation.except(:select, :create_with, :includes, :preload, :joins, :eager_load) ) @@ -36,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 @@ -99,7 +114,7 @@ module ActiveRecord attributes[inverse.foreign_key] = target.id end - super(attributes) + super end end end |