diff options
Diffstat (limited to 'activerecord/lib/active_record/reflection.rb')
-rw-r--r-- | activerecord/lib/active_record/reflection.rb | 320 |
1 files changed, 125 insertions, 195 deletions
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 24ca8b0be4..87bfd75bca 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -1,6 +1,7 @@ -require "thread" +# frozen_string_literal: true + require "active_support/core_ext/string/filters" -require "active_support/deprecation" +require "concurrent/map" module ActiveRecord # = Active Record Reflection @@ -8,10 +9,8 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :_reflections, instance_writer: false - class_attribute :aggregate_reflections, instance_writer: false - self._reflections = {} - self.aggregate_reflections = {} + class_attribute :_reflections, instance_writer: false, default: {} + class_attribute :aggregate_reflections, instance_writer: false, default: {} end def self.create(macro, name, scope, options, ar) @@ -138,7 +137,7 @@ module ActiveRecord # HasAndBelongsToManyReflection # ThroughReflection # PolymorphicReflection - # RuntimeReflection + # RuntimeReflection class AbstractReflection # :nodoc: def through_reflection? false @@ -154,14 +153,6 @@ module ActiveRecord klass.new(attributes, &block) end - def quoted_table_name - klass.quoted_table_name - end - - def primary_key_type - klass.type_for_attribute(klass.primary_key) - end - # Returns the class name for the macro. # # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>'Money'</tt> @@ -173,7 +164,7 @@ module ActiveRecord JoinKeys = Struct.new(:key, :foreign_key) # :nodoc: def join_keys - get_join_keys klass + @join_keys ||= get_join_keys(klass) end # Returns a list of scopes that should be applied for this Reflection @@ -182,37 +173,46 @@ module ActiveRecord scope ? [scope] : [] end - def scope_chain - chain.map(&:scopes) + def build_join_constraint(table, foreign_table) + key = join_keys.key + foreign_key = join_keys.foreign_key + + constraint = table[key].eq(foreign_table[foreign_key]) + + if klass.finder_needs_type_condition? + table.create_and([constraint, klass.send(:type_condition, table)]) + else + constraint + end + end + + def join_scope(table, foreign_klass) + predicate_builder = predicate_builder(table) + scope_chain_items = join_scopes(table, predicate_builder) + klass_scope = klass_join_scope(table, predicate_builder) + + if type + klass_scope.where!(type => foreign_klass.base_class.sti_name) + end + + scope_chain_items.inject(klass_scope, &:merge!) end - deprecate :scope_chain def join_scopes(table, predicate_builder) # :nodoc: if scope - [ActiveRecord::Relation.create(klass, table, predicate_builder) - .instance_exec(&scope)] + [scope_for(build_scope(table, predicate_builder))] else [] end end def klass_join_scope(table, predicate_builder) # :nodoc: - 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 + relation = build_scope(table, predicate_builder) + klass.scope_for_association(relation) end def constraints - chain.map(&:scopes).flatten + chain.flat_map(&:scopes) end def counter_cache_column @@ -286,17 +286,33 @@ module ActiveRecord end def get_join_keys(association_klass) - JoinKeys.new(join_pk(association_klass), join_fk) + JoinKeys.new(join_primary_key(association_klass), join_foreign_key) end - private + def build_scope(table, predicate_builder = predicate_builder(table)) + Relation.create(klass, table, predicate_builder) + end + + def join_primary_key(_) + foreign_key + end + + def join_foreign_key + active_record_primary_key + end + + protected + def actual_source_reflection # FIXME: this is a horrible name + self + end - def join_pk(_) - foreign_key + private + def predicate_builder(table) + PredicateBuilder.new(TableMetadata.new(klass, table)) end - def join_fk - active_record_primary_key + def primary_key(klass) + klass.primary_key || raise(UnknownPrimaryKey.new(klass)) end end @@ -343,6 +359,17 @@ module ActiveRecord # # <tt>composed_of :balance, class_name: 'Money'</tt> returns the Money class # <tt>has_many :clients</tt> returns the Client class + # + # class Company < ActiveRecord::Base + # has_many :clients + # end + # + # Company.reflect_on_association(:clients).klass + # # => Client + # + # <b>Note:</b> Do not call +klass.new+ or +klass.create+ to instantiate + # a new association object. Use +build_association+ or +create_association+ + # instead. This allows plugins to hook into association object creation. def klass @klass ||= compute_class(class_name) end @@ -361,8 +388,8 @@ module ActiveRecord active_record == other_aggregation.active_record end - def scope_for(klass) - scope ? klass.unscoped.instance_exec(nil, &scope) : klass.unscoped + def scope_for(relation, owner = nil) + relation.instance_exec(owner, &scope) || relation end private @@ -383,22 +410,6 @@ module ActiveRecord # Holds all the metadata about an association as it was specified in the # Active Record class. class AssociationReflection < MacroReflection #:nodoc: - # Returns the target association's class. - # - # class Author < ActiveRecord::Base - # has_many :books - # end - # - # Author.reflect_on_association(:books).klass - # # => Book - # - # <b>Note:</b> Do not call +klass.new+ or +klass.create+ to instantiate - # a new association object. Use +build_association+ or +create_association+ - # instead. This allows plugins to hook into association object creation. - def klass - @klass ||= compute_class(class_name) - end - def compute_class(name) active_record.send(:compute_type, name) end @@ -408,33 +419,22 @@ module ActiveRecord def initialize(name, scope, options, active_record) super - @automatic_inverse_of = nil @type = options[:as] && (options[:foreign_type] || "#{options[:as]}_type") - @foreign_type = options[:foreign_type] || "#{name}_type" + @foreign_type = options[:polymorphic] && (options[:foreign_type] || "#{name}_type") @constructable = calculate_constructable(macro, options) - @association_scope_cache = {} - @scope_lock = Mutex.new + @association_scope_cache = Concurrent::Map.new if options[:class_name] && options[:class_name].class == Class - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing a class to the `class_name` is deprecated and will raise - an ArgumentError in Rails 5.2. It eagerloads more classes than - necessary and potentially creates circular dependencies. - - Please pass the class name as a string: - `#{macro} :#{name}, class_name: '#{options[:class_name]}'` - MSG + raise ArgumentError, "A class was passed to `:class_name` but we are expecting a string." end end - def association_scope_cache(conn, owner) + def association_scope_cache(conn, owner, &block) key = conn.prepared_statements if polymorphic? key = [key, owner._read_attribute(@foreign_type)] end - @association_scope_cache[key] ||= @scope_lock.synchronize { - @association_scope_cache[key] ||= yield - } + @association_scope_cache.compute_if_absent(key) { StatementCache.create(conn, &block) } end def constructable? # :nodoc: @@ -484,7 +484,7 @@ module ActiveRecord alias :check_eager_loadable! :check_preloadable! def join_id_for(owner) # :nodoc: - owner[active_record_primary_key] + owner[join_foreign_key] end def through_reflection @@ -567,7 +567,7 @@ module ActiveRecord end VALID_AUTOMATIC_INVERSE_MACROS = [:has_many, :has_one, :belongs_to] - INVALID_AUTOMATIC_INVERSE_OPTIONS = [:conditions, :through, :polymorphic, :foreign_key] + INVALID_AUTOMATIC_INVERSE_OPTIONS = [:conditions, :through, :foreign_key] def add_as_source(seed) seed @@ -581,11 +581,9 @@ module ActiveRecord seed + [self] end - protected - - def actual_source_reflection # FIXME: this is a horrible name - self - end + def extensions + Array(options[:extend]) + end private @@ -597,12 +595,14 @@ module ActiveRecord # If it cannot find a suitable inverse association name, it returns # +nil+. def inverse_name - options.fetch(:inverse_of) do - @automatic_inverse_of ||= automatic_inverse_of + unless defined?(@inverse_name) + @inverse_name = options.fetch(:inverse_of) { automatic_inverse_of } end + + @inverse_name end - # returns either false or the inverse association name that it finds. + # returns either +nil+ or the inverse association name that it finds. def automatic_inverse_of if can_find_inverse_of_automatically?(self) inverse_name = ActiveSupport::Inflector.underscore(options[:as] || active_record.name.demodulize).to_sym @@ -619,8 +619,6 @@ module ActiveRecord return inverse_name end end - - false end # Checks if the inverse reflection that is returned from the @@ -632,7 +630,7 @@ module ActiveRecord # from calling +klass+, +reflection+ will already be set to false. def valid_inverse_reflection?(reflection) reflection && - klass.name == reflection.active_record.name && + klass <= reflection.active_record && can_find_inverse_of_automatically?(reflection) end @@ -640,9 +638,8 @@ module ActiveRecord # us from being able to guess the inverse automatically. First, the # <tt>inverse_of</tt> option cannot be set to false. Second, we must # have <tt>has_many</tt>, <tt>has_one</tt>, <tt>belongs_to</tt> associations. - # Third, we must not have options such as <tt>:polymorphic</tt> or - # <tt>:foreign_key</tt> which prevent us from correctly guessing the - # inverse association. + # Third, we must not have options such as <tt>:foreign_key</tt> + # which prevent us from correctly guessing the inverse association. # # Anything with a scope can additionally ruin our attempt at finding an # inverse, so we exclude reflections with scopes. @@ -672,10 +669,6 @@ module ActiveRecord def derive_join_table ModelSchema.derive_join_table_name active_record.table_name, klass.table_name end - - def primary_key(klass) - klass.primary_key || raise(UnknownPrimaryKey.new(klass)) - end end class HasManyReflection < AssociationReflection # :nodoc: @@ -690,6 +683,10 @@ module ActiveRecord Associations::HasManyAssociation end end + + def association_primary_key(klass = nil) + primary_key(klass || self.klass) + end end class HasOneReflection < AssociationReflection # :nodoc: @@ -725,8 +722,12 @@ module ActiveRecord end end - def join_id_for(owner) # :nodoc: - owner[foreign_key] + def join_primary_key(klass) + polymorphic? ? association_primary_key(klass) : association_primary_key + end + + def join_foreign_key + foreign_key end private @@ -734,21 +735,9 @@ module ActiveRecord def calculate_constructable(macro, options) !polymorphic? end - - def join_fk - foreign_key - end - - def join_pk(klass) - polymorphic? ? association_primary_key(klass) : association_primary_key - end end class HasAndBelongsToManyReflection < AssociationReflection # :nodoc: - def initialize(name, scope, options, active_record) - super - end - def macro; :has_and_belongs_to_many; end def collection? @@ -759,8 +748,7 @@ module ActiveRecord # Holds all the metadata about a :through association as it was specified # in the Active Record class. class ThroughReflection < AbstractReflection #:nodoc: - attr_reader :delegate_reflection - delegate :foreign_key, :foreign_type, :association_foreign_key, + delegate :foreign_key, :foreign_type, :association_foreign_key, :join_id_for, :active_record_primary_key, :type, :get_join_keys, to: :source_reflection def initialize(delegate_reflection) @@ -851,10 +839,6 @@ module ActiveRecord source_reflection.join_scopes(table, predicate_builder) + super end - def source_type_scope - through_reflection.klass.where(foreign_type => options[:source_type]) - end - def has_scope? scope || options[:source_type] || source_reflection.has_scope? || @@ -923,10 +907,6 @@ module ActiveRecord through_reflection.options end - def join_id_for(owner) # :nodoc: - source_reflection.join_id_for(owner) - end - def check_validity! if through_reflection.nil? raise HasManyThroughAssociationNotFoundError.new(active_record.name, self) @@ -985,23 +965,23 @@ module ActiveRecord collect_join_reflections(seed + [self]) end - def collect_join_reflections(seed) - a = source_reflection.add_as_source seed - if options[:source_type] - through_reflection.add_as_polymorphic_through self, a - else - through_reflection.add_as_through a - end - end - - private + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. + protected + attr_reader :delegate_reflection def actual_source_reflection # FIXME: this is a horrible name - source_reflection.send(:actual_source_reflection) + source_reflection.actual_source_reflection end - def primary_key(klass) - klass.primary_key || raise(UnknownPrimaryKey.new(klass)) + private + def collect_join_reflections(seed) + a = source_reflection.add_as_source seed + if options[:source_type] + through_reflection.add_as_polymorphic_through self, a + else + through_reflection.add_as_through a + end end def inverse_name; delegate_reflection.send(:inverse_name); end @@ -1018,66 +998,32 @@ module ActiveRecord end class PolymorphicReflection < AbstractReflection # :nodoc: + delegate :klass, :scope, :plural_name, :type, :get_join_keys, :scope_for, to: :@reflection + def initialize(reflection, previous_reflection) @reflection = reflection @previous_reflection = previous_reflection end - def scopes - scopes = @previous_reflection.scopes - if @previous_reflection.options[:source_type] - scopes + [@previous_reflection.source_type_scope] - else - scopes - end - end - def join_scopes(table, predicate_builder) # :nodoc: scopes = @previous_reflection.join_scopes(table, predicate_builder) + super - if @previous_reflection.options[:source_type] - scopes + [@previous_reflection.source_type_scope] - else - scopes - end - end - - def klass - @reflection.klass - end - - def scope - @reflection.scope - end - - def table_name - @reflection.table_name - end - - def plural_name - @reflection.plural_name - end - - def type - @reflection.type + scopes << build_scope(table, predicate_builder).instance_exec(nil, &source_type_scope) end def constraints - @reflection.constraints + [source_type_info] - end - - def source_type_info - type = @previous_reflection.foreign_type - source_type = @previous_reflection.options[:source_type] - lambda { |object| where(type => source_type) } + @reflection.constraints + [source_type_scope] end - def get_join_keys(association_klass) - @reflection.get_join_keys(association_klass) - end + private + def source_type_scope + type = @previous_reflection.foreign_type + source_type = @previous_reflection.options[:source_type] + lambda { |object| where(type => source_type) } + end end - class RuntimeReflection < PolymorphicReflection # :nodoc: - attr_accessor :next + class RuntimeReflection < AbstractReflection # :nodoc: + delegate :scope, :type, :constraints, :get_join_keys, to: :@reflection def initialize(reflection, association) @reflection = reflection @@ -1088,24 +1034,8 @@ module ActiveRecord @association.klass end - def table_name - klass.table_name - end - - def constraints - @reflection.constraints - end - - def source_type_info - @reflection.source_type_info - end - - def alias_candidate(name) - "#{plural_name}_#{name}_join" - end - - def alias_name - Arel::Table.new(table_name) + def aliased_table + @aliased_table ||= Arel::Table.new(table_name, type_caster: klass.type_caster) end def all_includes; yield; end |