1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00

Split AssociationProxy into an Association class (and subclasses) which manages the association, and a CollectionProxy class which is *only* a proxy. Singular associations no longer have a proxy. See CHANGELOG for more.

This commit is contained in:
Jon Leighton 2011-02-17 23:55:05 +00:00
parent 1d9f26ed47
commit 1644663ba7
26 changed files with 392 additions and 502 deletions

View file

@ -1,5 +1,17 @@
*Rails 3.1.0 (unreleased)*
* ActiveRecord::Associations::AssociationProxy has been split. There is now an Association class
(and subclasses) which are responsible for operating on associations, and then a separate,
thin wrapper called CollectionProxy, which proxies collection associations.
This prevents namespace pollution, separates concerns, and will allow further refactorings.
Singular associations (has_one, belongs_to) no longer have a proxy at all. They simply return
the associated record or nil. This means that you should not use undocumented methods such
as bob.mother.create - use bob.create_mother instead.
[Jon Leighton]
* Make has_many :through associations work correctly when you build a record and then save it. This
requires you to set the :inverse_of option on the source reflection on the join model, like so:

View file

@ -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 = {}

View file

@ -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 association instance for the given name, instantiating it if it doesn't already exist
def association(name) #:nodoc:
association = association_instance_get(name)
# 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
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

View file

@ -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
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 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
# 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
# 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
# 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

View file

@ -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

View file

@ -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 load_target
if find_target?
targets = []
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 = []
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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -313,7 +313,7 @@ module ActiveRecord
macro == :belongs_to
end
def proxy_class
def association_class
case macro
when :belongs_to
if options[:polymorphic]

View file

@ -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
}

View file

@ -1,52 +0,0 @@
require "cases/helper"
module ActiveRecord
module Associations
class AsssociationProxyTest < ActiveRecord::TestCase
class FakeOwner
attr_accessor :new_record
alias :new_record? :new_record
def initialize
@new_record = false
end
end
class FakeReflection < Struct.new(:options, :klass)
def initialize options = {}, klass = nil
super
end
def check_validity!
true
end
end
class FakeTarget
end
class FakeTargetProxy < AssociationProxy
def association_scope
true
end
def find_target
FakeTarget.new
end
end
def test_method_missing_error
reflection = FakeReflection.new({}, Object.new)
owner = FakeOwner.new
proxy = FakeTargetProxy.new(owner, reflection)
exception = assert_raises(NoMethodError) do
proxy.omg
end
assert_match('omg', exception.message)
assert_match(FakeTarget.name, exception.message)
end
end
end
end

View file

@ -50,11 +50,6 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_nothing_raised { account.firm = account.firm }
end
def test_triple_equality
assert Client.find(3).firm === Firm
assert Firm === Client.find(3).firm
end
def test_type_mismatch
assert_raise(ActiveRecord::AssociationTypeMismatch) { Account.find(1).firm = 1 }
assert_raise(ActiveRecord::AssociationTypeMismatch) { Account.find(1).firm = Project.find(1) }
@ -569,13 +564,15 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
def test_reloading_association_with_key_change
client = companies(:second_client)
firm = client.firm # note this is a proxy object
firm = client.association(:firm)
client.firm = companies(:another_firm)
assert_equal companies(:another_firm), firm.reload
firm.reload
assert_equal companies(:another_firm), firm.target
client.client_of = companies(:first_firm).id
assert_equal companies(:first_firm), firm.reload
firm.reload
assert_equal companies(:first_firm), firm.target
end
def test_polymorphic_counter_cache

View file

@ -66,11 +66,6 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert_nothing_raised { company.account = company.account }
end
def test_triple_equality
assert Account === companies(:first_firm).account
assert companies(:first_firm).account === Account
end
def test_type_mismatch
assert_raise(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).account = 1 }
assert_raise(ActiveRecord::AssociationTypeMismatch) { companies(:first_firm).account = Project.find(1) }
@ -320,7 +315,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
def test_creation_failure_without_dependent_option
pirate = pirates(:blackbeard)
orig_ship = pirate.ship.target
orig_ship = pirate.ship
assert_equal ships(:black_pearl), orig_ship
new_ship = pirate.create_ship
@ -333,7 +328,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
def test_creation_failure_with_dependent_option
pirate = pirates(:blackbeard).becomes(DestructivePirate)
orig_ship = pirate.dependent_ship.target
orig_ship = pirate.dependent_ship
new_ship = pirate.create_dependent_ship
assert new_ship.new_record?

