1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00
rails--rails/activerecord/lib/active_record/reflection.rb
Volmer Campos Soares 4e37f28881 Derive foreign key from model name in has_many associations
Currently Active Record derives the foreign key name for `has_many`
associations based on the Active Record Base `name. Sometimes when using
additional conventions for class names, such as with class name suffixes
and prefixes, it makes sense to be able to further customize the
foreign key logic to account for a different pattern.

Similar to what was done in the
[case of table names](https://github.com/rails/rails/pull/42213),
as Active Record Base extends Active Model Naming, we have already a
`model_name` at the class-level that behaves similarly to `name` while
at the same time giving more flexibility by allowing specifying a custom
name and namespacing.

This commit changes the foreign key computation in Reflections to infer
its value based on the Active Record's `model_name` object instead of
its class `name`. This allows customization of the foreing key pattern
since a distinct instance of `ActiveModel::Name` can be used instead.

For example, to use `post_id` as foreign key for a `PostRecord` class
in a `has_many` association:

```ruby
 class PostRecord < ActiveRecord::Base
   has_many :comments

   class << self
     def model_name
       ActiveModel::Name.new(self, nil, "Post")
     end
   end
 end

 PostRecord.reflect_on_association(:comments).foreign_key
 # => "post_id"
```
2021-05-12 15:21:31 -04:00

1058 lines
34 KiB
Ruby

# frozen_string_literal: true
require "active_support/core_ext/string/filters"
module ActiveRecord
# = Active Record Reflection
module Reflection # :nodoc:
extend ActiveSupport::Concern
included do
class_attribute :_reflections, instance_writer: false, default: {}
class_attribute :aggregate_reflections, instance_writer: false, default: {}
end
class << self
def create(macro, name, scope, options, ar)
reflection = reflection_class_for(macro).new(name, scope, options, ar)
options[:through] ? ThroughReflection.new(reflection) : reflection
end
def add_reflection(ar, name, reflection)
ar.clear_reflections_cache
name = -name.to_s
ar._reflections = ar._reflections.except(name).merge!(name => reflection)
end
def add_aggregate_reflection(ar, name, reflection)
ar.aggregate_reflections = ar.aggregate_reflections.merge(-name.to_s => reflection)
end
private
def reflection_class_for(macro)
case macro
when :composed_of
AggregateReflection
when :has_many
HasManyReflection
when :has_one
HasOneReflection
when :belongs_to
BelongsToReflection
else
raise "Unsupported Macro: #{macro}"
end
end
end
# \Reflection enables the ability to examine the associations and aggregations of
# Active Record classes and objects. This information, for example,
# can be used in a form builder that takes an Active Record object
# and creates input fields for all of the attributes depending on their type
# and displays the associations to other objects.
#
# MacroReflection class has info for AggregateReflection and AssociationReflection
# classes.
module ClassMethods
# Returns an array of AggregateReflection objects for all the aggregations in the class.
def reflect_on_all_aggregations
aggregate_reflections.values
end
# Returns the AggregateReflection object for the named +aggregation+ (use the symbol).
#
# Account.reflect_on_aggregation(:balance) # => the balance AggregateReflection
#
def reflect_on_aggregation(aggregation)
aggregate_reflections[aggregation.to_s]
end
# Returns a Hash of name of the reflection as the key and an AssociationReflection as the value.
#
# Account.reflections # => {"balance" => AggregateReflection}
#
def reflections
@__reflections ||= begin
ref = {}
_reflections.each do |name, reflection|
parent_reflection = reflection.parent_reflection
if parent_reflection
parent_name = parent_reflection.name
ref[parent_name.to_s] = parent_reflection
else
ref[name] = reflection
end
end
ref
end
end
# Returns an array of AssociationReflection objects for all the
# associations in the class. If you only want to reflect on a certain
# association type, pass in the symbol (<tt>:has_many</tt>, <tt>:has_one</tt>,
# <tt>:belongs_to</tt>) as the first parameter.
#
# Example:
#
# Account.reflect_on_all_associations # returns an array of all associations
# Account.reflect_on_all_associations(:has_many) # returns an array of all has_many associations
#
def reflect_on_all_associations(macro = nil)
association_reflections = reflections.values
association_reflections.select! { |reflection| reflection.macro == macro } if macro
association_reflections
end
# Returns the AssociationReflection object for the +association+ (use the symbol).
#
# Account.reflect_on_association(:owner) # returns the owner AssociationReflection
# Invoice.reflect_on_association(:line_items).macro # returns :has_many
#
def reflect_on_association(association)
reflections[association.to_s]
end
def _reflect_on_association(association) #:nodoc:
_reflections[association.to_s]
end
# Returns an array of AssociationReflection objects for all associations which have <tt>:autosave</tt> enabled.
def reflect_on_all_autosave_associations
reflections.values.select { |reflection| reflection.options[:autosave] }
end
def clear_reflections_cache # :nodoc:
@__reflections = nil
end
end
# Holds all the methods that are shared between MacroReflection and ThroughReflection.
#
# AbstractReflection
# MacroReflection
# AggregateReflection
# AssociationReflection
# HasManyReflection
# HasOneReflection
# BelongsToReflection
# HasAndBelongsToManyReflection
# ThroughReflection
# PolymorphicReflection
# RuntimeReflection
class AbstractReflection # :nodoc:
def through_reflection?
false
end
def table_name
klass.table_name
end
# Returns a new, unsaved instance of the associated class. +attributes+ will
# be passed to the class's constructor.
def build_association(attributes, &block)
klass.new(attributes, &block)
end
# Returns the class name for the macro.
#
# <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>'Money'</tt>
# <tt>has_many :clients</tt> returns <tt>'Client'</tt>
def class_name
@class_name ||= -(options[:class_name] || derive_class_name).to_s
end
# Returns a list of scopes that should be applied for this Reflection
# object when querying the database.
def scopes
scope ? [scope] : []
end
def join_scope(table, foreign_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.polymorphic_name)
end
scope_chain_items.inject(klass_scope, &:merge!)
primary_key = join_primary_key
foreign_key = join_foreign_key
klass_scope.where!(table[primary_key].eq(foreign_table[foreign_key]))
if klass.finder_needs_type_condition?
klass_scope.where!(klass.send(:type_condition, table))
end
klass_scope
end
def join_scopes(table, predicate_builder, klass = self.klass) # :nodoc:
if scope
[scope_for(build_scope(table, predicate_builder, klass))]
else
[]
end
end
def klass_join_scope(table, predicate_builder) # :nodoc:
relation = build_scope(table, predicate_builder)
klass.scope_for_association(relation)
end
def constraints
chain.flat_map(&:scopes)
end
def counter_cache_column
@counter_cache_column ||= if belongs_to?
if options[:counter_cache] == true
-"#{active_record.name.demodulize.underscore.pluralize}_count"
elsif options[:counter_cache]
-options[:counter_cache].to_s
end
else
-(options[:counter_cache]&.to_s || "#{name}_count")
end
end
def inverse_of
return unless inverse_name
@inverse_of ||= klass._reflect_on_association inverse_name
end
def check_validity_of_inverse!
unless polymorphic?
if has_inverse? && inverse_of.nil?
raise InverseOfAssociationNotFoundError.new(self)
end
end
end
# This shit is nasty. We need to avoid the following situation:
#
# * An associated record is deleted via record.destroy
# * Hence the callbacks run, and they find a belongs_to on the record with a
# :counter_cache options which points back at our owner. So they update the
# counter cache.
# * In which case, we must make sure to *not* update the counter cache, or else
# it will be decremented twice.
#
# Hence this method.
def inverse_which_updates_counter_cache
return @inverse_which_updates_counter_cache if defined?(@inverse_which_updates_counter_cache)
@inverse_which_updates_counter_cache = klass.reflect_on_all_associations(:belongs_to).find do |inverse|
inverse.counter_cache_column == counter_cache_column
end
end
alias inverse_updates_counter_cache? inverse_which_updates_counter_cache
def inverse_updates_counter_in_memory?
inverse_of && inverse_which_updates_counter_cache == inverse_of
end
# Returns whether a counter cache should be used for this association.
#
# The counter_cache option must be given on either the owner or inverse
# association, and the column must be present on the owner.
def has_cached_counter?
options[:counter_cache] ||
inverse_which_updates_counter_cache && inverse_which_updates_counter_cache.options[:counter_cache] &&
active_record.has_attribute?(counter_cache_column)
end
def counter_must_be_updated_by_has_many?
!inverse_updates_counter_in_memory? && has_cached_counter?
end
def alias_candidate(name)
"#{plural_name}_#{name}"
end
def chain
collect_join_chain
end
def build_scope(table, predicate_builder = predicate_builder(table), klass = self.klass)
Relation.create(
klass,
table: table,
predicate_builder: predicate_builder
)
end
def strict_loading?
options[:strict_loading]
end
protected
def actual_source_reflection # FIXME: this is a horrible name
self
end
private
def predicate_builder(table)
PredicateBuilder.new(TableMetadata.new(klass, table))
end
def primary_key(klass)
klass.primary_key || raise(UnknownPrimaryKey.new(klass))
end
def ensure_option_not_given_as_class!(option_name)
if options[option_name] && options[option_name].class == Class
raise ArgumentError, "A class was passed to `:#{option_name}` but we are expecting a string."
end
end
end
# Base class for AggregateReflection and AssociationReflection. Objects of
# AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods.
class MacroReflection < AbstractReflection
# Returns the name of the macro.
#
# <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>:balance</tt>
# <tt>has_many :clients</tt> returns <tt>:clients</tt>
attr_reader :name
attr_reader :scope
# Returns the hash of options used for the macro.
#
# <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>{ class_name: "Money" }</tt>
# <tt>has_many :clients</tt> returns <tt>{}</tt>
attr_reader :options
attr_reader :active_record
attr_reader :plural_name # :nodoc:
def initialize(name, scope, options, active_record)
@name = name
@scope = scope
@options = options
@active_record = active_record
@klass = options[:anonymous_class]
@plural_name = active_record.pluralize_table_names ?
name.to_s.pluralize : name.to_s
end
def autosave=(autosave)
@options[:autosave] = autosave
parent_reflection = self.parent_reflection
if parent_reflection
parent_reflection.autosave = autosave
end
end
# Returns the class for the macro.
#
# <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
def compute_class(name)
name.constantize
end
# Returns +true+ if +self+ and +other_aggregation+ have the same +name+ attribute, +active_record+ attribute,
# and +other_aggregation+ has an options hash assigned to it.
def ==(other_aggregation)
super ||
other_aggregation.kind_of?(self.class) &&
name == other_aggregation.name &&
!other_aggregation.options.nil? &&
active_record == other_aggregation.active_record
end
def scope_for(relation, owner = nil)
relation.instance_exec(owner, &scope) || relation
end
private
def derive_class_name
name.to_s.camelize
end
end
# Holds all the metadata about an aggregation as it was specified in the
# Active Record class.
class AggregateReflection < MacroReflection #:nodoc:
def mapping
mapping = options[:mapping] || [name, name]
mapping.first.is_a?(Array) ? mapping : [mapping]
end
end
# Holds all the metadata about an association as it was specified in the
# Active Record class.
class AssociationReflection < MacroReflection #:nodoc:
def compute_class(name)
if polymorphic?
raise ArgumentError, "Polymorphic associations do not support computing the class."
end
msg = <<-MSG.squish
Rails couldn't find a valid model for #{name} association.
Please provide the :class_name option on the association declaration.
If :class_name is already provided make sure is an ActiveRecord::Base subclass.
MSG
begin
klass = active_record.send(:compute_type, name)
unless klass < ActiveRecord::Base
raise ArgumentError, msg
end
klass
rescue NameError
raise NameError, msg
end
end
attr_reader :type, :foreign_type
attr_accessor :parent_reflection # Reflection
def initialize(name, scope, options, active_record)
super
@type = -(options[:foreign_type]&.to_s || "#{options[:as]}_type") if options[:as]
@foreign_type = -(options[:foreign_type]&.to_s || "#{name}_type") if options[:polymorphic]
ensure_option_not_given_as_class!(:class_name)
end
def association_scope_cache(klass, owner, &block)
key = self
if polymorphic?
key = [key, owner._read_attribute(@foreign_type)]
end
klass.cached_find_by_statement(key, &block)
end
def join_table
@join_table ||= -(options[:join_table]&.to_s || derive_join_table)
end
def foreign_key
@foreign_key ||= -(options[:foreign_key]&.to_s || derive_foreign_key)
end
def association_foreign_key
@association_foreign_key ||= -(options[:association_foreign_key]&.to_s || class_name.foreign_key)
end
def association_primary_key(klass = nil)
primary_key(klass || self.klass)
end
def active_record_primary_key
@active_record_primary_key ||= -(options[:primary_key]&.to_s || primary_key(active_record))
end
def join_primary_key(klass = nil)
foreign_key
end
def join_foreign_key
active_record_primary_key
end
def check_validity!
check_validity_of_inverse!
end
def check_preloadable!
return unless scope
unless scope.arity == 0
raise ArgumentError, <<-MSG.squish
The association scope '#{name}' is instance dependent (the scope
block takes an argument). Preloading instance dependent scopes is
not supported.
MSG
end
end
alias :check_eager_loadable! :check_preloadable!
def join_id_for(owner) # :nodoc:
owner[join_foreign_key]
end
def through_reflection
nil
end
def source_reflection
self
end
# A chain of reflections from this one back to the owner. For more see the explanation in
# ThroughReflection.
def collect_join_chain
[self]
end
# This is for clearing cache on the reflection. Useful for tests that need to compare
# SQL queries on associations.
def clear_association_scope_cache # :nodoc:
klass.initialize_find_by_cache
end
def nested?
false
end
def has_scope?
scope
end
def has_inverse?
inverse_name
end
def polymorphic_inverse_of(associated_class)
if has_inverse?
if inverse_relationship = associated_class._reflect_on_association(options[:inverse_of])
inverse_relationship
else
raise InverseOfAssociationNotFoundError.new(self, associated_class)
end
end
end
# Returns the macro type.
#
# <tt>has_many :clients</tt> returns <tt>:has_many</tt>
def macro; raise NotImplementedError; end
# Returns whether or not this association reflection is for a collection
# association. Returns +true+ if the +macro+ is either +has_many+ or
# +has_and_belongs_to_many+, +false+ otherwise.
def collection?
false
end
# Returns whether or not the association should be validated as part of
# the parent's validation.
#
# Unless you explicitly disable validation with
# <tt>validate: false</tt>, validation will take place when:
#
# * you explicitly enable validation; <tt>validate: true</tt>
# * you use autosave; <tt>autosave: true</tt>
# * the association is a +has_many+ association
def validate?
!options[:validate].nil? ? options[:validate] : (options[:autosave] == true || collection?)
end
# Returns +true+ if +self+ is a +belongs_to+ reflection.
def belongs_to?; false; end
# Returns +true+ if +self+ is a +has_one+ reflection.
def has_one?; false; end
def association_class; raise NotImplementedError; end
def polymorphic?
options[:polymorphic]
end
def add_as_source(seed)
seed
end
def add_as_polymorphic_through(reflection, seed)
seed + [PolymorphicReflection.new(self, reflection)]
end
def add_as_through(seed)
seed + [self]
end
def extensions
Array(options[:extend])
end
private
# Attempts to find the inverse association name automatically.
# If it cannot find a suitable inverse association name, it returns
# +nil+.
def inverse_name
unless defined?(@inverse_name)
@inverse_name = options.fetch(:inverse_of) { automatic_inverse_of }
end
@inverse_name
end
# 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
begin
reflection = klass._reflect_on_association(inverse_name)
rescue NameError
# Give up: we couldn't compute the klass type so we won't be able
# to find any associations either.
reflection = false
end
if valid_inverse_reflection?(reflection)
inverse_name
end
end
end
# Checks if the inverse reflection that is returned from the
# +automatic_inverse_of+ method is a valid reflection. We must
# make sure that the reflection's active_record name matches up
# with the current reflection's klass name.
def valid_inverse_reflection?(reflection)
reflection &&
foreign_key == reflection.foreign_key &&
klass <= reflection.active_record &&
can_find_inverse_of_automatically?(reflection)
end
# Checks to see if the reflection doesn't have any options that prevent
# 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>: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.
def can_find_inverse_of_automatically?(reflection)
reflection.options[:inverse_of] != false &&
!reflection.options[:through] &&
!reflection.options[:foreign_key] &&
!reflection.scope
end
def derive_class_name
class_name = name.to_s
class_name = class_name.singularize if collection?
class_name.camelize
end
def derive_foreign_key
if belongs_to?
"#{name}_id"
elsif options[:as]
"#{options[:as]}_id"
else
active_record.model_name.to_s.foreign_key
end
end
def derive_join_table
ModelSchema.derive_join_table_name active_record.table_name, klass.table_name
end
end
class HasManyReflection < AssociationReflection # :nodoc:
def macro; :has_many; end
def collection?; true; end
def association_class
if options[:through]
Associations::HasManyThroughAssociation
else
Associations::HasManyAssociation
end
end
end
class HasOneReflection < AssociationReflection # :nodoc:
def macro; :has_one; end
def has_one?; true; end
def association_class
if options[:through]
Associations::HasOneThroughAssociation
else
Associations::HasOneAssociation
end
end
end
class BelongsToReflection < AssociationReflection # :nodoc:
def macro; :belongs_to; end
def belongs_to?; true; end
def association_class
if polymorphic?
Associations::BelongsToPolymorphicAssociation
else
Associations::BelongsToAssociation
end
end
# klass option is necessary to support loading polymorphic associations
def association_primary_key(klass = nil)
if primary_key = options[:primary_key]
@association_primary_key ||= -primary_key.to_s
else
primary_key(klass || self.klass)
end
end
def join_primary_key(klass = nil)
polymorphic? ? association_primary_key(klass) : association_primary_key
end
def join_foreign_key
foreign_key
end
def join_foreign_type
foreign_type
end
private
def can_find_inverse_of_automatically?(_)
!polymorphic? && super
end
end
class HasAndBelongsToManyReflection < AssociationReflection # :nodoc:
def macro; :has_and_belongs_to_many; end
def collection?
true
end
end
# Holds all the metadata about a :through association as it was specified
# in the Active Record class.
class ThroughReflection < AbstractReflection #:nodoc:
delegate :foreign_key, :foreign_type, :association_foreign_key, :join_id_for, :type,
:active_record_primary_key, :join_foreign_key, to: :source_reflection
def initialize(delegate_reflection)
@delegate_reflection = delegate_reflection
@klass = delegate_reflection.options[:anonymous_class]
@source_reflection_name = delegate_reflection.options[:source]
ensure_option_not_given_as_class!(:source_type)
end
def through_reflection?
true
end
def klass
@klass ||= delegate_reflection.compute_class(class_name)
end
# Returns the source of the through reflection. It checks both a singularized
# and pluralized form for <tt>:belongs_to</tt> or <tt>:has_many</tt>.
#
# class Post < ActiveRecord::Base
# has_many :taggings
# has_many :tags, through: :taggings
# end
#
# class Tagging < ActiveRecord::Base
# belongs_to :post
# belongs_to :tag
# end
#
# tags_reflection = Post.reflect_on_association(:tags)
# tags_reflection.source_reflection
# # => <ActiveRecord::Reflection::BelongsToReflection: @name=:tag, @active_record=Tagging, @plural_name="tags">
#
def source_reflection
through_reflection.klass._reflect_on_association(source_reflection_name)
end
# Returns the AssociationReflection object specified in the <tt>:through</tt> option
# of a HasManyThrough or HasOneThrough association.
#
# class Post < ActiveRecord::Base
# has_many :taggings
# has_many :tags, through: :taggings
# end
#
# tags_reflection = Post.reflect_on_association(:tags)
# tags_reflection.through_reflection
# # => <ActiveRecord::Reflection::HasManyReflection: @name=:taggings, @active_record=Post, @plural_name="taggings">
#
def through_reflection
active_record._reflect_on_association(options[:through])
end
# Returns an array of reflections which are involved in this association. Each item in the
# array corresponds to a table which will be part of the query for this association.
#
# The chain is built by recursively calling #chain on the source reflection and the through
# reflection. The base case for the recursion is a normal association, which just returns
# [self] as its #chain.
#
# class Post < ActiveRecord::Base
# has_many :taggings
# has_many :tags, through: :taggings
# end
#
# tags_reflection = Post.reflect_on_association(:tags)
# tags_reflection.chain
# # => [<ActiveRecord::Reflection::ThroughReflection: @delegate_reflection=#<ActiveRecord::Reflection::HasManyReflection: @name=:tags...>,
# <ActiveRecord::Reflection::HasManyReflection: @name=:taggings, @options={}, @active_record=Post>]
#
def collect_join_chain
collect_join_reflections [self]
end
# This is for clearing cache on the reflection. Useful for tests that need to compare
# SQL queries on associations.
def clear_association_scope_cache # :nodoc:
delegate_reflection.clear_association_scope_cache
source_reflection.clear_association_scope_cache
through_reflection.clear_association_scope_cache
end
def scopes
source_reflection.scopes + super
end
def join_scopes(table, predicate_builder, klass = self.klass) # :nodoc:
source_reflection.join_scopes(table, predicate_builder, klass) + super
end
def has_scope?
scope || options[:source_type] ||
source_reflection.has_scope? ||
through_reflection.has_scope?
end
# A through association is nested if there would be more than one join table
def nested?
source_reflection.through_reflection? || through_reflection.through_reflection?
end
# We want to use the klass from this reflection, rather than just delegate straight to
# the source_reflection, because the source_reflection may be polymorphic. We still
# need to respect the source_reflection's :primary_key option, though.
def association_primary_key(klass = nil)
# Get the "actual" source reflection if the immediate source reflection has a
# source reflection itself
if primary_key = actual_source_reflection.options[:primary_key]
@association_primary_key ||= -primary_key.to_s
else
primary_key(klass || self.klass)
end
end
def join_primary_key(klass = self.klass)
source_reflection.join_primary_key(klass)
end
# Gets an array of possible <tt>:through</tt> source reflection names in both singular and plural form.
#
# class Post < ActiveRecord::Base
# has_many :taggings
# has_many :tags, through: :taggings
# end
#
# tags_reflection = Post.reflect_on_association(:tags)
# tags_reflection.source_reflection_names
# # => [:tag, :tags]
#
def source_reflection_names
options[:source] ? [options[:source]] : [name.to_s.singularize, name].uniq
end
def source_reflection_name # :nodoc:
return @source_reflection_name if @source_reflection_name
names = [name.to_s.singularize, name].collect(&:to_sym).uniq
names = names.find_all { |n|
through_reflection.klass._reflect_on_association(n)
}
if names.length > 1
raise AmbiguousSourceReflectionForThroughAssociation.new(
active_record.name,
macro,
name,
options,
source_reflection_names
)
end
@source_reflection_name = names.first
end
def source_options
source_reflection.options
end
def through_options
through_reflection.options
end
def check_validity!
if through_reflection.nil?
raise HasManyThroughAssociationNotFoundError.new(active_record, self)
end
if through_reflection.polymorphic?
if has_one?
raise HasOneAssociationPolymorphicThroughError.new(active_record.name, self)
else
raise HasManyThroughAssociationPolymorphicThroughError.new(active_record.name, self)
end
end
if source_reflection.nil?
raise HasManyThroughSourceAssociationNotFoundError.new(self)
end
if options[:source_type] && !source_reflection.polymorphic?
raise HasManyThroughAssociationPointlessSourceTypeError.new(active_record.name, self, source_reflection)
end
if source_reflection.polymorphic? && options[:source_type].nil?
raise HasManyThroughAssociationPolymorphicSourceError.new(active_record.name, self, source_reflection)
end
if has_one? && through_reflection.collection?
raise HasOneThroughCantAssociateThroughCollection.new(active_record.name, self, through_reflection)
end
if parent_reflection.nil?
reflections = active_record.reflections.keys.map(&:to_sym)
if reflections.index(through_reflection.name) > reflections.index(name)
raise HasManyThroughOrderError.new(active_record.name, self, through_reflection)
end
end
check_validity_of_inverse!
end
def constraints
scope_chain = source_reflection.constraints
scope_chain << scope if scope
scope_chain
end
def add_as_source(seed)
collect_join_reflections seed
end
def add_as_polymorphic_through(reflection, seed)
collect_join_reflections(seed + [PolymorphicReflection.new(self, reflection)])
end
def add_as_through(seed)
collect_join_reflections(seed + [self])
end
protected
def actual_source_reflection # FIXME: this is a horrible name
source_reflection.actual_source_reflection
end
private
attr_reader :delegate_reflection
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
def derive_class_name
# get the class_name of the belongs_to association of the through reflection
options[:source_type] || source_reflection.class_name
end
delegate_methods = AssociationReflection.public_instance_methods -
public_instance_methods
delegate(*delegate_methods, to: :delegate_reflection)
end
class PolymorphicReflection < AbstractReflection # :nodoc:
delegate :klass, :scope, :plural_name, :type, :join_primary_key, :join_foreign_key,
:name, :scope_for, to: :@reflection
def initialize(reflection, previous_reflection)
@reflection = reflection
@previous_reflection = previous_reflection
end
def join_scopes(table, predicate_builder, klass = self.klass) # :nodoc:
scopes = @previous_reflection.join_scopes(table, predicate_builder) + super
scopes << build_scope(table, predicate_builder, klass).instance_exec(nil, &source_type_scope)
end
def constraints
@reflection.constraints + [source_type_scope]
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 < AbstractReflection # :nodoc:
delegate :scope, :type, :constraints, :join_foreign_key, to: :@reflection
def initialize(reflection, association)
@reflection = reflection
@association = association
end
def klass
@association.klass
end
def aliased_table
klass.arel_table
end
def join_primary_key(klass = self.klass)
@reflection.join_primary_key(klass)
end
def all_includes; yield; end
end
end
end