require 'active_support/core_ext/array/wrap' module ActiveRecord module Associations # = Active Record Associations # # This is the root class of all association proxies ('+ Foo' signifies an included module Foo): # # AssociationProxy # BelongsToAssociation # BelongsToPolymorphicAssociation # AssociationCollection + HasAssociation # HasAndBelongsToManyAssociation # HasManyAssociation # HasManyThroughAssociation + ThroughAssociation # HasOneAssociation + HasAssociation # HasOneThroughAssociation + ThroughAssociation # # Association proxies in Active Record are middlemen between the object that # holds the association, known as the @owner, and the actual associated # object, known as the @target. The kind of association any proxy is # about is available in @reflection. That's an instance of the class # ActiveRecord::Reflection::AssociationReflection. # # For example, given # # class Blog < ActiveRecord::Base # has_many :posts # end # # blog = Blog.find(:first) # # the association proxy in blog.posts has the object in +blog+ as # @owner, the collection of its posts as @target, and # the @reflection object represents a :has_many macro. # # This class has most of the basic instance methods removed, and delegates # unknown methods to @target via method_missing. As a # corner case, it even removes the +class+ method and that's why you get # # blog.posts.class # => Array # # though the object behind blog.posts is not an Array, but an # ActiveRecord::Associations::HasManyAssociation. # # The @target object is not \loaded until needed. For example, # # blog.posts.count # # is computed directly through SQL and does not trigger by itself the # instantiation of the actual post records. class AssociationProxy #:nodoc: alias_method :proxy_respond_to?, :respond_to? alias_method :proxy_extend, :extend delegate :to_param, :to => :proxy_target instance_methods.each { |m| undef_method m unless m.to_s =~ /^(?:nil\?|send|object_id|to_a)$|^__|^respond_to_missing|proxy_/ } def initialize(owner, reflection) @owner, @reflection = owner, reflection @updated = false reflection.check_validity! Array.wrap(reflection.options[:extend]).each { |ext| proxy_extend(ext) } reset construct_scope end # Returns the owner of the proxy. def proxy_owner @owner end # Returns the reflection object that represents the association handled # by the proxy. def proxy_reflection @reflection end # Returns the \target of the proxy, same as +target+. def proxy_target @target end # Does the proxy or its \target respond to +symbol+? def respond_to?(*args) proxy_respond_to?(*args) || (load_target && @target.respond_to?(*args)) end # Forwards === explicitly to the \target because the instance method # removal above doesn't catch it. Loads the \target if needed. def ===(other) load_target other === @target end # Returns the name of the table of the related class: # # post.comments.aliased_table_name # => "comments" # def aliased_table_name @reflection.klass.table_name end # Returns the SQL string that corresponds to the :conditions # option of the macro, if given, or +nil+ otherwise. def conditions @conditions ||= interpolate_sql(@reflection.sanitized_conditions) if @reflection.sanitized_conditions end alias :sql_conditions :conditions # Resets the \loaded flag to +false+ and sets the \target to +nil+. def reset @loaded = false @target = nil end # Reloads the \target and returns +self+ on success. def reload reset construct_scope load_target self unless @target.nil? end # Has the \target been already \loaded? def loaded? @loaded end # Asserts the \target has been loaded setting the \loaded flag to +true+. def loaded @loaded = true @stale_state = stale_state end # The target is stale if the target no longer points to the record(s) that the # relevant foreign_key(s) refers to. If stale, the association accessor method # on the owner will reload the target. It's up to subclasses to implement the # state_state method if relevant. # # Note that if the target has not been loaded, it is not considered stale. def stale_target? loaded? && @stale_state != stale_state end # Returns the target of this proxy, same as +proxy_target+. def target @target end # Sets the target of this proxy to \target, and the \loaded flag to +true+. def target=(target) @target = target loaded end # Forwards the call to the target. Loads the \target if needed. def inspect load_target @target.inspect end def send(method, *args) if proxy_respond_to?(method) super else load_target @target.send(method, *args) end end def scoped with_scope(@scope) { target_klass.scoped } end protected def interpolate_sql(sql, record = nil) @owner.send(:interpolate_sql, sql, record) end # Forwards the call to the reflection class. def sanitize_sql(sql, table_name = @reflection.klass.table_name) @reflection.klass.send(:sanitize_sql, sql, table_name) end # Merges into +options+ the ones coming from the reflection. def merge_options_from_reflection!(options) options.reverse_merge!( :group => @reflection.options[:group], :having => @reflection.options[:having], :limit => @reflection.options[:limit], :offset => @reflection.options[:offset], :joins => @reflection.options[:joins], :include => @reflection.options[:include], :select => @reflection.options[:select], :readonly => @reflection.options[:readonly] ) end # Forwards +with_scope+ to the reflection. def with_scope(*args, &block) target_klass.send :with_scope, *args, &block end # Construct the scope used for find/create queries on the target def construct_scope if target_klass @scope = { :find => construct_find_scope, :create => construct_create_scope } else @scope = nil end end # Implemented by subclasses def construct_find_scope raise NotImplementedError end # Implemented by (some) subclasses def construct_create_scope {} end def aliased_table target_klass.arel_table end # Set the inverse association, if possible def set_inverse_instance(record) if record && invertible_for?(record) record.send("set_#{inverse_reflection_for(record).name}_target", @owner) end end # This class of the target. belongs_to polymorphic overrides this to look at the # polymorphic_type field on the owner. def target_klass @reflection.klass end private # Forwards any missing method call to the \target. def method_missing(method, *args) if load_target unless @target.respond_to?(method) message = "undefined method `#{method.to_s}' for \"#{@target}\":#{@target.class.to_s}" raise NoMethodError, message end if block_given? @target.send(method, *args) { |*block_args| yield(*block_args) } else @target.send(method, *args) end end end # Loads the \target if needed and returns it. # # This method is abstract in the sense that it relies on +find_target+, # which is expected to be provided by descendants. # # If the \target is already \loaded it is just returned. Thus, you can call # +load_target+ unconditionally to get the \target. # # ActiveRecord::RecordNotFound is rescued within the method, and it is # not reraised. The proxy is \reset and +nil+ is the return value. def load_target return nil unless defined?(@loaded) if !loaded? && (!@owner.new_record? || foreign_key_present) && @scope @target = find_target end loaded @target rescue ActiveRecord::RecordNotFound reset end # Should be true if there is a foreign key present on the @owner which # references the target. This is used to determine whether we can load # the target if the @owner is currently a new record (and therefore # without a key). # # Currently implemented by belongs_to (vanilla and polymorphic) and # has_one/has_many :through associations which go through a belongs_to def foreign_key_present false end # Raises ActiveRecord::AssociationTypeMismatch unless +record+ is of # the kind of the class of the associated objects. Meant to be used as # a sanity check when you are about to assign an associated record. def raise_on_type_mismatch(record) unless record.is_a?(@reflection.klass) || record.is_a?(@reflection.class_name.constantize) message = "#{@reflection.class_name}(##{@reflection.klass.object_id}) expected, got #{record.class}(##{record.class.object_id})" raise ActiveRecord::AssociationTypeMismatch, message end end # Can be redefined by subclasses, notably polymorphic belongs_to # The record parameter is necessary to support polymorphic inverses as we must check for # the association in the specific class of the record. def inverse_reflection_for(record) @reflection.inverse_of end # Is this association invertible? Can be redefined by subclasses. def invertible_for?(record) inverse_reflection_for(record) end # This should be implemented to return the values of the relevant key(s) on the owner, # so that when state_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. def stale_state nil end end end end