View file

@ -139,7 +139,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
def test_assigning_association_correctly_assigns_target
new_member = Member.create(:name => "Chris")
new_member.club = new_club = Club.create(:name => "LRUG")
assert_equal new_club, new_member.club.target
assert_equal new_club, new_member.association(:club).target
end
def test_has_one_through_proxy_should_not_respond_to_private_methods
@ -197,7 +197,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
MemberDetail.find(:all, :include => :member_type)
end
@new_detail = @member_details[0]
assert @new_detail.send(:association_proxy, :member_type).loaded?
assert @new_detail.send(:association, :member_type).loaded?
assert_not_nil assert_no_queries { @new_detail.member_type }
end

View file

@ -137,7 +137,7 @@ class InverseHasOneTests < ActiveRecord::TestCase
def test_parent_instance_should_be_shared_with_newly_created_child_via_bang_method
m = Man.find(:first)
f = m.face.create!(:description => 'haunted')
f = m.create_face!(:description => 'haunted')
assert_not_nil f.man
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
m.name = 'Bongo'
@ -158,18 +158,6 @@ class InverseHasOneTests < ActiveRecord::TestCase
assert_equal m.name, f.man.name, "Name of man should be the same after changes to replaced-child-owned instance"
end
def test_parent_instance_should_be_shared_with_replaced_via_method_child
m = Man.find(:first)
f = Face.new(:description => 'haunted')
m.face.replace(f)
assert_not_nil f.man
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
m.name = 'Bongo'
assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
f.man.name = 'Mungo'
assert_equal m.name, f.man.name, "Name of man should be the same after changes to replaced-child-owned instance"
end
def test_trying_to_use_inverses_that_dont_exist_should_raise_an_error
assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Man.find(:first).dirty_face }
end
@ -271,18 +259,6 @@ class InverseHasManyTests < ActiveRecord::TestCase
assert_equal m.name, i.man.name, "Name of man should be the same after changes to replaced-child-owned instance"
end
def test_parent_instance_should_be_shared_with_replaced_via_method_children
m = Man.find(:first)
i = Interest.new(:topic => 'Industrial Revolution Re-enactment')
m.interests.replace([i])
assert_not_nil i.man
assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
m.name = 'Bongo'
assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
i.man.name = 'Mungo'
assert_equal m.name, i.man.name, "Name of man should be the same after changes to replaced-child-owned instance"
end
def test_trying_to_use_inverses_that_dont_exist_should_raise_an_error
assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Man.find(:first).secret_interests }
end
@ -366,19 +342,6 @@ class InverseBelongsToTests < ActiveRecord::TestCase
assert_equal f.description, m.face.description, "Description of face should be the same after changes to replaced-parent-owned instance"
end
def test_child_instance_should_be_shared_with_replaced_via_method_parent
f = faces(:trusting)
assert_not_nil f.man
m = Man.new(:name => 'Charles')
f.man.replace(m)
assert_not_nil m.face
assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance"
f.description = 'gormless'
assert_equal f.description, m.face.description, "Description of face should be the same after changes to child instance"
m.face.description = 'pleasing'
assert_equal f.description, m.face.description, "Description of face should be the same after changes to replaced-parent-owned instance"
end
def test_trying_to_use_inverses_that_dont_exist_should_raise_an_error
assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.find(:first).horrible_man }
end
@ -434,7 +397,7 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase
new_man = Man.new
assert_not_nil face.polymorphic_man
face.polymorphic_man.replace(new_man)
face.polymorphic_man = new_man
assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same before changes to parent instance"
face.description = 'Bongo'

View file

@ -153,7 +153,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
def test_create_polymorphic_has_one_with_scope
old_count = Tagging.count
tagging = posts(:welcome).tagging.create(:tag => tags(:misc))
tagging = posts(:welcome).create_tagging(:tag => tags(:misc))
assert_equal "Post", tagging.taggable_type
assert_equal old_count+1, Tagging.count
end

