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.rb253
1 files changed, 196 insertions, 57 deletions
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index 7f47a812eb..e801bc4afa 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -1,10 +1,18 @@
+require 'active_support/core_ext/class/attribute'
+require 'active_support/core_ext/module/deprecation'
+
module ActiveRecord
# = Active Record Reflection
module Reflection # :nodoc:
extend ActiveSupport::Concern
+ included do
+ class_attribute :reflections
+ self.reflections = {}
+ end
+
# Reflection enables to interrogate Active Record classes and objects
- # about their associations and aggregations. This information can,
+ # about their associations and aggregations. This information can,
# for example, 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.
@@ -20,36 +28,27 @@ module ActiveRecord
when :composed_of
reflection = AggregateReflection.new(macro, name, options, active_record)
end
- write_inheritable_hash :reflections, name => reflection
- reflection
- end
- # Returns a hash containing all AssociationReflection objects for the current class.
- # Example:
- #
- # Invoice.reflections
- # Account.reflections
- #
- def reflections
- read_inheritable_attribute(:reflections) || write_inheritable_attribute(:reflections, {})
+ self.reflections = self.reflections.merge(name => reflection)
+ reflection
end
# Returns an array of AggregateReflection objects for all the aggregations in the class.
def reflect_on_all_aggregations
- reflections.values.select { |reflection| reflection.is_a?(AggregateReflection) }
+ reflections.values.grep(AggregateReflection)
end
- # Returns the AggregateReflection object for the named +aggregation+ (use the symbol).
+ # Returns the AggregateReflection object for the named +aggregation+ (use the symbol).
#
- # Account.reflect_on_aggregation(:balance) #=> the balance AggregateReflection
+ # Account.reflect_on_aggregation(:balance) # => the balance AggregateReflection
#
def reflect_on_aggregation(aggregation)
reflections[aggregation].is_a?(AggregateReflection) ? reflections[aggregation] : nil
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>,
+ # 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:
@@ -58,7 +57,7 @@ module ActiveRecord
# 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.select { |reflection| reflection.is_a?(AssociationReflection) }
+ association_reflections = reflections.values.grep(AssociationReflection)
macro ? association_reflections.select { |reflection| reflection.macro == macro } : association_reflections
end
@@ -78,7 +77,7 @@ module ActiveRecord
end
- # Abstract base class for AggregateReflection and AssociationReflection. Objects of
+ # Abstract base class for AggregateReflection and AssociationReflection. Objects of
# AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods.
class MacroReflection
attr_reader :active_record
@@ -89,23 +88,23 @@ module ActiveRecord
# Returns the name of the macro.
#
- # <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>:balance</tt>
+ # <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.
+ # 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
- # Returns the hash of options used for the macro.
+ # 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 +{}+
attr_reader :options
- # Returns the class for the macro.
+ # 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
@@ -113,7 +112,7 @@ module ActiveRecord
@klass ||= class_name.constantize
end
- # Returns the class name for the macro.
+ # 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>
@@ -138,12 +137,12 @@ module ActiveRecord
end
- # Holds all the meta-data about an aggregation as it was specified in the
+ # Holds all the meta-data about an aggregation as it was specified in the
# Active Record class.
class AggregateReflection < MacroReflection #:nodoc:
end
- # Holds all the meta-data about an association as it was specified in the
+ # Holds all the meta-data about an association as it was specified in the
# Active Record class.
class AssociationReflection < MacroReflection #:nodoc:
# Returns the target association's class.
@@ -198,8 +197,21 @@ module ActiveRecord
@quoted_table_name ||= klass.quoted_table_name
end
+ def foreign_key
+ @foreign_key ||= options[:foreign_key] || derive_foreign_key
+ end
+
def primary_key_name
- @primary_key_name ||= options[:foreign_key] || derive_primary_key_name
+ foreign_key
+ end
+ deprecate :primary_key_name => :foreign_key
+
+ def foreign_type
+ @foreign_type ||= options[:foreign_type] || "#{name}_type"
+ end
+
+ def type
+ @type ||= "#{options[:as]}_type"
end
def primary_key_column
@@ -207,7 +219,18 @@ module ActiveRecord
end
def association_foreign_key
- @association_foreign_key ||= @options[:association_foreign_key] || class_name.foreign_key
+ @association_foreign_key ||= options[:association_foreign_key] || class_name.foreign_key
+ end
+
+ def association_primary_key
+ @association_primary_key ||=
+ options[:primary_key] ||
+ !options[:polymorphic] && klass.primary_key ||
+ 'id'
+ end
+
+ def active_record_primary_key
+ @active_record_primary_key ||= options[:primary_key] || active_record.primary_key
end
def counter_cache_column
@@ -239,18 +262,32 @@ module ActiveRecord
end
def through_reflection
- false
- end
-
- def through_reflection_primary_key_name
+ nil
end
def source_reflection
nil
end
+ # A chain of reflections from this one back to the owner. For more see the explanation in
+ # ThroughReflection.
+ def chain
+ [self]
+ end
+
+ # An array of arrays of conditions. Each item in the outside array corresponds to a reflection
+ # in the #chain. The inside arrays are simply conditions (and each condition may itself be
+ # a hash, array, arel predicate, etc...)
+ def conditions
+ conditions = [options[:conditions]].compact
+ conditions << { type => active_record.base_class.name } if options[:as]
+ [conditions]
+ end
+
+ alias :source_macro :macro
+
def has_inverse?
- !@options[:inverse_of].nil?
+ @options[:inverse_of]
end
def inverse_of
@@ -289,22 +326,36 @@ module ActiveRecord
!options[:validate].nil? ? options[:validate] : (options[:autosave] == true || macro == :has_many)
end
- def dependent_conditions(record, base_class, extra_conditions)
- dependent_conditions = []
- dependent_conditions << "#{primary_key_name} = #{record.send(name).send(:owner_quoted_id)}"
- dependent_conditions << "#{options[:as]}_type = '#{base_class.name}'" if options[:as]
- dependent_conditions << klass.send(:sanitize_sql, options[:conditions]) if options[:conditions]
- dependent_conditions << extra_conditions if extra_conditions
- dependent_conditions = dependent_conditions.collect {|where| "(#{where})" }.join(" AND ")
- dependent_conditions = dependent_conditions.gsub('@', '\@')
- dependent_conditions
- end
-
# Returns +true+ if +self+ is a +belongs_to+ reflection.
def belongs_to?
macro == :belongs_to
end
+ def association_class
+ case macro
+ when :belongs_to
+ if options[:polymorphic]
+ Associations::BelongsToPolymorphicAssociation
+ else
+ Associations::BelongsToAssociation
+ end
+ when :has_and_belongs_to_many
+ Associations::HasAndBelongsToManyAssociation
+ when :has_many
+ if options[:through]
+ Associations::HasManyThroughAssociation
+ else
+ Associations::HasManyAssociation
+ end
+ when :has_one
+ if options[:through]
+ Associations::HasOneThroughAssociation
+ else
+ Associations::HasOneAssociation
+ end
+ end
+ end
+
private
def derive_class_name
class_name = name.to_s.camelize
@@ -312,7 +363,7 @@ module ActiveRecord
class_name
end
- def derive_primary_key_name
+ def derive_foreign_key
if belongs_to?
"#{name}_id"
elsif options[:as]
@@ -326,6 +377,8 @@ module ActiveRecord
# Holds all the meta-data about a :through association as it was specified
# in the Active Record class.
class ThroughReflection < AssociationReflection #:nodoc:
+ delegate :foreign_key, :foreign_type, :association_foreign_key, :active_record_primary_key, :to => :source_reflection
+
# Gets the source of the through reflection. It checks both a singularized
# and pluralized form for <tt>:belongs_to</tt> or <tt>:has_many</tt>.
#
@@ -353,6 +406,88 @@ module ActiveRecord
@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.
+ def chain
+ @chain ||= begin
+ chain = source_reflection.chain + through_reflection.chain
+ chain[0] = self # Use self so we don't lose the information from :source_type
+ chain
+ end
+ end
+
+ # Consider the following example:
+ #
+ # class Person
+ # has_many :articles
+ # has_many :comment_tags, :through => :articles
+ # end
+ #
+ # class Article
+ # has_many :comments
+ # has_many :comment_tags, :through => :comments, :source => :tags
+ # end
+ #
+ # class Comment
+ # has_many :tags
+ # end
+ #
+ # There may be conditions on Person.comment_tags, Article.comment_tags and/or Comment.tags,
+ # but only Comment.tags will be represented in the #chain. So this method creates an array
+ # of conditions corresponding to the chain. Each item in the #conditions array corresponds
+ # to an item in the #chain, and is itself an array of conditions from an arbitrary number
+ # of relevant reflections, plus any :source_type or polymorphic :as constraints.
+ def conditions
+ @conditions ||= begin
+ conditions = source_reflection.conditions
+
+ # Add to it the conditions from this reflection if necessary.
+ conditions.first << options[:conditions] if options[:conditions]
+
+ through_conditions = through_reflection.conditions
+
+ if options[:source_type]
+ through_conditions.first << { foreign_type => options[:source_type] }
+ end
+
+ # Recursively fill out the rest of the array from the through reflection
+ conditions += through_conditions
+
+ # And return
+ conditions
+ end
+ end
+
+ # The macro used by the source association
+ def source_macro
+ source_reflection.source_macro
+ end
+
+ # A through association is nested iff there would be more than one join table
+ def nested?
+ chain.length > 2 || through_reflection.macro == :has_and_belongs_to_many
+ 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
+ @association_primary_key ||= begin
+ # Get the "actual" source reflection if the immediate source reflection has a
+ # source reflection itself
+ source_reflection = self.source_reflection
+ while source_reflection.source_reflection
+ source_reflection = source_reflection.source_reflection
+ end
+
+ source_reflection.options[:primary_key] || klass.primary_key
+ end
+ end
+
# Gets an array of possible <tt>:through</tt> source reflection names:
#
# [:singularized, :pluralized]
@@ -361,11 +496,23 @@ module ActiveRecord
@source_reflection_names ||= (options[:source] ? [options[:source]] : [name.to_s.singularize, name]).collect { |n| n.to_sym }
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.name, self)
end
+ if through_reflection.options[:polymorphic]
+ raise HasManyThroughAssociationPolymorphicThroughError.new(active_record.name, self)
+ end
+
if source_reflection.nil?
raise HasManyThroughSourceAssociationNotFoundError.new(self)
end
@@ -375,24 +522,16 @@ module ActiveRecord
end
if source_reflection.options[:polymorphic] && options[:source_type].nil?
- raise HasManyThroughAssociationPolymorphicError.new(active_record.name, self, source_reflection)
+ raise HasManyThroughAssociationPolymorphicSourceError.new(active_record.name, self, source_reflection)
end
- unless [:belongs_to, :has_many, :has_one].include?(source_reflection.macro) && source_reflection.options[:through].nil?
- raise HasManyThroughSourceAssociationMacroError.new(self)
+ if macro == :has_one && through_reflection.collection?
+ raise HasOneThroughCantAssociateThroughCollection.new(active_record.name, self, through_reflection)
end
check_validity_of_inverse!
end
- def through_reflection_primary_key
- through_reflection.belongs_to? ? through_reflection.klass.primary_key : through_reflection.primary_key_name
- end
-
- def through_reflection_primary_key_name
- through_reflection.primary_key_name if through_reflection.belongs_to?
- end
-
private
def derive_class_name
# get the class_name of the belongs_to association of the through reflection