aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/reflection.rb
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/lib/active_record/reflection.rb')
-rw-r--r--activerecord/lib/active_record/reflection.rb322
1 files changed, 206 insertions, 116 deletions
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index f23ed263fb..dd746a4e10 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -1,4 +1,5 @@
require 'thread'
+require 'active_support/core_ext/string/filters'
module ActiveRecord
# = Active Record Reflection
@@ -13,14 +14,21 @@ module ActiveRecord
end
def self.create(macro, name, scope, options, ar)
- case macro
- when :has_many, :belongs_to, :has_one
- klass = options[:through] ? ThroughReflection : AssociationReflection
- when :composed_of
- klass = AggregateReflection
- end
-
- klass.new(macro, name, scope, options, ar)
+ klass = case macro
+ when :composed_of
+ AggregateReflection
+ when :has_many
+ HasManyReflection
+ when :has_one
+ HasOneReflection
+ when :belongs_to
+ BelongsToReflection
+ else
+ raise "Unsupported Macro: #{macro}"
+ end
+
+ reflection = klass.new(name, scope, options, ar)
+ options[:through] ? ThroughReflection.new(reflection) : reflection
end
def self.add_reflection(ar, name, reflection)
@@ -31,9 +39,9 @@ module ActiveRecord
ar.aggregate_reflections = ar.aggregate_reflections.merge(name.to_s => reflection)
end
- # \Reflection enables to interrogate Active Record classes and objects
- # about their associations and aggregations. This information can,
- # for example, be used in a form builder that takes an Active Record object
+ # \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.
#
@@ -55,7 +63,7 @@ module ActiveRecord
# Returns a Hash of name of the reflection as the key and a AssociationReflection as the value.
#
- # Account.reflections # => {balance: AggregateReflection}
+ # Account.reflections # => {"balance" => AggregateReflection}
#
# @api public
def reflections
@@ -110,26 +118,67 @@ module ActiveRecord
end
end
+ # Holds all the methods that are shared between MacroReflection, AssociationReflection
+ # and ThroughReflection
+ class AbstractReflection # :nodoc:
+ 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
+
+ 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>
+ # <tt>has_many :clients</tt> returns <tt>'Client'</tt>
+ def class_name
+ @class_name ||= (options[:class_name] || derive_class_name).to_s
+ end
+
+ JoinKeys = Struct.new(:key, :foreign_key) # :nodoc:
+
+ def join_keys(assoc_klass)
+ JoinKeys.new(foreign_key, active_record_primary_key)
+ end
+
+ def source_macro
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ ActiveRecord::Base.source_macro is deprecated and will be removed
+ without replacement.
+ MSG
+
+ macro
+ end
+ end
# Base class for AggregateReflection and AssociationReflection. Objects of
# AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods.
#
# MacroReflection
- # AggregateReflection
# AssociationReflection
- # ThroughReflection
- class MacroReflection
+ # AggregateReflection
+ # HasManyReflection
+ # HasOneReflection
+ # BelongsToReflection
+ # ThroughReflection
+ 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
- # Returns the macro type.
- #
- # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>:composed_of</tt>
- # <tt>has_many :clients</tt> returns <tt>:has_many</tt>
- attr_reader :macro
-
attr_reader :scope
# Returns the hash of options used for the macro.
@@ -142,8 +191,7 @@ module ActiveRecord
attr_reader :plural_name # :nodoc:
- def initialize(macro, name, scope, options, active_record)
- @macro = macro
+ def initialize(name, scope, options, active_record)
@name = name
@scope = scope
@options = options
@@ -167,15 +215,11 @@ module ActiveRecord
# <tt>composed_of :balance, class_name: 'Money'</tt> returns the Money class
# <tt>has_many :clients</tt> returns the Client class
def klass
- @klass ||= class_name.constantize
+ @klass ||= compute_class(class_name)
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
+ def compute_class(name)
+ name.constantize
end
# Returns +true+ if +self+ and +other_aggregation+ have the same +name+ attribute, +active_record+ attribute,
@@ -188,23 +232,6 @@ module ActiveRecord
active_record == other_aggregation.active_record
end
- JoinKeys = Struct.new(:key, :foreign_key) # :nodoc:
-
- def join_keys(assoc_klass)
- if source_macro == :belongs_to
- if polymorphic?
- reflection_key = association_primary_key(assoc_klass)
- else
- reflection_key = association_primary_key
- end
- reflection_foreign_key = foreign_key
- else
- reflection_key = foreign_key
- reflection_foreign_key = active_record_primary_key
- end
- JoinKeys.new(reflection_key, reflection_foreign_key)
- end
-
private
def derive_class_name
name.to_s.camelize
@@ -237,17 +264,20 @@ module ActiveRecord
# a new association object. Use +build_association+ or +create_association+
# instead. This allows plugins to hook into association object creation.
def klass
- @klass ||= active_record.send(:compute_type, class_name)
+ @klass ||= compute_class(class_name)
+ end
+
+ def compute_class(name)
+ active_record.send(:compute_type, name)
end
attr_reader :type, :foreign_type
attr_accessor :parent_reflection # [:name, Reflection]
- def initialize(macro, name, scope, options, active_record)
+ def initialize(name, scope, options, active_record)
super
- @collection = macro == :has_many
@automatic_inverse_of = nil
- @type = options[:as] && "#{options[:as]}_type"
+ @type = options[:as] && (options[:foreign_type] || "#{options[:as]}_type")
@foreign_type = options[:foreign_type] || "#{name}_type"
@constructable = calculate_constructable(macro, options)
@association_scope_cache = {}
@@ -257,31 +287,17 @@ module ActiveRecord
def association_scope_cache(conn, owner)
key = conn.prepared_statements
if polymorphic?
- key = [key, owner.read_attribute(@foreign_type)]
+ key = [key, owner._read_attribute(@foreign_type)]
end
@association_scope_cache[key] ||= @scope_lock.synchronize {
@association_scope_cache[key] ||= yield
}
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
-
def constructable? # :nodoc:
@constructable
end
- def table_name
- klass.table_name
- end
-
- def quoted_table_name
- klass.quoted_table_name
- end
-
def join_table
@join_table ||= options[:join_table] || derive_join_table
end
@@ -290,10 +306,6 @@ module ActiveRecord
@foreign_key ||= options[:foreign_key] || derive_foreign_key
end
- def primary_key_column
- klass.columns_hash[klass.primary_key]
- end
-
def association_foreign_key
@association_foreign_key ||= options[:association_foreign_key] || class_name.foreign_key
end
@@ -331,19 +343,20 @@ module ActiveRecord
return unless scope
if scope.arity > 0
- ActiveSupport::Deprecation.warn <<-WARNING
-The association scope '#{name}' is instance dependent (the scope block takes an argument).
-Preloading happens before the individual instances are created. This means that there is no instance
-being passed to the association scope. This will most likely result in broken or incorrect behavior.
-Joining, Preloading and eager loading of these associations is deprecated and will be removed in the future.
- WARNING
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ The association scope '#{name}' is instance dependent (the scope
+ block takes an argument). Preloading happens before the individual
+ instances are created. This means that there is no instance being
+ passed to the association scope. This will most likely result in
+ broken or incorrect behavior. Joining, Preloading and eager loading
+ of these associations is deprecated and will be removed in the future.
+ MSG
end
end
alias :check_eager_loadable! :check_preloadable!
- def join_id_for(owner) #:nodoc:
- key = (source_macro == :belongs_to) ? foreign_key : active_record_primary_key
- owner[key]
+ def join_id_for(owner) # :nodoc:
+ owner[active_record_primary_key]
end
def through_reflection
@@ -370,8 +383,6 @@ Joining, Preloading and eager loading of these associations is deprecated and wi
scope ? [[scope]] : [[]]
end
- alias :source_macro :macro
-
def has_inverse?
inverse_name
end
@@ -392,11 +403,16 @@ Joining, Preloading and eager loading of these associations is deprecated and wi
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?
- @collection
+ false
end
# Returns whether or not the association should be validated as part of
@@ -413,14 +429,10 @@ Joining, Preloading and eager loading of these associations is deprecated and wi
end
# Returns +true+ if +self+ is a +belongs_to+ reflection.
- def belongs_to?
- macro == :belongs_to
- end
+ def belongs_to?; false; end
# Returns +true+ if +self+ is a +has_one+ reflection.
- def has_one?
- macro == :has_one
- end
+ def has_one?; false; end
def association_class
case macro
@@ -552,7 +564,7 @@ Joining, Preloading and eager loading of these associations is deprecated and wi
end
def derive_join_table
- [active_record.table_name, klass.table_name].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').gsub("\0", "_")
+ ModelSchema.derive_join_table_name active_record.table_name, klass.table_name
end
def primary_key(klass)
@@ -560,22 +572,72 @@ Joining, Preloading and eager loading of these associations is deprecated and wi
end
end
- class HasAndBelongsToManyReflection < AssociationReflection #:nodoc:
- def initialize(macro, name, scope, options, active_record)
+ class HasManyReflection < AssociationReflection # :nodoc:
+ def initialize(name, scope, options, active_record)
+ super(name, scope, options, active_record)
+ end
+
+ def macro; :has_many; end
+
+ def collection?; true; end
+ end
+
+ class HasOneReflection < AssociationReflection # :nodoc:
+ def initialize(name, scope, options, active_record)
+ super(name, scope, options, active_record)
+ end
+
+ def macro; :has_one; end
+
+ def has_one?; true; end
+ end
+
+ class BelongsToReflection < AssociationReflection # :nodoc:
+ def initialize(name, scope, options, active_record)
+ super(name, scope, options, active_record)
+ end
+
+ def macro; :belongs_to; end
+
+ def belongs_to?; true; end
+
+ def join_keys(assoc_klass)
+ key = polymorphic? ? association_primary_key(assoc_klass) : association_primary_key
+ JoinKeys.new(key, foreign_key)
+ end
+
+ def join_id_for(owner) # :nodoc:
+ owner[foreign_key]
+ end
+ end
+
+ class HasAndBelongsToManyReflection < AssociationReflection # :nodoc:
+ def initialize(name, scope, options, active_record)
super
- @collection = true
+ end
+
+ def macro; :has_and_belongs_to_many; end
+
+ def collection?
+ true
end
end
# Holds all the meta-data about a :through association as it was specified
# in the Active Record class.
- class ThroughReflection < AssociationReflection #:nodoc:
+ class ThroughReflection < AbstractReflection #:nodoc:
+ attr_reader :delegate_reflection
delegate :foreign_key, :foreign_type, :association_foreign_key,
:active_record_primary_key, :type, :to => :source_reflection
- def initialize(macro, name, scope, options, active_record)
- super
- @source_reflection_name = options[:source]
+ def initialize(delegate_reflection)
+ @delegate_reflection = delegate_reflection
+ @klass = delegate_reflection.options[:class]
+ @source_reflection_name = delegate_reflection.options[:source]
+ end
+
+ def klass
+ @klass ||= delegate_reflection.compute_class(class_name)
end
# Returns the source of the through reflection. It checks both a singularized
@@ -593,7 +655,7 @@ Joining, Preloading and eager loading of these associations is deprecated and wi
#
# tags_reflection = Post.reflect_on_association(:tags)
# tags_reflection.source_reflection
- # # => <ActiveRecord::Reflection::AssociationReflection: @macro=:belongs_to, @name=:tag, @active_record=Tagging, @plural_name="tags">
+ # # => <ActiveRecord::Reflection::BelongsToReflection: @name=:tag, @active_record=Tagging, @plural_name="tags">
#
def source_reflection
through_reflection.klass._reflect_on_association(source_reflection_name)
@@ -609,7 +671,7 @@ Joining, Preloading and eager loading of these associations is deprecated and wi
#
# tags_reflection = Post.reflect_on_association(:tags)
# tags_reflection.through_reflection
- # # => <ActiveRecord::Reflection::AssociationReflection: @macro=:has_many, @name=:taggings, @active_record=Post, @plural_name="taggings">
+ # # => <ActiveRecord::Reflection::HasManyReflection: @name=:taggings, @active_record=Post, @plural_name="taggings">
#
def through_reflection
active_record._reflect_on_association(options[:through])
@@ -629,8 +691,8 @@ Joining, Preloading and eager loading of these associations is deprecated and wi
#
# tags_reflection = Post.reflect_on_association(:tags)
# tags_reflection.chain
- # # => [<ActiveRecord::Reflection::ThroughReflection: @macro=:has_many, @name=:tags, @options={:through=>:taggings}, @active_record=Post>,
- # <ActiveRecord::Reflection::AssociationReflection: @macro=:has_many, @name=:taggings, @options={}, @active_record=Post>]
+ # # => [<ActiveRecord::Reflection::ThroughReflection: @delegate_reflection=#<ActiveRecord::Reflection::HasManyReflection: @name=:tags...>,
+ # <ActiveRecord::Reflection::HasManyReflection: @name=:taggings, @options={}, @active_record=Post>]
#
def chain
@chain ||= begin
@@ -671,8 +733,11 @@ Joining, Preloading and eager loading of these associations is deprecated and wi
through_scope_chain = through_reflection.scope_chain.map(&:dup)
if options[:source_type]
- through_scope_chain.first <<
- through_reflection.klass.where(foreign_type => options[:source_type])
+ type = foreign_type
+ source_type = options[:source_type]
+ through_scope_chain.first << lambda { |object|
+ where(type => source_type)
+ }
end
# Recursively fill out the rest of the array from the through reflection
@@ -680,8 +745,17 @@ Joining, Preloading and eager loading of these associations is deprecated and wi
end
end
+ def join_keys(assoc_klass)
+ source_reflection.join_keys(assoc_klass)
+ end
+
# The macro used by the source association
def source_macro
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ ActiveRecord::Base.source_macro is deprecated and will be removed
+ without replacement.
+ MSG
+
source_reflection.source_macro
end
@@ -717,7 +791,7 @@ Joining, Preloading and eager loading of these associations is deprecated and wi
def source_reflection_name # :nodoc:
return @source_reflection_name if @source_reflection_name
- names = [name.to_s.singularize, name].collect { |n| n.to_sym }.uniq
+ names = [name.to_s.singularize, name].collect(&:to_sym).uniq
names = names.find_all { |n|
through_reflection.klass._reflect_on_association(n)
}
@@ -725,15 +799,13 @@ Joining, Preloading and eager loading of these associations is deprecated and wi
if names.length > 1
example_options = options.dup
example_options[:source] = source_reflection_names.first
- ActiveSupport::Deprecation.warn <<-eowarn
-Ambiguous source reflection for through association. Please specify a :source
-directive on your declaration like:
-
- class #{active_record.name} < ActiveRecord::Base
- #{macro} :#{name}, #{example_options}
- end
-
- eowarn
+ ActiveSupport::Deprecation.warn \
+ "Ambiguous source reflection for through association. Please " \
+ "specify a :source directive on your declaration like:\n" \
+ "\n" \
+ " class #{active_record.name} < ActiveRecord::Base\n" \
+ " #{macro} :#{name}, #{example_options}\n" \
+ " end"
end
@source_reflection_name = names.first
@@ -747,13 +819,21 @@ directive on your declaration like:
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)
end
if through_reflection.polymorphic?
- raise HasManyThroughAssociationPolymorphicThroughError.new(active_record.name, self)
+ if has_one?
+ raise HasOneAssociationPolymorphicThroughError.new(active_record.name, self)
+ else
+ raise HasManyThroughAssociationPolymorphicThroughError.new(active_record.name, self)
+ end
end
if source_reflection.nil?
@@ -777,15 +857,25 @@ directive on your declaration like:
protected
- def actual_source_reflection # FIXME: this is a horrible name
- source_reflection.actual_source_reflection
- end
+ def actual_source_reflection # FIXME: this is a horrible name
+ source_reflection.send(:actual_source_reflection)
+ end
+
+ def primary_key(klass)
+ klass.primary_key || raise(UnknownPrimaryKey.new(klass))
+ end
private
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
end
end