View file

@ -133,25 +133,6 @@ end
class AssociationProxyTest < ActiveRecord::TestCase
fixtures :authors, :posts, :categorizations, :categories, :developers, :projects, :developers_projects
def test_proxy_accessors
welcome = posts(:welcome)
assert_equal welcome, welcome.author.proxy_owner
assert_equal welcome.class.reflect_on_association(:author), welcome.author.proxy_reflection
welcome.author.class # force load target
assert_equal welcome.author, welcome.author.proxy_target
david = authors(:david)
assert_equal david, david.posts.proxy_owner
assert_equal david.class.reflect_on_association(:posts), david.posts.proxy_reflection
david.posts.class # force load target
assert_equal david.posts, david.posts.proxy_target
assert_equal david, david.posts_with_extension.testing_proxy_owner
assert_equal david.class.reflect_on_association(:posts_with_extension), david.posts_with_extension.testing_proxy_reflection
david.posts_with_extension.class # force load target
assert_equal david.posts_with_extension, david.posts_with_extension.testing_proxy_target
end
def test_push_does_not_load_target
david = authors(:david)
@ -216,16 +197,6 @@ class AssociationProxyTest < ActiveRecord::TestCase
assert_equal post.body, "More cool stuff!"
end
def test_failed_reload_returns_nil
p = setup_dangling_association
assert_nil p.author.reload
end
def test_failed_reset_returns_nil
p = setup_dangling_association
assert_nil p.author.reset
end
def test_reload_returns_assocition
david = developers(:david)
assert_nothing_raised do
@ -240,13 +211,6 @@ class AssociationProxyTest < ActiveRecord::TestCase
[*author]
end
end
def setup_dangling_association
josh = Author.create(:name => "Josh")
p = Post.create(:title => "New on Edge", :body => "More cool stuff!", :author => josh)
josh.destroy
p
end
end
class OverridingAssociationsTest < ActiveRecord::TestCase

View file

@ -112,7 +112,7 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas
def test_build_before_child_saved
firm = Firm.find(1)
account = firm.account.build("credit_limit" => 1000)
account = firm.build_account("credit_limit" => 1000)
assert_equal account, firm.account
assert !account.persisted?
assert firm.save

View file

@ -448,7 +448,7 @@ class NamedScopeTest < ActiveRecord::TestCase
[:destroy_all, :reset, :delete_all].each do |method|
before = post.comments.containing_the_letter_e
post.comments.send(method)
post.association(:comments).send(method)
assert before.object_id != post.comments.containing_the_letter_e.object_id, "AssociationCollection##{method} should reset the named scopes cache"
end
end

View file

@ -155,7 +155,7 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase
man = Man.find man.id
man.interests_attributes = [{:id => interest.id, :topic => 'gardening'}]
assert_equal man.interests.first.topic, man.interests[0].topic
end
end
end
class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
@ -918,16 +918,16 @@ class TestHasManyAutosaveAssociationWhichItselfHasAutosaveAssociations < ActiveR
test "if association is not loaded and association record is saved and then in memory record attributes should be saved" do
@ship.parts_attributes=[{:id => @part.id,:name =>'Deck'}]
assert_equal 1, @ship.parts.proxy_target.size
assert_equal 1, @ship.association(:parts).target.size
assert_equal 'Deck', @ship.parts[0].name
end
test "if association is not loaded and child doesn't change and I am saving a grandchild then in memory record should be used" do
@ship.parts_attributes=[{:id => @part.id,:trinkets_attributes =>[{:id => @trinket.id, :name => 'Ruby'}]}]
assert_equal 1, @ship.parts.proxy_target.size
assert_equal 1, @ship.association(:parts).target.size
assert_equal 'Mast', @ship.parts[0].name
assert_no_difference("@ship.parts[0].trinkets.proxy_target.size") do
@ship.parts[0].trinkets.proxy_target.size
assert_no_difference("@ship.parts[0].association(:trinkets).target.size") do
@ship.parts[0].association(:trinkets).target.size
end
assert_equal 'Ruby', @ship.parts[0].trinkets[0].name
@ship.save