diff options
Diffstat (limited to 'activerecord/lib/active_record/associations')
-rw-r--r-- | activerecord/lib/active_record/associations/association.rb (renamed from activerecord/lib/active_record/associations/association_proxy.rb) | 217 | ||||
-rw-r--r-- | activerecord/lib/active_record/associations/class_methods/join_dependency.rb | 14 | ||||
-rw-r--r-- | activerecord/lib/active_record/associations/collection_association.rb (renamed from activerecord/lib/active_record/associations/association_collection.rb) | 131 | ||||
-rw-r--r-- | activerecord/lib/active_record/associations/collection_proxy.rb | 127 | ||||
-rw-r--r-- | activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb | 36 | ||||
-rw-r--r-- | activerecord/lib/active_record/associations/has_many_association.rb | 12 | ||||
-rw-r--r-- | activerecord/lib/active_record/associations/has_many_through_association.rb | 20 | ||||
-rw-r--r-- | activerecord/lib/active_record/associations/has_one_through_association.rb | 2 | ||||
-rw-r--r-- | activerecord/lib/active_record/associations/singular_association.rb | 4 |
9 files changed, 287 insertions, 276 deletions
diff --git a/activerecord/lib/active_record/associations/association_proxy.rb b/activerecord/lib/active_record/associations/association.rb index fc03a4b619..2264631584 100644 --- a/activerecord/lib/active_record/associations/association_proxy.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -4,102 +4,33 @@ module ActiveRecord module Associations # = Active Record Associations # - # This is the root class of all association proxies ('+ Foo' signifies an included module Foo): + # This is the root class of all associations ('+ Foo' signifies an included module Foo): # - # AssociationProxy + # Association # SingularAssociaton # HasOneAssociation # HasOneThroughAssociation + ThroughAssociation # BelongsToAssociation # BelongsToPolymorphicAssociation - # AssociationCollection + # CollectionAssociation # HasAndBelongsToManyAssociation # HasManyAssociation # 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 - # object, known as the <tt>@target</tt>. The kind of association any proxy is - # about is available in <tt>@reflection</tt>. 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 <tt>blog.posts</tt> has the object in +blog+ as - # <tt>@owner</tt>, the collection of its posts as <tt>@target</tt>, and - # the <tt>@reflection</tt> object represents a <tt>:has_many</tt> macro. - # - # This class has most of the basic instance methods removed, and delegates - # unknown methods to <tt>@target</tt> via <tt>method_missing</tt>. As a - # corner case, it even removes the +class+ method and that's why you get - # - # blog.posts.class # => Array - # - # though the object behind <tt>blog.posts</tt> is not an Array, but an - # ActiveRecord::Associations::HasManyAssociation. - # - # The <tt>@target</tt> 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_extend, :extend + class Association #:nodoc: + attr_reader :owner, :target, :reflection - instance_methods.each { |m| undef_method m unless m.to_s =~ /^(?:nil\?|send|object_id|to_a)$|^__|^respond_to|proxy_/ } + delegate :options, :klass, :to => :reflection def initialize(owner, reflection) + reflection.check_validity! + @owner, @reflection = owner, reflection @updated = false - reflection.check_validity! - Array.wrap(reflection.options[:extend]).each { |ext| proxy_extend(ext) } + reset construct_scope end - def to_param - proxy_target.to_param - 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 - - # Does the proxy or its \target respond to +symbol+? - def 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) - other === load_target - end - # Returns the name of the table of the related class: # # post.comments.aliased_table_name # => "comments" @@ -143,97 +74,79 @@ module ActiveRecord 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+. + # Sets the target of this association to <tt>\target</tt>, 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.inspect - end - - def send(method, *args) - return super if respond_to?(method) - load_target.send(method, *args) - end - def scoped target_scope.merge(@association_scope) end - protected - - # 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 + # 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 - 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) + 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 - def aliased_table - target_klass.arel_table - 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) - inverse = record.send(:association_proxy, inverse_reflection_for(record).name) - inverse.target = @owner - end + # Set the inverse association, if possible + def set_inverse_instance(record) + if record && invertible_for?(record) + inverse = record.association(inverse_reflection_for(record).name) + inverse.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 + # 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 - # 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 + # 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. - # - # 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 - @target = find_target if find_target? - loaded! - target - rescue ActiveRecord::RecordNotFound - reset - 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 + @target = find_target if find_target? + loaded! + target + rescue ActiveRecord::RecordNotFound + reset + end private diff --git a/activerecord/lib/active_record/associations/class_methods/join_dependency.rb b/activerecord/lib/active_record/associations/class_methods/join_dependency.rb index fdd4fe8946..89503ccafa 100644 --- a/activerecord/lib/active_record/associations/class_methods/join_dependency.rb +++ b/activerecord/lib/active_record/associations/class_methods/join_dependency.rb @@ -209,10 +209,10 @@ module ActiveRecord association = join_part.instantiate(row) case macro when :has_many, :has_and_belongs_to_many - collection = record.send(join_part.reflection.name) - collection.loaded! - collection.target.push(association) - collection.send(:set_inverse_instance, association) + other = record.association(join_part.reflection.name) + other.loaded! + other.target.push(association) + other.set_inverse_instance(association) when :belongs_to set_target_and_inverse(join_part, association, record) else @@ -223,9 +223,9 @@ module ActiveRecord end def set_target_and_inverse(join_part, association, record) - association_proxy = record.send(:association_proxy, join_part.reflection.name) - association_proxy.target = association - association_proxy.send(:set_inverse_instance, association) + other = record.association(join_part.reflection.name) + other.target = association + other.set_inverse_instance(association) end end end diff --git a/activerecord/lib/active_record/associations/association_collection.rb b/activerecord/lib/active_record/associations/collection_association.rb index ca350f51c9..68631681e4 100644 --- a/activerecord/lib/active_record/associations/association_collection.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -17,8 +17,26 @@ module ActiveRecord # # If you need to work on all current children, new and existing records, # +load_target+ and the +loaded+ flag are your friends. - class AssociationCollection < AssociationProxy #:nodoc: - delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :to => :scoped + class CollectionAssociation < Association #:nodoc: + attr_reader :proxy + + def initialize(owner, reflection) + # When scopes are created via method_missing on the proxy, they are stored so that + # any records fetched from the database are kept around for future use. + @scopes_cache = Hash.new do |hash, method| + hash[method] = { } + end + + super + + @proxy = CollectionProxy.new(self) + end + + def reset + @loaded = false + @target = [] + @scopes_cache.clear + end def select(select = nil) if block_given? @@ -44,17 +62,6 @@ module ActiveRecord first_or_last(:last, *args) end - def to_ary - load_target.dup - end - alias_method :to_a, :to_ary - - def reset - @_scopes_cache = {} - @loaded = false - @target = [] - end - def build(attributes = {}, &block) build_or_create(attributes, :build, &block) end @@ -75,7 +82,7 @@ module ActiveRecord # Add +records+ to this association. Returns +self+ so method calls may be chained. # Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically. - def <<(*records) + def concat(*records) result = true load_target if @owner.new_record? @@ -88,12 +95,9 @@ module ActiveRecord end end - result && self + result && records end - alias_method :push, :<< - alias_method :concat, :<< - # Starts a transaction in the association class's database connection. # # class Author < ActiveRecord::Base @@ -119,13 +123,6 @@ module ActiveRecord end end - # Identical to delete_all, except that the return value is the association (for chaining) - # rather than the records which have been removed. - def clear - delete_all - self - end - # Destroy all the records from this association. # # See destroy for more info. @@ -254,7 +251,7 @@ module ActiveRecord end end - def uniq(collection = self) + def uniq(collection = load_target) seen = {} collection.find_all do |record| seen[record.id] = true unless seen.key?(record.id) @@ -291,70 +288,50 @@ module ActiveRecord end end - def respond_to?(method, include_private = false) - super || @reflection.klass.respond_to?(method, include_private) + def cached_scope(method, args) + @scopes_cache[method][args] ||= scoped.readonly(nil).send(method, *args) end - def method_missing(method, *args, &block) - match = DynamicFinderMatch.match(method) - if match && match.creator? - attributes = match.attribute_names - return send(:"find_by_#{attributes.join('_and_')}", *args) || create(Hash[attributes.zip(args)]) - end - - if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method)) - super - elsif @reflection.klass.scopes[method] - @_scopes_cache ||= {} - @_scopes_cache[method] ||= {} - @_scopes_cache[method][args] ||= scoped.readonly(nil).send(method, *args) - else - scoped.readonly(nil).send(method, *args, &block) - end + def association_scope + options = @reflection.options.slice(:order, :limit, :joins, :group, :having, :offset) + super.apply_finder_options(options) end - protected - - def association_scope - options = @reflection.options.slice(:order, :limit, :joins, :group, :having, :offset) - super.apply_finder_options(options) - end - - def load_target - if find_target? - targets = [] + def load_target + if find_target? + targets = [] - begin - targets = find_target - rescue ActiveRecord::RecordNotFound - reset - end - - @target = merge_target_lists(targets, @target) + begin + targets = find_target + rescue ActiveRecord::RecordNotFound + reset end - loaded! - target + @target = merge_target_lists(targets, @target) end - def add_to_target(record) - transaction do - callback(:before_add, record) - yield(record) if block_given? + loaded! + target + end - if @reflection.options[:uniq] && index = @target.index(record) - @target[index] = record - else - @target << record - end + def add_to_target(record) + transaction do + callback(:before_add, record) + yield(record) if block_given? - callback(:after_add, record) - set_inverse_instance(record) + if @reflection.options[:uniq] && index = @target.index(record) + @target[index] = record + else + @target << record end - record + callback(:after_add, record) + set_inverse_instance(record) end + record + end + private def select_value @@ -498,8 +475,8 @@ module ActiveRecord def include_in_memory?(record) if @reflection.is_a?(ActiveRecord::Reflection::ThroughReflection) - @owner.send(proxy_reflection.through_reflection.name).any? { |source| - target = source.send(proxy_reflection.source_reflection.name) + @owner.send(@reflection.through_reflection.name).any? { |source| + target = source.send(@reflection.source_reflection.name) target.respond_to?(:include?) ? target.include?(record) : target == record } || @target.include?(record) else diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb new file mode 100644 index 0000000000..cf77d770c9 --- /dev/null +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -0,0 +1,127 @@ +module ActiveRecord + module Associations + # Association proxies in Active Record are middlemen between the object that + # holds the association, known as the <tt>@owner</tt>, and the actual associated + # object, known as the <tt>@target</tt>. The kind of association any proxy is + # about is available in <tt>@reflection</tt>. 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 <tt>blog.posts</tt> has the object in +blog+ as + # <tt>@owner</tt>, the collection of its posts as <tt>@target</tt>, and + # the <tt>@reflection</tt> object represents a <tt>:has_many</tt> macro. + # + # This class has most of the basic instance methods removed, and delegates + # unknown methods to <tt>@target</tt> via <tt>method_missing</tt>. As a + # corner case, it even removes the +class+ method and that's why you get + # + # blog.posts.class # => Array + # + # though the object behind <tt>blog.posts</tt> is not an Array, but an + # ActiveRecord::Associations::HasManyAssociation. + # + # The <tt>@target</tt> 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 CollectionProxy # :nodoc: + alias :proxy_extend :extend + + instance_methods.each { |m| undef_method m unless m.to_s =~ /^(?:nil\?|send|object_id|to_a)$|^__|^respond_to|proxy_/ } + + delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from, + :lock, :readonly, :having, :to => :scoped + + delegate :target, :load_target, :loaded?, :scoped, + :to => :@association + + delegate :select, :find, :first, :last, + :build, :create, :create!, + :concat, :delete_all, :destroy_all, :delete, :destroy, :uniq, + :sum, :count, :size, :length, :empty?, + :any?, :many?, :include?, + :to => :@association + + def initialize(association) + @association = association + Array.wrap(association.options[:extend]).each { |ext| proxy_extend(ext) } + end + + def respond_to?(*args) + super || + (load_target && target.respond_to?(*args)) || + @association.klass.respond_to?(*args) + end + + def method_missing(method, *args, &block) + match = DynamicFinderMatch.match(method) + if match && match.creator? + attributes = match.attribute_names + return send(:"find_by_#{attributes.join('_and_')}", *args) || create(Hash[attributes.zip(args)]) + end + + if target.respond_to?(method) || (!@association.klass.respond_to?(method) && Class.respond_to?(method)) + if load_target + if target.respond_to?(method) + target.send(method, *args, &block) + else + begin + super + rescue NoMethodError => e + raise e, e.message.sub(/ for #<.*$/, " via proxy for #{target}") + end + end + end + + elsif @association.klass.scopes[method] + @association.cached_scope(method, args) + else + scoped.readonly(nil).send(method, *args, &block) + end + 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) + other === load_target + end + + def to_ary + load_target.dup + end + alias_method :to_a, :to_ary + + def <<(*records) + @association.concat(records) && self + end + alias_method :push, :<< + + def clear + delete_all + self + end + + def reload + @association.reload + self + end + + def new(*args, &block) + if @association.is_a?(HasManyThroughAssociation) + @association.build(*args, &block) + else + method_missing(:new, *args, &block) + end + end + end + end +end diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index b9c9919e7a..bcaea5ded4 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -1,7 +1,7 @@ module ActiveRecord # = Active Record Has And Belongs To Many Association module Associations - class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc: + class HasAndBelongsToManyAssociation < CollectionAssociation #:nodoc: attr_reader :join_table def initialize(owner, reflection) @@ -9,28 +9,26 @@ module ActiveRecord super end - protected + def insert_record(record, validate = true) + return if record.new_record? && !record.save(:validate => validate) - def insert_record(record, validate = true) - return if record.new_record? && !record.save(:validate => validate) + if @reflection.options[:insert_sql] + @owner.connection.insert(interpolate(@reflection.options[:insert_sql], record)) + else + stmt = join_table.compile_insert( + join_table[@reflection.foreign_key] => @owner.id, + join_table[@reflection.association_foreign_key] => record.id + ) - if @reflection.options[:insert_sql] - @owner.connection.insert(interpolate(@reflection.options[:insert_sql], record)) - else - stmt = join_table.compile_insert( - join_table[@reflection.foreign_key] => @owner.id, - join_table[@reflection.association_foreign_key] => record.id - ) - - @owner.connection.insert stmt.to_sql - end - - record + @owner.connection.insert stmt.to_sql end - def association_scope - super.joins(construct_joins) - end + record + end + + def association_scope + super.joins(construct_joins) + end private diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 543b073393..91565b247a 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -5,13 +5,11 @@ module ActiveRecord # # If the association has a <tt>:through</tt> option further specialization # is provided by its child HasManyThroughAssociation. - class HasManyAssociation < AssociationCollection #:nodoc: - protected - - def insert_record(record, validate = true) - set_owner_attributes(record) - record.save(:validate => validate) - end + class HasManyAssociation < CollectionAssociation #:nodoc: + def insert_record(record, validate = true) + set_owner_attributes(record) + record.save(:validate => validate) + end private diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index c4d3ef8fef..664c284d45 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -22,7 +22,7 @@ module ActiveRecord end end - def <<(*records) + def concat(*records) unless @owner.new_record? records.flatten.each do |record| raise_on_type_mismatch(record) @@ -33,19 +33,17 @@ module ActiveRecord super end - protected - - def insert_record(record, validate = true) - return if record.new_record? && !record.save(:validate => validate) - through_record(record).save! - update_counter(1) - record - end + def insert_record(record, validate = true) + return if record.new_record? && !record.save(:validate => validate) + through_record(record).save! + update_counter(1) + record + end private def through_record(record) - through_association = @owner.send(:association_proxy, @reflection.through_reflection.name) + through_association = @owner.association(@reflection.through_reflection.name) attributes = construct_join_attributes(record) through_record = Array.wrap(through_association.target).find { |candidate| @@ -95,7 +93,7 @@ module ActiveRecord end def delete_records(records, method) - through = @owner.send(:association_proxy, @reflection.through_reflection.name) + through = @owner.association(@reflection.through_reflection.name) scope = through.scoped.where(construct_join_attributes(*records)) case method diff --git a/activerecord/lib/active_record/associations/has_one_through_association.rb b/activerecord/lib/active_record/associations/has_one_through_association.rb index 149daf69f7..112b773ec4 100644 --- a/activerecord/lib/active_record/associations/has_one_through_association.rb +++ b/activerecord/lib/active_record/associations/has_one_through_association.rb @@ -12,7 +12,7 @@ module ActiveRecord private def create_through_record(record) - through_proxy = @owner.send(:association_proxy, @reflection.through_reflection.name) + through_proxy = @owner.association(@reflection.through_reflection.name) through_record = through_proxy.send(:load_target) if through_record && !record diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index 7f92d9712a..0aa647c63d 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -1,6 +1,6 @@ module ActiveRecord module Associations - class SingularAssociation < AssociationProxy #:nodoc: + class SingularAssociation < Association #:nodoc: def create(attributes = {}) new_record(:create, attributes) end @@ -29,7 +29,7 @@ module ActiveRecord end def check_record(record) - record = record.target if AssociationProxy === record + record = record.target if Association === record raise_on_type_mismatch(record) if record record end |