diff options
Diffstat (limited to 'activerecord/lib/active_record/associations')
27 files changed, 767 insertions, 834 deletions
diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb index 021bc32237..3963008a76 100644 --- a/activerecord/lib/active_record/associations/alias_tracker.rb +++ b/activerecord/lib/active_record/associations/alias_tracker.rb @@ -1,4 +1,4 @@ -require 'active_support/core_ext/string/conversions' +require "active_support/core_ext/string/conversions" module ActiveRecord module Associations diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 62e867a353..84d0493a60 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -1,4 +1,4 @@ -require 'active_support/core_ext/array/wrap' +require "active_support/core_ext/array/wrap" module ActiveRecord module Associations @@ -19,7 +19,7 @@ module ActiveRecord attr_reader :owner, :target, :reflection attr_accessor :inversed - delegate :options, :to => :reflection + delegate :options, to: :reflection def initialize(owner, reflection) reflection.check_validity! @@ -112,6 +112,15 @@ module ActiveRecord 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 + end + end + # Returns the class of the target. belongs_to polymorphic overrides this to look at the # polymorphic_type field on the owner. def klass @@ -166,13 +175,21 @@ module ActiveRecord def initialize_attributes(record, except_from_scope_attributes = nil) #:nodoc: except_from_scope_attributes ||= {} skip_assign = [reflection.foreign_key, reflection.type].compact - assigned_keys = record.changed + 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) set_inverse_instance(record) end + def create(attributes = {}, &block) + _create_record(attributes, &block) + end + + def create!(attributes = {}, &block) + _create_record(attributes, true, &block) + end + private def find_target? @@ -246,7 +263,7 @@ module ActiveRecord # so that when stale_state is different from the value stored on the last find_target, # the target is stale. # - # This is only relevant to certain associations, which is why it returns nil by default. + # This is only relevant to certain associations, which is why it returns +nil+ by default. def stale_state end diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 15844de0bc..c6d204d3c2 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -49,117 +49,119 @@ module ActiveRecord binds end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected - attr_reader :value_transformation + attr_reader :value_transformation private - def join(table, constraint) - table.create_join(table, table.create_on(constraint), join_type) - end + def join(table, constraint) + table.create_join(table, table.create_on(constraint), join_type) + end + + def last_chain_scope(scope, table, reflection, owner, association_klass) + join_keys = reflection.join_keys(association_klass) + key = join_keys.key + foreign_key = join_keys.foreign_key - def last_chain_scope(scope, table, reflection, owner, association_klass) - join_keys = reflection.join_keys(association_klass) - key = join_keys.key - foreign_key = join_keys.foreign_key + value = transform_value(owner[foreign_key]) + scope = scope.where(table.name => { key => value }) - value = transform_value(owner[foreign_key]) - scope = scope.where(table.name => { key => value }) + if reflection.type + polymorphic_type = transform_value(owner.class.base_class.name) + scope = scope.where(table.name => { reflection.type => polymorphic_type }) + end - if reflection.type - polymorphic_type = transform_value(owner.class.base_class.name) - scope = scope.where(table.name => { reflection.type => polymorphic_type }) + scope end - scope - end + def transform_value(value) + value_transformation.call(value) + end - def transform_value(value) - 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) + key = join_keys.key + foreign_key = join_keys.foreign_key - def next_chain_scope(scope, table, reflection, association_klass, foreign_table, next_reflection) - join_keys = reflection.join_keys(association_klass) - key = join_keys.key - foreign_key = join_keys.foreign_key + constraint = table[key].eq(foreign_table[foreign_key]) - 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 }) + end - if reflection.type - value = transform_value(next_reflection.klass.base_class.name) - scope = scope.where(table.name => { reflection.type => value }) + scope = scope.joins(join(foreign_table, constraint)) end - scope = scope.joins(join(foreign_table, constraint)) - end + class ReflectionProxy < SimpleDelegator # :nodoc: + attr_accessor :next + attr_reader :alias_name - class ReflectionProxy < SimpleDelegator # :nodoc: - attr_accessor :next - attr_reader :alias_name + def initialize(reflection, alias_name) + super(reflection) + @alias_name = alias_name + end - def initialize(reflection, alias_name) - super(reflection) - @alias_name = alias_name + def all_includes; nil; end end - def all_includes; nil; end - end - - def get_chain(reflection, association, tracker) - name = reflection.name - runtime_reflection = Reflection::RuntimeReflection.new(reflection, association) - previous_reflection = runtime_reflection - 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 + def get_chain(reflection, association, tracker) + name = reflection.name + runtime_reflection = Reflection::RuntimeReflection.new(reflection, association) + previous_reflection = runtime_reflection + 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 + end + [runtime_reflection, previous_reflection] end - [runtime_reflection, previous_reflection] - 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) + 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 + reflection = chain_head + while reflection + table = reflection.alias_name - unless reflection == chain_tail - next_reflection = reflection.next - foreign_table = next_reflection.alias_name - scope = next_chain_scope(scope, table, reflection, association_klass, foreign_table, next_reflection) - end + unless reflection == chain_tail + next_reflection = reflection.next + foreign_table = next_reflection.alias_name + scope = next_chain_scope(scope, table, reflection, association_klass, foreign_table, next_reflection) + end - # 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, scope_chain_item, owner) + # 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, scope_chain_item, owner) - if scope_chain_item == refl.scope - scope.merge! item.except(:where, :includes) - end + if scope_chain_item == refl.scope + scope.merge! item.except(:where, :includes) + end + + reflection.all_includes do + scope.includes! item.includes_values + end - reflection.all_includes do - scope.includes! item.includes_values + scope.unscope!(*item.unscope_values) + scope.where_clause += item.where_clause + scope.order_values |= item.order_values end - scope.unscope!(*item.unscope_values) - scope.where_clause += item.where_clause - scope.order_values |= item.order_values + reflection = reflection.next end - reflection = reflection.next + scope end - scope - end - - def eval_scope(klass, scope, owner) - klass.unscoped.instance_exec(owner, &scope) - end + def eval_scope(klass, scope, owner) + klass.unscoped.instance_exec(owner, &scope) + end 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 24997370b2..64b2311911 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -2,7 +2,6 @@ module ActiveRecord # = Active Record Belongs To Association module Associations class BelongsToAssociation < SingularAssociation #:nodoc: - def handle_dependency target.send(options[:dependent]) if load_target end diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index 3121e70a04..a1609ab0fb 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -35,17 +35,17 @@ module ActiveRecord::Associations::Builder # :nodoc: @_after_create_counter_called = false elsif (@_after_replace_counter_called ||= false) @_after_replace_counter_called = false - elsif attribute_changed?(foreign_key) && !new_record? + elsif saved_change_to_attribute?(foreign_key) && !new_record? if reflection.polymorphic? - model = attribute(reflection.foreign_type).try(:constantize) - model_was = attribute_was(reflection.foreign_type).try(:constantize) + model = attribute_in_database(reflection.foreign_type).try(:constantize) + model_was = attribute_before_last_save(reflection.foreign_type).try(:constantize) else model = reflection.klass model_was = reflection.klass end - foreign_key_was = attribute_was foreign_key - foreign_key = attribute foreign_key + 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) @@ -70,14 +70,16 @@ module ActiveRecord::Associations::Builder # :nodoc: klass.attr_readonly cache_column if klass && klass.respond_to?(:attr_readonly) end - def self.touch_record(o, foreign_key, name, touch, touch_method) # :nodoc: - old_foreign_id = o.changed_attributes[foreign_key] + def self.touch_record(o, changes, foreign_key, name, touch, touch_method) # :nodoc: + old_foreign_id = changes[foreign_key] && changes[foreign_key].first if old_foreign_id association = o.association(name) reflection = association.reflection if reflection.polymorphic? - klass = o.public_send("#{reflection.foreign_type}_was").constantize + foreign_type = reflection.foreign_type + klass = changes[foreign_type] && changes[foreign_type].first || o.public_send(foreign_type) + klass = klass.constantize else klass = association.klass end @@ -107,13 +109,13 @@ module ActiveRecord::Associations::Builder # :nodoc: n = reflection.name touch = reflection.options[:touch] - callback = lambda { |record| - BelongsTo.touch_record(record, foreign_key, n, touch, belongs_to_touch_method) - } + callback = lambda { |changes_method| lambda { |record| + BelongsTo.touch_record(record, record.send(changes_method), foreign_key, n, touch, belongs_to_touch_method) + }} - model.after_save callback, if: :changed? - model.after_touch callback - model.after_destroy callback + model.after_save callback.(:saved_changes), if: :saved_changes? + model.after_touch callback.(:changes_to_save) + model.after_destroy callback.(:changes_to_save) 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 f25bd7ca9f..edeb6491bd 100644 --- a/activerecord/lib/active_record/associations/builder/collection_association.rb +++ b/activerecord/lib/active_record/associations/builder/collection_association.rb @@ -1,10 +1,9 @@ # This class is inherited by the has_many and has_many_and_belongs_to_many association classes -require 'active_record/associations' +require "active_record/associations" module ActiveRecord::Associations::Builder # :nodoc: class CollectionAssociation < Association #:nodoc: - CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove] def self.valid_options(options) 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 5fbd79d118..6b71826431 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 @@ -16,9 +16,9 @@ module ActiveRecord::Associations::Builder # :nodoc: private - def klass - @lhs_class.send(:compute_type, @rhs_class_name) - end + def klass + @lhs_class.send(:compute_type, @rhs_class_name) + end end def self.build(lhs_class, name, options) @@ -28,7 +28,7 @@ module ActiveRecord::Associations::Builder # :nodoc: class_name = options.fetch(:class_name) { name.to_s.camelize.singularize } - KnownClass.new lhs_class, class_name + KnownClass.new lhs_class, class_name.to_s end end end @@ -76,9 +76,11 @@ module ActiveRecord::Associations::Builder # :nodoc: left_model.retrieve_connection end - def self.primary_key - false - end + private + + def self.suppress_composite_primary_key(pk) + pk unless pk.is_a?(Array) + end } join_model.name = "HABTM_#{association_name.to_s.camelize}" @@ -92,7 +94,7 @@ module ActiveRecord::Associations::Builder # :nodoc: def middle_reflection(join_model) middle_name = [lhs_model.name.downcase.pluralize, - association_name].join('_'.freeze).gsub('::'.freeze, '_'.freeze).to_sym + association_name].join("_".freeze).gsub("::".freeze, "_".freeze).to_sym middle_options = middle_options join_model HasMany.create_reflection(lhs_model, @@ -103,29 +105,29 @@ module ActiveRecord::Associations::Builder # :nodoc: private - def middle_options(join_model) - middle_options = {} - middle_options[:class_name] = "#{lhs_model.name}::#{join_model.name}" - middle_options[:source] = join_model.left_reflection.name - if options.key? :foreign_key - middle_options[:foreign_key] = options[:foreign_key] + def middle_options(join_model) + middle_options = {} + middle_options[:class_name] = "#{lhs_model.name}::#{join_model.name}" + middle_options[:source] = join_model.left_reflection.name + if options.key? :foreign_key + middle_options[:foreign_key] = options[:foreign_key] + end + middle_options end - middle_options - end - def belongs_to_options(options) - rhs_options = {} + def belongs_to_options(options) + rhs_options = {} - if options.key? :class_name - rhs_options[:foreign_key] = options[:class_name].to_s.foreign_key - rhs_options[:class_name] = options[:class_name] - end + if options.key? :class_name + rhs_options[:foreign_key] = options[:class_name].to_s.foreign_key + rhs_options[:class_name] = options[:class_name] + end - if options.key? :association_foreign_key - rhs_options[:foreign_key] = options[:association_foreign_key] - end + if options.key? :association_foreign_key + rhs_options[:foreign_key] = options[:association_foreign_key] + end - rhs_options - end + rhs_options + end end end diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb index bb96202a22..7732b63af6 100644 --- a/activerecord/lib/active_record/associations/builder/singular_association.rb +++ b/activerecord/lib/active_record/associations/builder/singular_association.rb @@ -8,7 +8,16 @@ module ActiveRecord::Associations::Builder # :nodoc: def self.define_accessors(model, reflection) super - define_constructors(model.generated_association_methods, reflection.name) if reflection.constructable? + mixin = model.generated_association_methods + name = reflection.name + + define_constructors(mixin, name) if reflection.constructable? + + mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def reload_#{name} + association(:#{name}).force_reload_reader + end + CODE end # Defines the (build|create)_association methods for belongs_to or has_one association diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 0eaa0a4f36..0437a79b84 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -24,18 +24,9 @@ module ActiveRecord # If you need to work on all current children, new and existing records, # +load_target+ and the +loaded+ flag are your friends. class CollectionAssociation < Association #:nodoc: - # Implements the reader method, e.g. foo.items for Foo.has_many :items - def reader(force_reload = false) - if force_reload - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing an argument to force an association to reload is now - deprecated and will be removed in Rails 5.1. Please call `reload` - on the result collection proxy instead. - MSG - - klass.uncached { reload } - elsif stale_target? + def reader + if stale_target? reload end @@ -56,9 +47,7 @@ module ActiveRecord # Implements the ids reader method, e.g. foo.item_ids for Foo.has_many :items def ids_reader if loaded? - load_target.map do |record| - record.send(reflection.association_primary_key) - end + target.pluck(reflection.association_primary_key) else @association_ids ||= ( column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}" @@ -69,13 +58,17 @@ module ActiveRecord # Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items def ids_writer(ids) - pk_type = reflection.primary_key_type + pk_type = reflection.association_primary_key_type 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) - end.values_at(*ids) - replace(records) + 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) + else + replace(records) + end end def reset @@ -83,14 +76,6 @@ module ActiveRecord @target = [] end - def select(*fields) - if block_given? - load_target.select.each { |e| yield e } - else - scope.select(*fields) - end - end - def find(*args) if block_given? load_target.find(*args) { |*block_args| yield(*block_args) } @@ -112,52 +97,6 @@ module ActiveRecord end end - def first(*args) - first_nth_or_last(:first, *args) - end - - def second(*args) - first_nth_or_last(:second, *args) - end - - def third(*args) - first_nth_or_last(:third, *args) - end - - def fourth(*args) - first_nth_or_last(:fourth, *args) - end - - def fifth(*args) - first_nth_or_last(:fifth, *args) - end - - def forty_two(*args) - first_nth_or_last(:forty_two, *args) - end - - def third_to_last(*args) - first_nth_or_last(:third_to_last, *args) - end - - def second_to_last(*args) - first_nth_or_last(:second_to_last, *args) - end - - def last(*args) - first_nth_or_last(:last, *args) - end - - def take(n = nil) - if loaded? - n ? target.take(n) : target.first - else - scope.take(n).tap do |record| - set_inverse_instance record if record.is_a? ActiveRecord::Base - end - end - end - def build(attributes = {}, &block) if attributes.is_a?(Array) attributes.collect { |attr| build(attr, &block) } @@ -168,14 +107,6 @@ module ActiveRecord end end - def create(attributes = {}, &block) - _create_record(attributes, &block) - end - - def create!(attributes = {}, &block) - _create_record(attributes, true, &block) - end - # Add +records+ to this association. Returns +self+ so method calls may # be chained. Since << flattens its argument list and inserts each record, # +push+ and +concat+ behave identically. @@ -223,12 +154,12 @@ module ActiveRecord end dependent = if dependent - dependent - elsif options[:dependent] == :destroy - :delete_all - else - options[:dependent] - end + dependent + elsif options[:dependent] == :destroy + :delete_all + else + options[:dependent] + end delete_or_nullify_all_records(dependent).tap do reset @@ -246,31 +177,6 @@ module ActiveRecord end end - # Returns the number of records. If no arguments are given, it counts all - # columns using SQL. If one argument is given, it counts only the passed - # column using SQL. If a block is given, it counts the number of records - # yielding a true value. - def count(column_name = nil) - return super if block_given? - relation = scope - if association_scope.distinct_value - # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL. - column_name ||= reflection.klass.primary_key - relation = relation.distinct - end - - value = relation.count(column_name) - - limit = options[:limit] - offset = options[:offset] - - if limit || offset - [ [value - offset.to_i, 0].max, limit.to_i ].min - else - value - end - end - # Removes +records+ from this association calling +before_remove+ and # +after_remove+ callbacks. # @@ -280,11 +186,8 @@ module ActiveRecord # +delete_records+. They are in any case removed from the collection. def delete(*records) return if records.empty? - _options = records.extract_options! - dependent = _options[:dependent] || options[:dependent] - records = find(records) if records.any? { |record| record.kind_of?(Integer) || record.kind_of?(String) } - delete_or_destroy(records, dependent) + delete_or_destroy(records, options[:dependent]) end # Deletes the +records+ and removes them from this association calling @@ -310,14 +213,10 @@ module ActiveRecord # +count_records+, which is a method descendants have to provide. def size if !find_target? || loaded? - if association_scope.distinct_value - target.uniq.size - else - target.size - end - elsif !loaded? && !association_scope.group_values.empty? + target.size + elsif !association_scope.group_values.empty? load_target.size - elsif !loaded? && !association_scope.distinct_value && target.is_a?(Array) + elsif !association_scope.distinct_value && target.is_a?(Array) unsaved_records = target.select(&:new_record?) unsaved_records.size + count_records else @@ -325,15 +224,6 @@ module ActiveRecord end end - # Returns the size of the collection calling +size+ on the target. - # - # If the collection has been already loaded +length+ and +size+ are - # equivalent. If not and you are going to need the records anyway this - # method will take one less query. Otherwise +size+ is more efficient. - def length - load_target.size - end - # Returns true if the collection is empty. # # If the collection has been loaded @@ -350,36 +240,6 @@ module ActiveRecord end end - # Returns true if the collections is not empty. - # If block given, loads all records and checks for one or more matches. - # Otherwise, equivalent to +!collection.empty?+. - def any? - if block_given? - load_target.any? { |*block_args| yield(*block_args) } - else - !empty? - end - end - - # Returns true if the collection has more than 1 record. - # If block given, loads all records and checks for two or more matches. - # Otherwise, equivalent to +collection.size > 1+. - def many? - if block_given? - load_target.many? { |*block_args| yield(*block_args) } - else - size > 1 - end - end - - def distinct - seen = {} - load_target.find_all do |record| - seen[record.id] = true unless seen.key?(record.id) - end - end - alias uniq distinct - # Replace this collection with +other_array+. This will perform a diff # and delete/add only records that have changed. def replace(other_array) @@ -429,19 +289,28 @@ module ActiveRecord def replace_on_target(record, index, skip_callbacks) callback(:before_add, record) unless skip_callbacks - was_loaded = loaded? - yield(record) if block_given? + begin + if index + record_was = target[index] + target[index] = record + else + target << record + end + + set_inverse_instance(record) - unless !was_loaded && loaded? + yield(record) if block_given? + rescue if index - @target[index] = record + target[index] = record_was else - @target << record + target.delete(record) end + + raise end callback(:after_add, record) unless skip_callbacks - set_inverse_instance(record) record end @@ -456,26 +325,29 @@ module ActiveRecord owner.new_record? && !foreign_key_present? end - private - def get_records - return scope.to_a if skip_statement_cache? - - 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) - } - end - - binds = AssociationScope.get_bind_values(owner, reflection.chain) - sc.execute binds, klass, klass.connection + def find_from_target? + loaded? || + owner.new_record? || + target.any? { |record| record.new_record? || record.changed? } end + private + def find_target - records = get_records - records.each { |record| set_inverse_instance(record) } - records + return scope.to_a if skip_statement_cache? + + 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) + } + end + + binds = AssociationScope.get_bind_values(owner, reflection.chain) + sc.execute(binds, klass, conn) do |record| + set_inverse_instance(record) + end end # We have some records loaded from the database (persisted) and some that are @@ -495,7 +367,7 @@ module ActiveRecord persisted.map! do |record| if mem_record = memory.delete(record) - ((record.attribute_names & mem_record.attribute_names) - mem_record.changes.keys).each do |name| + ((record.attribute_names & mem_record.attribute_names) - mem_record.changed_attribute_names_to_save).each do |name| mem_record[name] = record[name] end @@ -555,8 +427,9 @@ module ActiveRecord records.each { |record| callback(:after_remove, record) } end - # Delete the given records from the association, using one of the methods :destroy, - # :delete_all or :nullify (or nil, in which case a default is used). + # Delete the given records from the association, + # using one of the methods +:destroy+, +:delete_all+ + # or +:nullify+ (or +nil+, in which case a default is used). def delete_records(records, method) raise NotImplementedError end @@ -605,25 +478,6 @@ module ActiveRecord owner.class.send(full_callback_name) end - # Should we deal with assoc.first or assoc.last by issuing an independent query to - # the database, or by getting the target, and then taking the first/last item from that? - # - # If the args is just a non-empty options hash, go to the database. - # - # Otherwise, go to the database only if none of the following are true: - # * target already loaded - # * owner is new record - # * target contains new or changed record(s) - def fetch_first_nth_or_last_using_find?(args) - if args.first.is_a?(Hash) - true - else - !(loaded? || - owner.new_record? || - target.any? { |record| record.new_record? || record.changed? }) - end - end - def include_in_memory?(record) if reflection.is_a?(ActiveRecord::Reflection::ThroughReflection) assoc = owner.association(reflection.through_reflection.name) @@ -650,16 +504,6 @@ module ActiveRecord load_target.select { |r| ids.include?(r.id.to_s) } end end - - # Fetches the first/last using SQL if possible, otherwise from the target array. - def first_nth_or_last(type, *args) - args.shift if args.first.is_a?(Hash) && args.first.empty? - - collection = fetch_first_nth_or_last_using_find?(args) ? scope : load_target - collection.send(type, *args).tap do |record| - set_inverse_instance record if record.is_a? ActiveRecord::Base - end - end end end end diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 5d1e7ffb73..0d84805b4d 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -28,8 +28,7 @@ module ActiveRecord # is computed directly through SQL and does not trigger by itself the # instantiation of the actual post records. class CollectionProxy < Relation - delegate(*(ActiveRecord::Calculations.public_instance_methods - [:count]), to: :scope) - delegate :find_nth, to: :scope + delegate :exists?, :update_all, :arel, to: :scope def initialize(klass, association) #:nodoc: @association = association @@ -54,6 +53,12 @@ module ActiveRecord @association.loaded? end + ## + # :method: select + # + # :call-seq: + # select(*fields, &block) + # # Works in two ways. # # *First:* Specify a subset of fields to be selected from the result set. @@ -101,15 +106,6 @@ module ActiveRecord # # #<Pet id: 2, name: "Spook", person_id: 1>, # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> # # ] - # - # person.pets.select(:name) { |pet| pet.name =~ /oo/ } - # # => [ - # # #<Pet id: 2, name: "Spook">, - # # #<Pet id: 3, name: "Choo-Choo"> - # # ] - def select(*fields, &block) - @association.select(*fields, &block) - end # Finds an object in the collection responding to the +id+. Uses the same # rules as ActiveRecord::Base.find. Returns ActiveRecord::RecordNotFound @@ -141,6 +137,12 @@ module ActiveRecord @association.find(*args, &block) end + ## + # :method: first + # + # :call-seq: + # first(limit = nil) + # # Returns the first record, or the first +n+ records, from the collection. # If the collection is empty, the first form returns +nil+, and the second # form returns an empty array. @@ -167,45 +169,63 @@ module ActiveRecord # another_person_without.pets # => [] # another_person_without.pets.first # => nil # another_person_without.pets.first(3) # => [] - def first(*args) - @association.first(*args) - end + ## + # :method: second + # + # :call-seq: + # second() + # # Same as #first except returns only the second record. - def second(*args) - @association.second(*args) - end + ## + # :method: third + # + # :call-seq: + # third() + # # Same as #first except returns only the third record. - def third(*args) - @association.third(*args) - end + ## + # :method: fourth + # + # :call-seq: + # fourth() + # # Same as #first except returns only the fourth record. - def fourth(*args) - @association.fourth(*args) - end + ## + # :method: fifth + # + # :call-seq: + # fifth() + # # Same as #first except returns only the fifth record. - def fifth(*args) - @association.fifth(*args) - end + ## + # :method: forty_two + # + # :call-seq: + # forty_two() + # # Same as #first except returns only the forty second record. # Also known as accessing "the reddit". - def forty_two(*args) - @association.forty_two(*args) - end + ## + # :method: third_to_last + # + # :call-seq: + # third_to_last() + # # Same as #first except returns only the third-to-last record. - def third_to_last(*args) - @association.third_to_last(*args) - end + ## + # :method: second_to_last + # + # :call-seq: + # second_to_last() + # # Same as #first except returns only the second-to-last record. - def second_to_last(*args) - @association.second_to_last(*args) - end # Returns the last record, or the last +n+ records, from the collection. # If the collection is empty, the first form returns +nil+, and the second @@ -233,8 +253,9 @@ module ActiveRecord # another_person_without.pets # => [] # another_person_without.pets.last # => nil # another_person_without.pets.last(3) # => [] - def last(*args) - @association.last(*args) + def last(limit = nil) + load_target if find_from_target? + super end # Gives a record (or N records if a parameter is supplied) from the collection @@ -262,8 +283,9 @@ module ActiveRecord # another_person_without.pets # => [] # another_person_without.pets.take # => nil # another_person_without.pets.take(2) # => [] - def take(n = nil) - @association.take(n) + def take(limit = nil) + load_target if find_from_target? + super end # Returns a new object of the collection type that has been instantiated @@ -696,6 +718,12 @@ module ActiveRecord @association.destroy(*records) end + ## + # :method: distinct + # + # :call-seq: + # distinct(value = true) + # # Specifies whether the records should be unique or not. # # class Person < ActiveRecord::Base @@ -710,11 +738,32 @@ module ActiveRecord # # person.pets.select(:name).distinct # # => [#<Pet name: "Fancy-Fancy">] - def distinct - @association.distinct + # + # person.pets.select(:name).distinct.distinct(false) + # # => [ + # # #<Pet name: "Fancy-Fancy">, + # # #<Pet name: "Fancy-Fancy"> + # # ] + + #-- + def uniq + load_target.uniq end - alias uniq distinct + def calculate(operation, column_name) + null_scope? ? scope.calculate(operation, column_name) : super + end + + def pluck(*column_names) + null_scope? ? scope.pluck(*column_names) : super + end + + ## + # :method: count + # + # :call-seq: + # count(column_name = nil, &block) + # # Count all records. # # class Person < ActiveRecord::Base @@ -734,9 +783,6 @@ module ActiveRecord # perform the count using Ruby. # # person.pets.count { |pet| pet.name.include?('-') } # => 2 - def count(column_name = nil, &block) - @association.count(column_name, &block) - end # Returns the size of the collection. If the collection hasn't been loaded, # it executes a <tt>SELECT COUNT(*)</tt> query. Else it calls <tt>collection.size</tt>. @@ -766,6 +812,12 @@ module ActiveRecord @association.size end + ## + # :method: length + # + # :call-seq: + # length() + # # Returns the size of the collection calling +size+ on the target. # If the collection has been already loaded, +length+ and +size+ are # equivalent. If not and you are going to need the records anyway this @@ -786,14 +838,11 @@ module ActiveRecord # # #<Pet id: 2, name: "Spook", person_id: 1>, # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> # # ] - def length - @association.length - end # Returns +true+ if the collection is empty. If the collection has been # loaded it is equivalent # to <tt>collection.size.zero?</tt>. If the collection has not been loaded, - # it is equivalent to <tt>collection.exists?</tt>. If the collection has + # it is equivalent to <tt>!collection.exists?</tt>. If the collection has # not already been loaded and you are going to fetch the records anyway it # is better to check <tt>collection.length.zero?</tt>. # @@ -812,6 +861,12 @@ module ActiveRecord @association.empty? end + ## + # :method: any? + # + # :call-seq: + # any?() + # # Returns +true+ if the collection is not empty. # # class Person < ActiveRecord::Base @@ -841,10 +896,13 @@ module ActiveRecord # pet.group == 'dogs' # end # # => true - def any?(&block) - @association.any?(&block) - end + ## + # :method: many? + # + # :call-seq: + # many?() + # # Returns true if the collection has more than one record. # Equivalent to <tt>collection.size > 1</tt>. # @@ -879,9 +937,6 @@ module ActiveRecord # pet.group == 'cats' # end # # => true - def many?(&block) - @association.many?(&block) - end # Returns +true+ if the given +record+ is present in the collection. # @@ -897,10 +952,6 @@ module ActiveRecord !!@association.include?(record) end - def arel #:nodoc: - scope.arel - end - def proxy_association @association end @@ -1074,6 +1125,30 @@ module ActiveRecord proxy_association.reset_scope self end + + private + + def find_nth_with_limit(index, limit) + load_target if find_from_target? + super + end + + def find_nth_from_last(index) + load_target if find_from_target? + super + end + + def null_scope? + @association.null_scope? + end + + def find_from_target? + @association.find_from_target? + end + + def exec_queries + load_target + end end end end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index a9f6aaafef..25613d2fa7 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -16,36 +16,26 @@ module ActiveRecord when :restrict_with_error unless empty? record = owner.class.human_attribute_name(reflection.name).downcase - message = owner.errors.generate_message(:base, :'restrict_dependent_destroy.many', record: record, raise: true) rescue nil - if message - ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) - The error key `:'restrict_dependent_destroy.many'` has been deprecated and will be removed in Rails 5.1. - Please use `:'restrict_dependent_destroy.has_many'` instead. - MESSAGE - end - owner.errors.add(:base, message || :'restrict_dependent_destroy.has_many', record: record) + owner.errors.add(:base, :'restrict_dependent_destroy.has_many', record: record) throw(:abort) end + when :destroy + # No point in executing the counter update since we're going to destroy the parent anyway + load_target.each { |t| t.destroyed_by_association = reflection } + destroy_all else - if options[:dependent] == :destroy - # No point in executing the counter update since we're going to destroy the parent anyway - load_target.each { |t| t.destroyed_by_association = reflection } - destroy_all - else - delete_all - end + delete_all end end def insert_record(record, validate = true, raise = false) set_owner_attributes(record) - set_inverse_instance(record) if raise - record.save!(:validate => validate) + record.save!(validate: validate) else - record.save(:validate => validate) + record.save(validate: validate) end end @@ -74,7 +64,7 @@ module ActiveRecord # the loaded flag is set to true as well. def count_records count = if reflection.has_cached_counter? - owner._read_attribute reflection.counter_cache_column + owner._read_attribute(reflection.counter_cache_column).to_i else scope.count end @@ -82,7 +72,7 @@ module ActiveRecord # If there's nothing in the database and @target has no new records # we are certain the current target is an empty array. This is a # documented side-effect of the method that may avoid an extra SELECT. - @target ||= [] and loaded! if count == 0 + (@target ||= []) && loaded! if count == 0 [association_scope.limit_value, count].compact.min end diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index 36fc381343..c4a7fe4432 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -38,10 +38,12 @@ module ActiveRecord def insert_record(record, validate = true, raise = false) ensure_not_nested - if raise - record.save!(:validate => validate) - else - return unless record.save(:validate => validate) + if record.new_record? || record.has_changes_to_save? + if raise + record.save!(validate: validate) + else + return unless record.save(validate: validate) + end end save_through_record(record) @@ -86,7 +88,10 @@ module ActiveRecord end def save_through_record(record) - build_through_record(record).save! + association = build_through_record(record) + if association.changed? + association.save! + end ensure @through_records.delete(record.object_id) end @@ -146,7 +151,7 @@ module ActiveRecord stmt.from scope.klass.arel_table stmt.wheres = arel.constraints - count = scope.klass.connection.delete(stmt, 'SQL', scope.bound_attributes) + count = scope.klass.connection.delete(stmt, "SQL", scope.bound_attributes) end when :nullify count = scope.update_all(source_reflection.foreign_key => nil) @@ -196,7 +201,7 @@ module ActiveRecord def find_target return [] unless target_reflection_has_associated_record? - get_records + super end # NOTE - not sure that we can actually cope with inverses here diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 0fe9b2e81b..b624154def 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -12,14 +12,7 @@ module ActiveRecord when :restrict_with_error if load_target record = owner.class.human_attribute_name(reflection.name).downcase - message = owner.errors.generate_message(:base, :'restrict_dependent_destroy.one', record: record, raise: true) rescue nil - if message - ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) - The error key `:'restrict_dependent_destroy.one'` has been deprecated and will be removed in Rails 5.1. - Please use `:'restrict_dependent_destroy.has_one'` instead. - MESSAGE - end - owner.errors.add(:base, message || :'restrict_dependent_destroy.has_one', record: record) + owner.errors.add(:base, :'restrict_dependent_destroy.has_one', record: record) throw(:abort) end @@ -32,10 +25,10 @@ module ActiveRecord raise_on_type_mismatch!(record) if record load_target - return self.target if !(target || record) + return target unless target || record assigning_another_record = target != record - if assigning_another_record || record.changed? + if assigning_another_record || record.has_changes_to_save? save &&= owner.persisted? transaction_if(save) do @@ -60,12 +53,12 @@ module ActiveRecord def delete(method = options[:dependent]) if load_target case method - when :delete - target.delete - when :destroy - target.destroy - when :nullify - target.update_columns(reflection.foreign_key => nil) if target.persisted? + when :delete + target.delete + when :destroy + target.destroy + when :nullify + target.update_columns(reflection.foreign_key => nil) if target.persisted? end end end @@ -82,18 +75,19 @@ module ActiveRecord def remove_target!(method) case method - when :delete - target.delete - when :destroy - target.destroy - else - nullify_owner_attributes(target) - - if target.persisted? && owner.persisted? && !target.save - set_owner_attributes(target) - raise RecordNotSaved, "Failed to remove the existing associated #{reflection.name}. " + - "The record failed to save after its foreign key was set to nil." - end + when :delete + target.delete + when :destroy + target.destroy + else + nullify_owner_attributes(target) + remove_inverse_instance(target) + + if target.persisted? && owner.persisted? && !target.save + set_owner_attributes(target) + raise RecordNotSaved, "Failed to remove the existing associated #{reflection.name}. " + + "The record failed to save after its foreign key was set to nil." + 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 08e0ec691f..604904abcc 100644 --- a/activerecord/lib/active_record/associations/has_one_through_association.rb +++ b/activerecord/lib/active_record/associations/has_one_through_association.rb @@ -15,7 +15,7 @@ module ActiveRecord ensure_not_nested through_proxy = owner.association(through_reflection.name) - through_record = through_proxy.send(:load_target) + through_record = through_proxy.load_target if through_record && !record through_record.destroy diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index 491152bbcb..4cd1e64c3d 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -1,18 +1,18 @@ module ActiveRecord module Associations class JoinDependency # :nodoc: - autoload :JoinBase, 'active_record/associations/join_dependency/join_base' - autoload :JoinAssociation, 'active_record/associations/join_dependency/join_association' + autoload :JoinBase, "active_record/associations/join_dependency/join_base" + autoload :JoinAssociation, "active_record/associations/join_dependency/join_association" class Aliases # :nodoc: def initialize(tables) @tables = tables - @alias_cache = tables.each_with_object({}) { |table,h| - h[table.node] = table.columns.each_with_object({}) { |column,i| + @alias_cache = tables.each_with_object({}) { |table, h| + h[table.node] = table.columns.each_with_object({}) { |column, i| i[column.name] = column.alias } } - @name_and_alias_cache = tables.each_with_object({}) { |table,h| + @name_and_alias_cache = tables.each_with_object({}) { |table, h| h[table.node] = table.columns.map { |column| [column.name, column.alias] } @@ -62,7 +62,7 @@ module ActiveRecord walk_tree assoc, hash end when Hash - associations.each do |k,v| + associations.each do |k, v| cache = hash[k] ||= {} walk_tree v, cache end @@ -126,8 +126,8 @@ module ActiveRecord 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.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) @@ -143,7 +143,7 @@ module ActiveRecord } } - model_cache = Hash.new { |h,klass| h[klass] = {} } + model_cache = Hash.new { |h, klass| h[klass] = {} } parents = model_cache[join_root] column_aliases = aliases.column_aliases join_root @@ -154,7 +154,7 @@ module ActiveRecord class_name: join_root.base_klass.name } - message_bus.instrument('instantiation.active_record', payload) do + 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) @@ -167,134 +167,140 @@ module ActiveRecord private - 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, child.reflection.scope_chain, chain) - 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, child.reflection.scope_chain, chain) + end - 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 + 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 - [info] + child.children.flat_map { |c| make_outer_joins(child, c) } - end + [info] + child.children.flat_map { |c| make_outer_joins(child, c) } + end - def make_left_outer_joins(parent, child) - tables = child.tables - join_type = Arel::Nodes::OuterJoin - info = make_constraints parent, child, tables, join_type + 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) } - end - - def make_inner_joins(parent, child) - tables = child.tables - join_type = Arel::Nodes::InnerJoin - info = make_constraints parent, child, tables, join_type + [info] + child.children.flat_map { |c| make_left_outer_joins(child, c) } + end - [info] + child.children.flat_map { |c| make_inner_joins(child, c) } - end + def make_inner_joins(parent, child) + tables = child.tables + join_type = Arel::Nodes::InnerJoin + info = make_constraints parent, child, tables, join_type - 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) - ) - } - end + [info] + child.children.flat_map { |c| make_inner_joins(child, c) } + end - def construct_tables!(parent, node) - node.tables = table_aliases_for(parent, node) - node.children.each { |child| construct_tables! node, child } - 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) + ) + } + end - def table_alias_for(reflection, parent, join) - name = "#{reflection.plural_name}_#{parent.table_name}" - name << "_join" if join - name - end + def construct_tables!(parent, node) + node.tables = table_aliases_for(parent, node) + node.children.each { |child| construct_tables! node, child } + end - def walk(left, right) - intersection, missing = right.children.map { |node1| - [left.children.find { |node2| node1.match? node2 }, node1] - }.partition(&:first) + def table_alias_for(reflection, parent, join) + name = "#{reflection.plural_name}_#{parent.table_name}" + name << "_join" if join + name + end - ojs = missing.flat_map { |_,n| make_outer_joins left, n } - intersection.flat_map { |l,r| walk l, r }.concat ojs - end + def walk(left, right) + intersection, missing = right.children.map { |node1| + [left.children.find { |node2| node1.match? node2 }, node1] + }.partition(&:first) - def find_reflection(klass, name) - klass._reflect_on_association(name) or - raise ConfigurationError, "Can't join '#{ klass.name }' to association named '#{ name }'; perhaps you misspelled it?" - end + ojs = missing.flat_map { |_, n| make_outer_joins left, n } + intersection.flat_map { |l, r| walk l, r }.concat ojs + end - def build(associations, base_klass) - associations.map do |name, right| - reflection = find_reflection base_klass, name - reflection.check_validity! - reflection.check_eager_loadable! + def find_reflection(klass, name) + klass._reflect_on_association(name) || + raise(ConfigurationError, "Can't join '#{klass.name}' to association named '#{name}'; perhaps you misspelled it?") + end - if reflection.polymorphic? - next unless @eager_loading - raise EagerLoadPolymorphicError.new(reflection) - end + def build(associations, base_klass) + associations.map do |name, right| + reflection = find_reflection base_klass, name + reflection.check_validity! + reflection.check_eager_loadable! - JoinAssociation.new reflection, build(right, reflection.klass) - end.compact - end + if reflection.polymorphic? + next unless @eager_loading + raise EagerLoadPolymorphicError.new(reflection) + end - def construct(ar_parent, parent, row, rs, seen, model_cache, aliases) - return if ar_parent.nil? + JoinAssociation.new reflection, build(right, reflection.klass) + end.compact + end - parent.children.each do |node| - if node.reflection.collection? - other = ar_parent.association(node.reflection.name) - 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) - next + def construct(ar_parent, parent, row, rs, seen, model_cache, aliases) + return if ar_parent.nil? + + parent.children.each do |node| + if node.reflection.collection? + other = ar_parent.association(node.reflection.name) + 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) + next + end + + key = aliases.column_alias(node, node.primary_key) + id = row[key] + if id.nil? + nil_association = ar_parent.association(node.reflection.name) + nil_association.loaded! + next + end + + model = seen[ar_parent.object_id][node.base_klass][id] + + if model + construct(model, node, row, rs, seen, model_cache, aliases) + 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 + + seen[ar_parent.object_id][node.base_klass][id] = model + construct(model, node, row, rs, seen, model_cache, aliases) + end end + end - key = aliases.column_alias(node, node.primary_key) - id = row[key] - if id.nil? - nil_association = ar_parent.association(node.reflection.name) - nil_association.loaded! - next - end + def construct_model(record, node, row, model_cache, id, aliases) + other = record.association(node.reflection.name) - model = seen[ar_parent.object_id][node.base_klass][id] + model = model_cache[node][id] ||= + node.instantiate(row, aliases.column_aliases(node)) do |m| + other.set_inverse_instance(m) + end - if model - construct(model, node, row, rs, seen, model_cache, aliases) + if node.reflection.collection? + other.target.push(model) else - model = construct_model(ar_parent, node, row, model_cache, id, aliases) - model.readonly! - seen[ar_parent.object_id][node.base_klass][id] = model - construct(model, node, row, rs, seen, model_cache, aliases) + other.target = model end - end - end - def construct_model(record, node, row, model_cache, id, aliases) - model = model_cache[node][id] ||= node.instantiate(row, - aliases.column_aliases(node)) - other = record.association(node.reflection.name) - - if node.reflection.collection? - other.target.push(model) - else - other.target = model + model end - - other.set_inverse_instance(model) - model - end end 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 c5fbe0d1d1..a5705951f3 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -1,4 +1,4 @@ -require 'active_record/associations/join_dependency/join_part' +require "active_record/associations/join_dependency/join_part" module ActiveRecord module Associations @@ -56,7 +56,9 @@ module ActiveRecord klass_scope = if klass.current_scope - klass.current_scope.clone + klass.current_scope.clone.tap { |scope| + scope.joins_values = [] + } else relation = ActiveRecord::Relation.create( klass, 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 3a26c25737..fca20514d1 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_base.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_base.rb @@ -1,4 +1,4 @@ -require 'active_record/associations/join_dependency/join_part' +require "active_record/associations/join_dependency/join_part" module ActiveRecord module Associations 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 9c6573f913..61cec5403a 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_part.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_part.rb @@ -15,7 +15,7 @@ module ActiveRecord # association. attr_reader :base_klass, :children - delegate :table_name, :column_names, :primary_key, :to => :base_klass + delegate :table_name, :column_names, :primary_key, to: :base_klass def initialize(base_klass, children) @base_klass = base_klass @@ -62,8 +62,8 @@ module ActiveRecord hash end - def instantiate(row, aliases) - base_klass.instantiate(extract_record(row, aliases)) + def instantiate(row, aliases, &block) + base_klass.instantiate(extract_record(row, aliases), &block) end end end diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index e64af84e1a..9f77f38b35 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -42,16 +42,16 @@ 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 :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" end NULL_RELATION = Struct.new(:values, :where_clause, :joins_values).new({}, Relation::WhereClause.empty, []) @@ -106,108 +106,108 @@ module ActiveRecord private - # Loads all the given data into +records+ for the +association+. - def preloaders_on(association, records, scope) - case association - when Hash - preloaders_for_hash(association, records, scope) - when Symbol - preloaders_for_one(association, records, scope) - when String - preloaders_for_one(association.to_sym, records, scope) - else - raise ArgumentError, "#{association.inspect} was not recognized for preload" + # Loads all the given data into +records+ for the +association+. + def preloaders_on(association, records, scope) + case association + when Hash + preloaders_for_hash(association, records, scope) + when Symbol + preloaders_for_one(association, records, scope) + when String + preloaders_for_one(association.to_sym, records, scope) + else + raise ArgumentError, "#{association.inspect} was not recognized for preload" + end end - end - def preloaders_for_hash(association, records, scope) - association.flat_map { |parent, child| - loaders = preloaders_for_one parent, records, scope + def preloaders_for_hash(association, records, scope) + association.flat_map { |parent, child| + loaders = preloaders_for_one parent, records, scope - recs = loaders.flat_map(&:preloaded_records).uniq - loaders.concat Array.wrap(child).flat_map { |assoc| - preloaders_on assoc, recs, scope + recs = loaders.flat_map(&:preloaded_records).uniq + loaders.concat Array.wrap(child).flat_map { |assoc| + preloaders_on assoc, recs, scope + } + loaders } - loaders - } - end + end - # Loads all the given data into +records+ for a singular +association+. - # - # Functions by instantiating a preloader class such as Preloader::HasManyThrough and - # call the +run+ method for each passed in class in the +records+ argument. - # - # Not all records have the same class, so group then preload group on the reflection - # itself so that if various subclass share the same association then we do not split - # them unnecessarily - # - # Additionally, polymorphic belongs_to associations can have multiple associated - # classes, depending on the polymorphic_type field. So we group by the classes as - # well. - def preloaders_for_one(association, records, scope) - grouped_records(association, records).flat_map do |reflection, klasses| - klasses.map do |rhs_klass, rs| - loader = preloader_for(reflection, rs, rhs_klass).new(rhs_klass, rs, reflection, scope) - loader.run self - loader + # Loads all the given data into +records+ for a singular +association+. + # + # Functions by instantiating a preloader class such as Preloader::HasManyThrough and + # call the +run+ method for each passed in class in the +records+ argument. + # + # Not all records have the same class, so group then preload group on the reflection + # itself so that if various subclass share the same association then we do not split + # them unnecessarily + # + # Additionally, polymorphic belongs_to associations can have multiple associated + # classes, depending on the polymorphic_type field. So we group by the classes as + # well. + def preloaders_for_one(association, records, scope) + grouped_records(association, records).flat_map do |reflection, klasses| + klasses.map do |rhs_klass, rs| + loader = preloader_for(reflection, rs, rhs_klass).new(rhs_klass, rs, reflection, scope) + loader.run self + loader + end end end - end - def grouped_records(association, records) - h = {} - records.each do |record| - next unless record - assoc = record.association(association) - klasses = h[assoc.reflection] ||= {} - (klasses[assoc.klass] ||= []) << record + def grouped_records(association, records) + h = {} + records.each do |record| + next unless record + assoc = record.association(association) + klasses = h[assoc.reflection] ||= {} + (klasses[assoc.klass] ||= []) << record + end + h end - h - end - class AlreadyLoaded # :nodoc: - attr_reader :owners, :reflection + class AlreadyLoaded # :nodoc: + attr_reader :owners, :reflection - def initialize(klass, owners, reflection, preload_scope) - @owners = owners - @reflection = reflection - end + def initialize(klass, owners, reflection, preload_scope) + @owners = owners + @reflection = reflection + end - def run(preloader); end + def run(preloader); end - def preloaded_records - owners.flat_map { |owner| owner.association(reflection.name).target } + def preloaded_records + owners.flat_map { |owner| owner.association(reflection.name).target } + end 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 - 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 + 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 - # that accepts a preloader. - def preloader_for(reflection, owners, rhs_klass) - return NullPreloader unless rhs_klass + # 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 + # that accepts a preloader. + def preloader_for(reflection, owners, rhs_klass) + return NullPreloader unless rhs_klass - 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 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 + end end - end end end end diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 3032bc786e..4072d19380 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -28,10 +28,6 @@ module ActiveRecord end def records_for(ids) - query_scope(ids) - end - - def query_scope(ids) scope.where(association_key_name => ids) end @@ -61,99 +57,113 @@ module ActiveRecord private - def associated_records_by_owner(preloader) - records = load_records - owners.each_with_object({}) do |owner, result| - result[owner] = records[convert_key(owner[owner_key_name])] || [] + 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 + + owners.each_with_object({}) do |owner, result| + result[owner] = records[convert_key(owner[owner_key_name])] || [] + end end - end - def owner_keys - unless defined?(@owner_keys) - @owner_keys = owners.map do |owner| - owner[owner_key_name] + 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.uniq! - @owner_keys.compact! + @owner_keys end - @owner_keys - end - def key_conversion_required? - @key_conversion_required ||= association_key_type != owner_key_type - 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 + end + end + @owners_by_key + end - def convert_key(key) - if key_conversion_required? - key.to_s - else - key + def key_conversion_required? + @key_conversion_required ||= association_key_type != owner_key_type end - end - def association_key_type - @klass.type_for_attribute(association_key_name.to_s).type - end + def convert_key(key) + if key_conversion_required? + key.to_s + else + key + end + end - def owner_key_type - @model.type_for_attribute(owner_key_name.to_s).type - end + def association_key_type + @klass.type_for_attribute(association_key_name.to_s).type + end + + def owner_key_type + @model.type_for_attribute(owner_key_name.to_s).type + end - def load_records - return {} if owner_keys.empty? - # Some databases impose a limit on the number of ids in a list (in Oracle it's 1000) - # 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) + def load_records(&block) + return {} if owner_keys.empty? + # Some databases impose a limit on the number of ids in a list (in Oracle it's 1000) + # 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) + end + @preloaded_records.group_by do |record| + convert_key(record[association_key_name]) + end end - @preloaded_records.group_by do |record| - convert_key(record[association_key_name]) + + def reflection_scope + @reflection_scope ||= reflection.scope_for(klass) end - end - def reflection_scope - @reflection_scope ||= reflection.scope ? klass.unscoped.instance_exec(nil, &reflection.scope) : klass.unscoped - end + def build_scope + scope = klass.unscoped - def build_scope - scope = klass.unscoped + values = reflection_scope.values + preload_values = preload_scope.values - 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]) - 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 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 - if order_values = preload_values[:order] || values[:order] - scope.order!(order_values) - end + if preload_values[:reordering] || values[:reordering] + scope.reordering_value = true + end - if preload_values[:reordering] || values[:reordering] - scope.reordering_value = true - end + if preload_values[:readonly] || values[:readonly] + scope.readonly! + end - if preload_values[:readonly] || values[:readonly] - scope.readonly! - end + if options[:as] + scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name }) + end - if options[:as] - scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name }) + scope.unscope_values = Array(values[:unscope]) + Array(preload_values[:unscope]) + klass.default_scoped.merge(scope) end - - scope.unscope_values = Array(values[:unscope]) + Array(preload_values[:unscope]) - klass.default_scoped.merge(scope) - end 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 index 5091d4717a..38e231826c 100644 --- a/activerecord/lib/active_record/associations/preloader/belongs_to.rb +++ b/activerecord/lib/active_record/associations/preloader/belongs_to.rb @@ -2,7 +2,6 @@ module ActiveRecord module Associations class Preloader class BelongsTo < SingularAssociation #:nodoc: - def association_key_name reflection.options[:primary_key] || klass && klass.primary_key end @@ -10,7 +9,6 @@ module ActiveRecord def owner_key_name reflection.foreign_key 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 index 9939280fa4..26690bf16d 100644 --- a/activerecord/lib/active_record/associations/preloader/collection_association.rb +++ b/activerecord/lib/active_record/associations/preloader/collection_association.rb @@ -4,14 +4,13 @@ module ActiveRecord 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) - records.each { |record| association.set_inverse_instance(record) } + 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 index 3ea91a8c11..20df1cc19a 100644 --- a/activerecord/lib/active_record/associations/preloader/has_many.rb +++ b/activerecord/lib/active_record/associations/preloader/has_many.rb @@ -2,7 +2,6 @@ module ActiveRecord module Associations class Preloader class HasMany < CollectionAssociation #:nodoc: - def association_key_name reflection.foreign_key end @@ -10,7 +9,6 @@ module ActiveRecord def owner_key_name reflection.active_record_primary_key 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 index f60647a81e..5c5828262e 100644 --- a/activerecord/lib/active_record/associations/preloader/singular_association.rb +++ b/activerecord/lib/active_record/associations/preloader/singular_association.rb @@ -2,19 +2,16 @@ 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 + 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 - association.set_inverse_instance(record) if record + 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 b0203909ce..9d44a02021 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -24,7 +24,7 @@ module ActiveRecord reset_association owners, through_reflection.name - middle_records = through_records.flat_map { |(_,rec)| rec } + middle_records = through_records.flat_map { |(_, rec)| rec } preloaders = preloader.preload(middle_records, source_reflection.name, @@ -32,13 +32,13 @@ module ActiveRecord @preloaded_records = preloaders.flat_map(&:preloaded_records) - middle_to_pl = preloaders.each_with_object({}) do |pl,h| + 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| + 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| @@ -61,48 +61,47 @@ module ActiveRecord private - def id_to_index_map(ids) - id_map = {} - ids.each_with_index { |id, index| id_map[id] = index } - id_map - end + def id_to_index_map(ids) + id_map = {} + ids.each_with_index { |id, index| id_map[id] = index } + id_map + end - def reset_association(owners, association_name) - should_reset = (through_scope != through_reflection.klass.unscoped) || - (reflection.options[:source_type] && through_reflection.collection?) + def reset_association(owners, association_name) + should_reset = (through_scope != through_reflection.klass.unscoped) || + (reflection.options[:source_type] && through_reflection.collection?) - # Don't cache the association - we would only be caching a subset - if should_reset - owners.each { |owner| - owner.association(association_name).reset - } + # Don't cache the association - we would only be caching a subset + if should_reset + owners.each { |owner| + owner.association(association_name).reset + } + end end - end + def through_scope + scope = through_reflection.klass.unscoped - def through_scope - scope = through_reflection.klass.unscoped + 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 + end - 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 + scope.references! reflection_scope.values[:references] + if scope.eager_loading? && order_values = reflection_scope.values[:order] + scope = scope.order(order_values) + end end - scope.references! reflection_scope.values[:references] - if scope.eager_loading? && order_values = reflection_scope.values[:order] - scope = scope.order(order_values) - end + scope end - scope - end - - def target_records_from_association(association) - association.loaded? ? association.target : association.reader - end + def target_records_from_association(association) + association.loaded? ? association.target : association.reader + 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 f913f0852a..91580a28d0 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -2,16 +2,8 @@ module ActiveRecord module Associations class SingularAssociation < Association #:nodoc: # Implements the reader method, e.g. foo.bar for Foo.has_one :bar - def reader(force_reload = false) - if force_reload && klass - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing an argument to force an association to reload is now - deprecated and will be removed in Rails 5.1. Please call `reload` - on the parent object instead. - MSG - - klass.uncached { reload } - elsif !loaded? || stale_target? + def reader + if !loaded? || stale_target? reload end @@ -23,14 +15,6 @@ module ActiveRecord replace(record) end - def create(attributes = {}, &block) - _create_record(attributes, &block) - end - - def create!(attributes = {}, &block) - _create_record(attributes, true, &block) - end - def build(attributes = {}) record = build_record(attributes) yield(record) if block_given? @@ -38,14 +22,21 @@ module ActiveRecord record end + # Implements the reload reader method, e.g. foo.reload_bar for + # Foo.has_one :bar + def force_reload_reader + klass.uncached { reload } + target + end + private def create_scope scope.scope_for_create.stringify_keys.except(klass.primary_key) end - def get_records - return scope.limit(1).records if skip_statement_cache? + def find_target + return scope.take if skip_statement_cache? conn = klass.connection sc = reflection.association_scope_cache(conn, owner) do @@ -56,13 +47,11 @@ module ActiveRecord end binds = AssociationScope.get_bind_values(owner, reflection.chain) - sc.execute binds, klass, klass.connection - end - - def find_target - if record = get_records.first + sc.execute(binds, klass, conn) do |record| set_inverse_instance record - end + end.first + rescue ::RangeError + nil end def replace(record) diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index d0ec3e8015..6b87993ba3 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -2,10 +2,9 @@ module ActiveRecord # = Active Record Through Association module Associations module ThroughAssociation #:nodoc: + delegate :source_reflection, :through_reflection, to: :reflection - delegate :source_reflection, :through_reflection, :to => :reflection - - protected + private # We merge in these scopes for two reasons: # @@ -22,8 +21,6 @@ module ActiveRecord scope end - private - # Construct attributes for :through pointing to owner and associate. This is used by the # methods which create and delete records on the association. # |