diff options
Diffstat (limited to 'activerecord/lib/active_record')
15 files changed, 361 insertions, 350 deletions
diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index 52d2c49836..a34a73cf5d 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -124,16 +124,16 @@ module ActiveRecord def add_preloaded_records_to_collection(parent_records, reflection_name, associated_record) parent_records.each do |parent_record| - association_proxy = parent_record.send(reflection_name) - association_proxy.loaded! - association_proxy.target.concat(Array.wrap(associated_record)) - association_proxy.send(:set_inverse_instance, associated_record) + association = parent_record.association(reflection_name) + association.loaded! + association.target.concat(Array.wrap(associated_record)) + association.set_inverse_instance(associated_record) end end def add_preloaded_record_to_collection(parent_records, reflection_name, associated_record) parent_records.each do |parent_record| - parent_record.send(:association_proxy, reflection_name).target = associated_record + parent_record.association(reflection_name).target = associated_record end end @@ -158,7 +158,7 @@ module ActiveRecord seen_keys[seen_key] = true mapped_records = id_to_record_map[seen_key] mapped_records.each do |mapped_record| - association_proxy = mapped_record.send(:association_proxy, reflection_name) + association_proxy = mapped_record.association(reflection_name) association_proxy.target = associated_record association_proxy.send(:set_inverse_instance, associated_record) end @@ -187,7 +187,7 @@ module ActiveRecord id_to_record_map = construct_id_map(records) - records.each { |record| record.send(reflection.name).loaded! } + records.each { |record| record.association(reflection.name).loaded! } options = reflection.options right = Arel::Table.new(options[:join_table]).alias('t0') @@ -233,7 +233,7 @@ module ActiveRecord end def preload_has_one_association(records, reflection, preload_options={}) - return if records.first.send(:association_proxy, reflection.name).loaded? + return if records.first.association(reflection.name).loaded? id_to_record_map = construct_id_map(records, reflection.options[:primary_key]) options = reflection.options @@ -268,7 +268,7 @@ module ActiveRecord foreign_key = reflection.through_reflection_foreign_key id_to_record_map = construct_id_map(records, foreign_key || reflection.options[:primary_key]) - records.each { |record| record.send(reflection.name).loaded! } + records.each { |record| record.association(reflection.name).loaded! } if options[:through] through_records = preload_through_records(records, reflection, options[:through]) @@ -298,7 +298,7 @@ module ActiveRecord # Dont cache the association - we would only be caching a subset records.map { |record| - proxy = record.send(through_association) + proxy = record.association(through_association) if proxy.respond_to?(:target) Array.wrap(proxy.target).tap { proxy.reset } @@ -320,7 +320,7 @@ module ActiveRecord end def preload_belongs_to_association(records, reflection, preload_options={}) - return if records.first.send(:association_proxy, reflection.name).loaded? + return if records.first.association(reflection.name).loaded? options = reflection.options klasses_and_ids = {} diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index bf0f6dc542..77110c721b 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -118,17 +118,19 @@ module ActiveRecord # These classes will be loaded when associations are created. # So there is no need to eager load them. - autoload :AssociationCollection, 'active_record/associations/association_collection' - autoload :SingularAssociation, 'active_record/associations/singular_association' - autoload :AssociationProxy, 'active_record/associations/association_proxy' - autoload :ThroughAssociation, 'active_record/associations/through_association' - autoload :BelongsToAssociation, 'active_record/associations/belongs_to_association' + autoload :Association, 'active_record/associations/association' + autoload :SingularAssociation, 'active_record/associations/singular_association' + autoload :CollectionAssociation, 'active_record/associations/collection_association' + autoload :CollectionProxy, 'active_record/associations/collection_proxy' + + autoload :BelongsToAssociation, 'active_record/associations/belongs_to_association' autoload :BelongsToPolymorphicAssociation, 'active_record/associations/belongs_to_polymorphic_association' - autoload :HasAndBelongsToManyAssociation, 'active_record/associations/has_and_belongs_to_many_association' - autoload :HasManyAssociation, 'active_record/associations/has_many_association' - autoload :HasManyThroughAssociation, 'active_record/associations/has_many_through_association' - autoload :HasOneAssociation, 'active_record/associations/has_one_association' - autoload :HasOneThroughAssociation, 'active_record/associations/has_one_through_association' + autoload :HasAndBelongsToManyAssociation, 'active_record/associations/has_and_belongs_to_many_association' + autoload :HasManyAssociation, 'active_record/associations/has_many_association' + autoload :HasManyThroughAssociation, 'active_record/associations/has_many_through_association' + autoload :HasOneAssociation, 'active_record/associations/has_one_association' + autoload :HasOneThroughAssociation, 'active_record/associations/has_one_through_association' + autoload :ThroughAssociation, 'active_record/associations/through_association' # Clears out the association cache. def clear_association_cache #:nodoc: @@ -138,29 +140,23 @@ module ActiveRecord # :nodoc: attr_reader :association_cache - protected - - # Returns the proxy for the given association name, instantiating it if it doesn't - # already exist - def association_proxy(name) - association = association_instance_get(name) - - if association.nil? - reflection = self.class.reflect_on_association(name) - association = reflection.proxy_class.new(self, reflection) - association_instance_set(name, association) - end + # Returns the association instance for the given name, instantiating it if it doesn't already exist + def association(name) #:nodoc: + association = association_instance_get(name) - association + if association.nil? + reflection = self.class.reflect_on_association(name) + association = reflection.association_class.new(self, reflection) + association_instance_set(name, association) end + association + end + private # Returns the specified association instance if it responds to :loaded?, nil otherwise. def association_instance_get(name) - if @association_cache.key? name - association = @association_cache[name] - association if association.respond_to?(:loaded?) - end + @association_cache[name] end # Set the specified association instance. @@ -1574,7 +1570,7 @@ module ActiveRecord def association_accessor_methods(reflection) redefine_method(reflection.name) do |*params| force_reload = params.first unless params.empty? - association = association_proxy(reflection.name) + association = association(reflection.name) if force_reload reflection.klass.uncached { association.reload } @@ -1582,18 +1578,18 @@ module ActiveRecord association.reload end - association.target.nil? ? nil : association + association.target end redefine_method("#{reflection.name}=") do |record| - association_proxy(reflection.name).replace(record) + association(reflection.name).replace(record) end end def collection_reader_method(reflection) redefine_method(reflection.name) do |*params| force_reload = params.first unless params.empty? - association = association_proxy(reflection.name) + association = association(reflection.name) if force_reload reflection.klass.uncached { association.reload } @@ -1601,7 +1597,7 @@ module ActiveRecord association.reload end - association + association.proxy end redefine_method("#{reflection.name.to_s.singularize}_ids") do @@ -1621,7 +1617,7 @@ module ActiveRecord if writer redefine_method("#{reflection.name}=") do |new_value| - association_proxy(reflection.name).replace(new_value) + association(reflection.name).replace(new_value) end redefine_method("#{reflection.name.to_s.singularize}_ids=") do |new_value| @@ -1643,7 +1639,7 @@ module ActiveRecord constructors.each do |name, proxy_name| redefine_method(name) do |*params| attributes = params.first unless params.empty? - association_proxy(reflection.name).send(proxy_name, attributes) + association(reflection.name).send(proxy_name, attributes) end end end 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 diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 95357ee5dc..72499ea5b8 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -227,7 +227,7 @@ module ActiveRecord # unless the parent is/was a new record itself. def associated_records_to_validate_or_save(association, new_record, autosave) if new_record - association + association && association.target elsif autosave association.target.find_all { |record| record.changed_for_autosave? } else @@ -247,9 +247,9 @@ module ActiveRecord # Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is # turned on for the association. def validate_single_association(reflection) - if (association = association_instance_get(reflection.name)) && !association.target.nil? - association_valid?(reflection, association) - end + association = association_instance_get(reflection.name) + record = association && association.target + association_valid?(reflection, record) if record end # Validate the associated records if <tt>:validate</tt> or @@ -266,12 +266,12 @@ module ActiveRecord # Returns whether or not the association is valid and applies any errors to # the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt> # enabled records if they're marked_for_destruction? or destroyed. - def association_valid?(reflection, association) - return true if association.destroyed? || association.marked_for_destruction? + def association_valid?(reflection, record) + return true if record.destroyed? || record.marked_for_destruction? - unless valid = association.valid? + unless valid = record.valid? if reflection.options[:autosave] - association.errors.each do |attribute, message| + record.errors.each do |attribute, message| attribute = "#{reflection.name}.#{attribute}" errors[attribute] << message errors[attribute].uniq! @@ -309,12 +309,12 @@ module ActiveRecord saved = true if autosave && record.marked_for_destruction? - association.destroy(record) + association.proxy.destroy(record) elsif autosave != false && (@new_record_before_save || record.new_record?) if autosave - saved = association.send(:insert_record, record, false) + saved = association.insert_record(record, false) else - association.send(:insert_record, record) + association.insert_record(record) end elsif autosave saved = record.save(:validate => false) @@ -338,16 +338,18 @@ module ActiveRecord # This all happens inside a transaction, _if_ the Transactions module is included into # ActiveRecord::Base after the AutosaveAssociation module, which it does by default. def save_has_one_association(reflection) - if (association = association_instance_get(reflection.name)) && !association.target.nil? && !association.destroyed? + association = association_instance_get(reflection.name) + record = association && association.load_target + if record && !record.destroyed? autosave = reflection.options[:autosave] - if autosave && association.marked_for_destruction? - association.destroy + if autosave && record.marked_for_destruction? + record.destroy else key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id - if autosave != false && (new_record? || association.new_record? || association[reflection.foreign_key] != key || autosave) - association[reflection.foreign_key] = key - saved = association.save(:validate => !autosave) + if autosave != false && (new_record? || record.new_record? || record[reflection.foreign_key] != key || autosave) + record[reflection.foreign_key] = key + saved = record.save(:validate => !autosave) raise ActiveRecord::Rollback if !saved && autosave saved end @@ -359,16 +361,18 @@ module ActiveRecord # # In addition, it will destroy the association if it was marked for destruction. def save_belongs_to_association(reflection) - if (association = association_instance_get(reflection.name)) && !association.destroyed? + association = association_instance_get(reflection.name) + record = association && association.load_target + if record && !record.destroyed? autosave = reflection.options[:autosave] - if autosave && association.marked_for_destruction? - association.destroy + if autosave && record.marked_for_destruction? + record.destroy elsif autosave != false - saved = association.save(:validate => !autosave) if association.new_record? || (autosave && association.changed_for_autosave?) + saved = record.save(:validate => !autosave) if record.new_record? || (autosave && record.changed_for_autosave?) if association.updated? - association_id = association.send(reflection.options[:primary_key] || :id) + association_id = record.send(reflection.options[:primary_key] || :id) self[reflection.foreign_key] = association_id association.loaded! end diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index fc68d7254a..9bbcf71603 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -387,13 +387,13 @@ module ActiveRecord end end - association = send(association_name) + association = association(association_name) existing_records = if association.loaded? - association.to_a + association.target else attribute_ids = attributes_collection.map {|a| a['id'] || a[:id] }.compact - attribute_ids.empty? ? [] : association.all(:conditions => {association.primary_key => attribute_ids}) + attribute_ids.empty? ? [] : association.scoped.where(association.klass.primary_key => attribute_ids) end attributes_collection.each do |attributes| @@ -408,12 +408,12 @@ module ActiveRecord unless association.loaded? || call_reject_if(association_name, attributes) # Make sure we are operating on the actual object which is in the association's # proxy_target array (either by finding it, or adding it if not found) - target_record = association.proxy_target.detect { |record| record == existing_record } + target_record = association.target.detect { |record| record == existing_record } if target_record existing_record = target_record else - association.send(:add_to_target, existing_record) + association.add_to_target(existing_record) end end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 0a0e8d650c..4093a1a209 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -313,7 +313,7 @@ module ActiveRecord macro == :belongs_to end - def proxy_class + def association_class case macro when :belongs_to if options[:polymorphic] diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 65852cb281..9633fd3d82 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -21,7 +21,7 @@ module ActiveRecord when ActiveRecord::Relation value.select_values = [value.klass.arel_table['id']] if value.select_values.empty? attribute.in(value.arel.ast) - when Array, ActiveRecord::Associations::AssociationCollection + when Array, ActiveRecord::Associations::CollectionProxy values = value.to_a.map { |x| x.is_a?(ActiveRecord::Base) ? x.id : x } |