diff options
Diffstat (limited to 'activerecord/lib/active_record/associations/association_proxy.rb')
-rw-r--r-- | activerecord/lib/active_record/associations/association_proxy.rb | 312 |
1 files changed, 169 insertions, 143 deletions
diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association_proxy.rb index 53ec5a0da6..d16cda5585 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association_proxy.rb @@ -4,17 +4,18 @@ module ActiveRecord module Associations # = Active Record Associations # - # This is the root class of all association proxies: + # This is the root class of all association proxies ('+ Foo' signifies an included module Foo): # # AssociationProxy - # BelongsToAssociation + # SingularAssociaton # HasOneAssociation - # BelongsToPolymorphicAssociation + # HasOneThroughAssociation + ThroughAssociation + # BelongsToAssociation + # BelongsToPolymorphicAssociation # AssociationCollection # HasAndBelongsToManyAssociation # HasManyAssociation - # HasManyThroughAssociation - # HasOneThroughAssociation + # HasManyThroughAssociation + ThroughAssociation # # Association proxies in Active Record are middlemen between the object that # holds the association, known as the <tt>@owner</tt>, and the actual associated @@ -50,10 +51,9 @@ module ActiveRecord # 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_/ } + + instance_methods.each { |m| undef_method m unless m.to_s =~ /^(?:nil\?|send|object_id|to_a)$|^__|^respond_to|proxy_/ } def initialize(owner, reflection) @owner, @reflection = owner, reflection @@ -64,6 +64,10 @@ module ActiveRecord construct_scope end + def to_param + proxy_target.to_param + end + # Returns the owner of the proxy. def proxy_owner @owner @@ -75,21 +79,25 @@ module ActiveRecord @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)) + super || (load_target && @target.respond_to?(*args)) + end + + # Forwards any missing method call to the \target. + def method_missing(method, *args, &block) + if load_target + return super unless @target.respond_to?(method) + @target.send(method, *args, &block) + end + rescue NoMethodError => e + raise e, e.message.sub(/ for #<.*$/, " via proxy for #{@target}") end # Forwards <tt>===</tt> 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 + other === load_target end # Returns the name of the table of the related class: @@ -100,13 +108,6 @@ module ActiveRecord @reflection.klass.table_name end - # Returns the SQL string that corresponds to the <tt>:conditions</tt> - # 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 @@ -117,6 +118,7 @@ module ActiveRecord # Reloads the \target and returns +self+ on success. def reload reset + construct_scope load_target self unless @target.nil? end @@ -127,117 +129,93 @@ module ActiveRecord end # Asserts the \target has been loaded setting the \loaded flag to +true+. - def loaded - @loaded = true + def loaded! + @loaded = true + @stale_state = stale_state end - # Returns the target of this proxy, same as +proxy_target+. - def target - @target + # 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+. + attr_reader :target + + # Returns the \target of the proxy, same as +target+. + alias :proxy_target :target + # Sets the target of this proxy to <tt>\target</tt>, and the \loaded flag to +true+. def target=(target) @target = target - loaded + loaded! end # Forwards the call to the target. Loads the \target if needed. def inspect - load_target - @target.inspect + load_target.inspect end def send(method, *args) - if proxy_respond_to?(method) - super - else - load_target - @target.send(method, *args) - end + return super if respond_to?(method) + load_target.send(method, *args) end - protected - # Does the association have a <tt>:dependent</tt> option? - def dependent? - @reflection.options[:dependent] - end + def scoped + target_scope.merge(@association_scope) + end - def interpolate_sql(sql, record = nil) - @owner.send(:interpolate_sql, sql, record) - end + protected - # 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) + # Construct the scope for this association. + # + # Note that the association_scope is merged into the targed_scope only when the + # scoped method is called. This is because at that point the call may be surrounded + # by scope.scoping { ... } or with_scope { ... } etc, which affects the scope which + # actually gets built. + def construct_scope + @association_scope = association_scope if target_klass end - # Assigns the ID of the owner to the corresponding foreign key in +record+. - # If the association is polymorphic the type of the owner is also set. - def set_belongs_to_association_for(record) - if @reflection.options[:as] - record["#{@reflection.options[:as]}_id"] = @owner.id if @owner.persisted? - record["#{@reflection.options[:as]}_type"] = @owner.class.base_class.name.to_s - else - if @owner.persisted? - primary_key = @reflection.options[:primary_key] || :id - record[@reflection.primary_key_name] = @owner.send(primary_key) - end + def association_scope + scope = target_klass.unscoped + scope = scope.create_with(creation_attributes) + scope = scope.apply_finder_options(@reflection.options.slice(:readonly, :include)) + scope = scope.where(interpolate(@reflection.options[:conditions])) + if select = select_value + scope = scope.select(select) end + scope = scope.extending(*Array.wrap(@reflection.options[:extend])) + scope.where(construct_owner_conditions) 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] - ) + def aliased_table + target_klass.arel_table end - # Forwards +with_scope+ to the reflection. - def with_scope(*args, &block) - @reflection.klass.send :with_scope, *args, &block - end - - # Construct the scope used for find/create queries on the target - def construct_scope - @scope = { - :find => construct_find_scope, - :create => construct_create_scope - } + # Set the inverse association, if possible + def set_inverse_instance(record) + if record && invertible_for?(record) + inverse = record.send(:association_proxy, inverse_reflection_for(record).name) + inverse.target = @owner + end end - # Implemented by subclasses - def construct_find_scope - raise NotImplementedError + # 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 - # Implemented by (some) subclasses - def construct_create_scope - {} - 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 + # Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the + # through association's scope) + def target_scope + target_klass.scoped end # Loads the \target if needed and returns it. @@ -251,26 +229,84 @@ module ActiveRecord # 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) - if IdentityMap.enabled? && association_class - @target = IdentityMap.get(association_class, @owner[@reflection.association_foreign_key]) + if find_target? + begin + if IdentityMap.enabled? && association_class && association_class.respond_to?(:base_class) + @target = IdentityMap.get(association_class, @owner[@reflection.foreign_key]) + end + rescue NameError + nil + ensure + @target ||= find_target end - @target ||= find_target end - - @loaded = true - @target + loaded! + target rescue ActiveRecord::RecordNotFound reset end - # Can be overwritten by associations that might have the foreign key - # available for an association without having the object itself (and - # still being a new record). Currently, only +belongs_to+ presents - # this scenario (both vanilla and polymorphic). - def foreign_key_present + private + + def find_target? + !loaded? && (!@owner.new_record? || foreign_key_present?) && target_klass + end + + def interpolate(sql, record = nil) + if sql.respond_to?(:to_proc) + @owner.send(:instance_exec, record, &sql) + else + sql + end + end + + def select_value + @reflection.options[:select] + end + + # Implemented by (some) subclasses + def creation_attributes + { } + end + + # Returns a hash linking the owner to the association represented by the reflection + def construct_owner_attributes(reflection = @reflection) + attributes = {} + if reflection.macro == :belongs_to + attributes[reflection.association_primary_key] = @owner[reflection.foreign_key] + else + attributes[reflection.foreign_key] = @owner[reflection.active_record_primary_key] + + if reflection.options[:as] + attributes["#{reflection.options[:as]}_type"] = @owner.class.base_class.name + end + end + attributes + end + + # Builds an array of arel nodes from the owner attributes hash + def construct_owner_conditions(table = aliased_table, reflection = @reflection) + conditions = construct_owner_attributes(reflection).map do |attr, value| + table[attr].eq(value) + end + table.create_and(conditions) + end + + # Sets the owner attributes on the given record + def set_owner_attributes(record) + if @owner.persisted? + construct_owner_attributes.each { |key, value| record[key] = value } + end + 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 @@ -284,34 +320,24 @@ module ActiveRecord end end - if RUBY_VERSION < '1.9.2' - # Array#flatten has problems with recursive arrays before Ruby 1.9.2. - # Going one level deeper solves the majority of the problems. - def flatten_deeper(array) - array.collect { |element| (element.respond_to?(:flatten) && !element.is_a?(Hash)) ? element.flatten : element }.flatten - end - else - def flatten_deeper(array) - array.flatten - 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 - # Returns the ID of the owner, quoted if needed. - def owner_quoted_id - @owner.quoted_id + # Is this association invertible? Can be redefined by subclasses. + def invertible_for?(record) + inverse_reflection_for(record) end - def set_inverse_instance(record, instance) - return if record.nil? || !we_can_set_the_inverse_on_this?(record) - inverse_relationship = @reflection.inverse_of - unless inverse_relationship.nil? - record.send(:"set_#{inverse_relationship.name}_target", instance) - end - end - - # Override in subclasses - def we_can_set_the_inverse_on_this?(record) - false + # 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 end def association_class |