Reputation: 571
I have 3 models that are set up like this:
Class A
has_many class_b dependent :destroy
has_many class_c dependent :destroy
end
Class B
belongs_to class_a
has_many class_c through class_a conditions: proc {<conditions>} dependent :destroy
end
Class C
belongs to class_a
end
conditions: only a subset of the class_c objects that belong to class_a also belong
to class_b. class_c has a column that is essentially class_b_id, so only those
instances will be deleted.
I'm trying to destroy an instance of class_b, but I get this error:
ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection (Cannot modify 'association ClassB#class_c' because the source reflection class 'ClassC' is associated to 'ClassA' via :has_many.)
What can I do to fix this error? Do I have to rework my associations?
Upvotes: 2
Views: 1819
Reputation: 571
I didn't want to change the fundamental associations in my codebase so what I ended up doing is removing the dependent: destroy
from class_b
's association with class_c
and just writing a custom query in the before_destroy
of class_b
Upvotes: 1
Reputation: 953
Honestly, your models configuration is very cont-intuitive. You have parent class A which has may B's and C's. Then, for some reason, you want that deletion of any B (which may be many for a single A) would go to its parent A and delete its all C's (of that parent A). Then, for example, another child B of the same parent A would eventually lose all it's C's by doing nothing.
C's don't look like dependent in such case, structurally they're on the same level as B. And as described, this could lead to some unexpected things - imagine that in parallel you have work with the second B and its C's, while suddenly they all disappear because of the first B.
Providing real names and logic for class associations would make more sense on how to better design relations - but they definitely needs to be changed
Update. has_one or has_many association of the inner (connecting table) can't be used for making through association. So your class_A must belong_to class_C (target class) so Class_B can has_many it through class_A.
Look at Rails sources for through connection implementation, there is a check there for source_reflection - this is connection of the middle (joining) class to the destination (target) one.
def ensure_mutable
unless source_reflection.belongs_to? #interim connection must be belongs_to or it fails
if reflection.has_one?
raise HasOneThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection)
else
raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection)
end
end
end
You can also check here
Meanwhile, you also can't have belong_to through - see here - see accepted answer
Update 2 - Long-long story reflecting on Rails sources.
First, ActiveRecord::Base class (which you inherit all your models from) includes Associations module (not class!) here:
include Associations
Then, Associations module by using Concerns will add several class methods to the Base class. Specifically, it adds has_many method to your model that you use to build association. This method creates instance of Builder::HasMany class, and passes self (which is reference to class B in your case) and name (which is :class_c symbol), plus all your supporting options, including option[:through] equal to Class_A. Look in sources
def has_many(name, scope = nil, options = {}, &extension)
reflection = Builder::HasMany.build(self, name, scope, options, &extension)
Reflection.add_reflection self, name, reflection
end
Technically, HasMany is a small class on top of CollectionAssociation class which in its order lays on Association class, all inside Builder:: name-scope (note it's all now classes, different from parent Association module). HasMany
module ActiveRecord::Associations::Builder # :nodoc:
class HasMany < CollectionAssociation #:nodoc:
def self.macro
:has_many
end
Inheritance of CollectionAssociation
module ActiveRecord::Associations::Builder # :nodoc:
class CollectionAssociation < Association #:nodoc:
Now, Association class has that build method used above to create has_many reflection in has_many method call above. Let's look at sources. Notice, that it creates a Reflection using create method:
module ActiveRecord::Associations::Builder # :nodoc:
class Association #:nodoc:
def self.build(model, name, scope, options, &block)
..
reflection = create_reflection model, name, scope, options, extension
..
reflection
end
def self.create_reflection(model, name, scope, options, extension = nil)
..
ActiveRecord::Reflection.create(macro, name, scope, options, model)
end
...
Notice, that in your case following parameters are passed: macro would be ":has_many" symbol, name (class_c) will be the first parameter, model was self (class B). It will also have your options[:through] = class_a inside options.
Now let's look at Reflection module create method that was used. BTW, Reflection module is also included into ActiveRecord::Base class, but is references through namespace to avoid naming confusion for .create call. This method will create one of the specific type Reflecton subclasses. sources
module ActiveRecord
# = Active Record Reflection
module Reflection # :nodoc:
extend ActiveSupport::Concern
def self.create(macro, name, scope, options, ar)
klass = case macro
when :composed_of
AggregateReflection
when :has_many
HasManyReflection
when :has_one
HasOneReflection
when :belongs_to
BelongsToReflection
else
raise "Unsupported Macro: #{macro}"
end
reflection = klass.new(name, scope, options, ar)
options[:through] ? ThroughReflection.new(reflection) : reflection
end
In your case, it will create ThroughReflection.new (because of presence of options[:through]) class instance with inner HasManyReflection instance (because of :has_many value of macro). Inner HasManyReflection will have all the original parameters inside it - name of class c, and ar (seems to mean active_record) is model which was originally set to self (class b), and also a through option as well.
ThroughReflection class is also defined inside same Reflection module here.
# Holds all the meta-data about a :through association as it was specified
# in the Active Record class.
class ThroughReflection < AbstractReflection #:nodoc:
It has the implementation of source_reflection method, which will be used in ensure_mutable call (line 736).
# Returns the source of the through reflection. It checks both a singularized
# and pluralized form for <tt>:belongs_to</tt> or <tt>:has_many</tt>.
#
# class Post < ActiveRecord::Base
# has_many :taggings
# has_many :tags, through: :taggings
# end
#
# class Tagging < ActiveRecord::Base
# belongs_to :post
# belongs_to :tag
# end
#
# tags_reflection = Post.reflect_on_association(:tags)
# tags_reflection.source_reflection
# # => <ActiveRecord::Reflection::BelongsToReflection: @name=:tag, @active_record=Tagging, @plural_name="tags">
#
def source_reflection
through_reflection.klass._reflect_on_association(source_reflection_name)
end
In your case, source_reflection call will return value of HasManyReflection type as Class A has_many of Class C. Look through the provided comments for method to understand what it does.
See also through_reflection definition on the next lines - it checks for the association you use through to get to the needed class - it's your association from class_b to class_a (class_b belongs to class_a). Then it will check for association from the interim class (class_a) to the final class (class_c) in source_reflection above.
# Returns the AssociationReflection object specified in the <tt>:through</tt> option
# of a HasManyThrough or HasOneThrough association.
#
# class Post < ActiveRecord::Base
# has_many :taggings
# has_many :tags, through: :taggings
# end
#
# tags_reflection = Post.reflect_on_association(:tags)
# tags_reflection.through_reflection
# # => <ActiveRecord::Reflection::HasManyReflection: @name=:taggings, @active_record=Post, @plural_name="taggings">
#
def through_reflection
active_record._reflect_on_association(options[:through])
end
-- Finally--
Inner HasManyReflection instance of ThroughReflection will return HasManyThroughAssociation class in association_class method - here
class HasManyReflection < AssociationReflection # :nodoc:
def association_class
if options[:through]
Associations::HasManyThroughAssociation
else
Associations::HasManyAssociation
end
end
end
That returned association_class name is used within Associations module (again, not class), which as said long above, is included in ActiveRecord::Base. It's used in order to to create an association (and save it to your class_b model) - will be of HasManyThroughAssociation class in your case. HasManyThroughAssociation instance will also have that ThroughReflection inside it passed here in new (see reflection parameter). sources
def association(name) #:nodoc:
if association.nil? # if was not created before
unless reflection = self.class._reflect_on_association(name)
raise AssociationNotFoundError.new(self, name)
end
association = reflection.association_class.new(self, reflection)
association_instance_set(name, association)
end
association
end
_reflect_on_association(name) here loads the previously created reflection (of ThroughReflection) from the local "storage" of the model (class_b) by its name (class_a in your case). So for "class_a" name parameter it has that ThroughReflection as you wrote has_many :class_a.
Also here for the constructor call:
class HasManyThroughAssociation < HasManyAssociation #:nodoc:
include ThroughAssociation
def initialize(owner, reflection)
super
@through_records = {}
@through_association = nil
end
Just in case, here is the constructor call that will eventually be called by super here:
class Association #:nodoc:
delegate :options, :to => :reflection
def initialize(owner, reflection)
@owner, @reflection = owner, reflection
...
So it saves passed reflection (which was ThroughReflection).
Now, let's take a look at that HasManyThroughAssociation class that was created and has ThroughReflection inside - sources. It actually includes reusable portion of ThroughAssociation module:
include ThroughAssociation
It also delegates source_reflection and through_reflection calls to it's saved reflection variable - here. As you remember, reflection would be of ThroughReflection class.
delegate :source_reflection, :through_reflection, :to => :reflection
Included ThroughAssociation, in its case, has that ensure_mutable call that raise exception - here
def ensure_mutable
unless source_reflection.belongs_to?
if reflection.has_one?
raise HasOneThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection)
else
raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection)
end
end
end
source_reflection call is delegated to saved reflection, which is ThroughReflection And as said above (look for above source_reflection method definition in ThroughReflection), your source_reflection for your ThroughReflection will be of has_many type, as Class A has_many of Class C. So as it's not belongs_to (interim class a not belongs to the target class c).
General comments from the code:
# Construct attributes for :through pointing to owner and associate. This is used by the
# methods which create and delete records on the association.
#
# We only support indirectly modifying through associations which have a belongs_to source.
# This is the "has_many :tags, through: :taggings" situation, where the join model
# typically has a belongs_to on both side. In other words, associations which could also
# be represented as has_and_belongs_to_many associations.
#
# We do not support creating/deleting records on the association where the source has
# some other type, because this opens up a whole can of worms, and in basically any
# situation it is more natural for the user to just create or modify their join records
# directly as required.
Upvotes: 3