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:
parent
1d9f26ed47
commit
1644663ba7
26 changed files with 392 additions and 502 deletions
|
@ -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:
|
||||
|
||||
|
|
|
@ -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 = {}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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
|
127
activerecord/lib/active_record/associations/collection_proxy.rb
Normal file
127
activerecord/lib/active_record/associations/collection_proxy.rb
Normal 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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -313,7 +313,7 @@ module ActiveRecord
|
|||
macro == :belongs_to
|
||||
end
|
||||
|
||||
def proxy_class
|
||||
def association_class
|
||||
case macro
|
||||
when :belongs_to
|
||||
if options[:polymorphic]
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue