Flexible model associations

I am having trouble trying to figure out how to set up this many to many
relationship. I have 4 models (ModelA, ModelB, ModelC, ModelD). A user
can create any number of these models and they can relate instances of
these models to each other in a many to many relationship. So a ModelA
record can be associated with another ModelA record, a ModelB record can
be associated with a ModelB and a ModelC record, etc…

My original idea was to create a model_relationship table that had a
structure such as:

t.int object1_id
t.string object1_type
t.int object2_id
t.string object2_type

I am having trouble figuring out how to set this up as a Model though,
especially since you can’t always know if the object you are looking at
is object1 or object2 in the relationship field.

Does anyone have any advice for this type of problem?

Matthew Shapiro wrote:

I am having trouble trying to figure out how to set up this many to many
relationship. I have 4 models (ModelA, ModelB, ModelC, ModelD). A user
can create any number of these models and they can relate instances of
these models to each other in a many to many relationship. So a ModelA
record can be associated with another ModelA record, a ModelB record can
be associated with a ModelB and a ModelC record, etc…

My original idea was to create a model_relationship table that had a
structure such as:

t.int object1_id
t.string object1_type
t.int object2_id
t.string object2_type

That looks good so far; you’ll want to make both associations
polymorphic.

I am having trouble figuring out how to set this up as a Model though,
especially since you can’t always know if the object you are looking at
is object1 or object2 in the relationship field.

Does anyone have any advice for this type of problem?

Are the associations one-way or two-way? In other words, if ObjA is
related to ObjB, is ObjB automatically related to ObjA?

Marnen Laibow-Koser wrote:

I am having trouble figuring out how to set this up as a Model though,
especially since you can’t always know if the object you are looking at
is object1 or object2 in the relationship field.

Does anyone have any advice for this type of problem?

Are the associations one-way or two-way? In other words, if ObjA is
related to ObjB, is ObjB automatically related to ObjA?

It is two-way (If A is related to B, B is also automatically related to
A)

Matthew Shapiro wrote:

Marnen Laibow-Koser wrote:

I am having trouble figuring out how to set this up as a Model though,
especially since you can’t always know if the object you are looking at
is object1 or object2 in the relationship field.

Does anyone have any advice for this type of problem?

Are the associations one-way or two-way? In other words, if ObjA is
related to ObjB, is ObjB automatically related to ObjA?

It is two-way (If A is related to B, B is also automatically related to
A)

So you have an arbitrary graph. Normally I’d say that you just want to
do the object1_id and object2_id stuff you already thought of, and just
sort the two objects unambiguously. But that won’t really give you the
ability to do @myobject.show_all_related_objects without looking for it
in both object1 and object2. Two ideas come to mind:

1 (probably less good). Create relationship records in both orders:
| object1_id | object2_id |
| 1 | 2 |
| 2 | 1 |
Of course, this stores everything twice, with all attendant problems.

2 (probably the better idea). A bit more complex, but easier to
traverse. You may need the nested_has_many_through plugin (or
:finder_sql) for this to work correctly.

class NodeMembership < AR::B # join model since Rails won’t do
polymorphic habtm
belongs_to :node, :polymorphic => true
belongs_to :relationship
end

class Relationship < AR::B
has_many :node_memberships
has_many :nodes, :through => :node_memberships # may also need
:polymorphic => true – not sure
end

class Model[A,B,C…] < AR::B

you will probably want to refactor this into an abstract base class

has_many :node_memberships, :as => :node
has_many :relationships, :through => :node_memberships
end

Then, to find all relationships that a model participates in, you can
just do @model_a.relationships, which will translate into something like
SELECT r.*
FROM node_memberships nm LEFT JOIN relationships r ON (r.id =
nm.relationship_id)
WHERE nm.node_id = #{@model_a.id} AND nm.node_type = ‘ModelA’

I hope that’s clear…

Best,

Marnen Laibow-Koser
[email protected]
http://www.marnen.org

Marnen Laibow-Koser wrote:

2 (probably the better idea). A bit more complex, but easier to
traverse. You may need the nested_has_many_through plugin (or
:finder_sql) for this to work correctly.

Excellent. That looks like it would work perfectly actually.

Thanks! :slight_smile:

Marnen Laibow-Koser wrote:

class NodeMembership < AR::B # join model since Rails won’t do
polymorphic habtm
belongs_to :node, :polymorphic => true
belongs_to :relationship
end

class Relationship < AR::B
has_many :node_memberships
has_many :nodes, :through => :node_memberships # may also need
:polymorphic => true – not sure
end

class Model[A,B,C…] < AR::B

you will probably want to refactor this into an abstract base class

has_many :node_memberships, :as => :node
has_many :relationships, :through => :node_memberships
end

What is the best way to get a list of related nodes using these models?
For example, I want to find all nodes that have a relation to a single
node. The only way I can see to do this is:

@obj = ModelA.find(1, :include => 'relationships)
@rels = @obj.relationships
@rels.each do |r|
r.nodes.each do |n|
if n.node_type != ‘ModelA’ && n.node_id != @obj.id
if n.node_type == ‘ModelA’
nodes.push(ModelA.find(n.node_id)
end
end
end
end

This seems horribly complicated and I think there would be a lot of SQL
calls making this massively inefficient. Is there a better way?

Thanks.

Marnen Laibow-Koser wrote:

@node.relationships.collect(&:nodes).flatten.uniq - [@node]

Sorry about this but I’m still a newbie when it comes to Ruby syntax.
What is the &:nodes section supposed to do? Also is the " - [@node]"
section supposed to be in the code itself?

Thanks,

Matthew Shapiro wrote:
[…]

What is the best way to get a list of related nodes using these models?
For example, I want to find all nodes that have a relation to a single
node. The only way I can see to do this is:

@obj = ModelA.find(1, :include => 'relationships)
@rels = @obj.relationships
@rels.each do |r|
r.nodes.each do |n|
if n.node_type != ‘ModelA’ && n.node_id != @obj.id
if n.node_type == ‘ModelA’
nodes.push(ModelA.find(n.node_id)
end
end
end
end

This seems horribly complicated and I think there would be a lot of SQL
calls making this massively inefficient. Is there a better way?

Sure. Remember, Ruby’s array methods are very powerful. A first stab:

@node.relationships.collect(&:nodes).flatten.uniq - [@node]

It may be possible to improve this further.

Thanks.

Best,

Marnen Laibow-Koser
http://www.marnen.org
[email protected]

Matthew Shapiro wrote:

Marnen Laibow-Koser wrote:

@node.relationships.collect(&:nodes).flatten.uniq - [@node]

Sorry about this but I’m still a newbie when it comes to Ruby syntax.

Have you read Programming Ruby yet?

What is the &:nodes section supposed to do?

&:symbol is the same as :symbol.to_proc. This in turn is defined in
such a way that array.collect(&:fn) is equivalent to (but slightly
slower than) array.collect{|x| x.fn} .

Also is the " - [@node]"
section supposed to be in the code itself?

Yes. It’s an array subtraction.

Thanks,

Best,

Marnen Laibow-Koser
http://www.marnen.org
[email protected]

Marnen Laibow-Koser wrote:
[…]

@node.relationships.collect(&:nodes).flatten.uniq - [@node]

It may be possible to improve this further.

I think it is – even just replacing relationships with node_memberships
would help a bit. But the generated SQL will still be somewhat
inefficient. I’m trying to come up with a way to fix it and keep the
polymorphism.

Thanks.

Best,

Marnen Laibow-Koser
http://www.marnen.org
[email protected]

Marnen Laibow-Koser wrote:

I think it is – even just replacing relationships with node_memberships
would help a bit. But the generated SQL will still be somewhat
inefficient. I’m trying to come up with a way to fix it and keep the
polymorphism.

I don’t see how going doing a @node.node_memberships will help, because
that seems to only give me the node_membership records that @node is in
(meaning I would then have to get the relationship_id value from that
and then .find it). Unless I am missing something which is entirely
possible.

However, it turns out that the relationships aren’t working how you set
them up, even with the nested_has_many plugin. Look at the following
output from ruby’s console:

ModelA.find(1).node_memberships.count
ModelA.find(1).node_memberships.count
=> 1

ModelA.find(1).relationships.count
ModelA.find(1).relationships.count
=> 0

ModelA.find(1).node_memberships[0]
ModelA.find(1).node_memberships[0]
=> #<NodeMembership id: 8, node_id: 1, node_type: “ModelA”,
relationship_id: 60, created_at: “2009-10-27 01:03:56”, updated_at:
“2009-10-27 01:03:56”>

Relationship.find(60)
Relationship.find(60)
=> #<Relationship id: 60, name: nil, description: nil, created_at:
“2009-10-27 00:35:11”, updated_at: “2009-10-27 00:35:11”>

NodeMembership.find(5).relationship
NodeMembership.find(5).relationship
=> #<Relationship id: 60, name: nil, description: nil, created_at:
“2009-10-27 00:35:11”, updated_at: “2009-10-27 00:35:11”>

Relationship.find(60).nodes
Relationship.find(60).nodes
ActiveRecord::HasManyThroughAssociationPolymorphicError: Cannot have a
has_many :through association ‘Relationship#nodes’ on the polymorphic
object ‘Node#node’.
from
c:/ruby/lib/ruby/gems/1.8/gems/activerecord-2.3.4/lib/active_record/reflection.rb:297:in
check_validity_without_nested_has_many_through!' from C:/Users/KallDrexx/Documents/Scrawl/scrawl/vendor/plugins/nested_has_many_through/lib/nested_has_many_through.rb:8:incheck_validity!’
from
c:/ruby/lib/ruby/gems/1.8/gems/activerecord-2.3.4/lib/active_record/associations/has_many_through_association.rb:5:in
initialize' from c:/ruby/lib/ruby/gems/1.8/gems/activerecord-2.3.4/lib/active_record/associations.rb:1297:innew’
from
c:/ruby/lib/ruby/gems/1.8/gems/activerecord-2.3.4/lib/active_record/associations.rb:1297:in
`nodes’
from (irb):11


To summarize, the database has the correct records in the
node_memberships and relationships table, yet for some reason rails
can’t connect from relationships to ModelA and vice-versa. Does
anything pop out at you on why?

Have you read Programming Ruby yet?

No, I learned ruby via tutorials found on google, and most of them
didn’t go in depth into the array methods unfortunately.

&:symbol is the same as :symbol.to_proc. This in turn is defined in
such a way that array.collect(&:fn) is equivalent to (but slightly
slower than) array.collect{|x| x.fn} .

Thanks for that explanation, that made sense.

Thanks again for all your help by the way.

Matthew Shapiro wrote:

Marnen Laibow-Koser wrote:

I think it is – even just replacing relationships with node_memberships
would help a bit. But the generated SQL will still be somewhat
inefficient. I’m trying to come up with a way to fix it and keep the
polymorphism.

I don’t see how going doing a @node.node_memberships will help, because
that seems to only give me the node_membership records that @node is in
(meaning I would then have to get the relationship_id value from that
and then .find it). Unless I am missing something which is entirely
possible.

No, I think you’re right. I apparently wasn’t able to juggle complex
associations in my head as well as I thought.

However, it turns out that the relationships aren’t working how you set
them up, even with the nested_has_many plugin. Look at the following
output from ruby’s console:

ModelA.find(1).node_memberships.count
ModelA.find(1).node_memberships.count
=> 1
ModelA.find(1).relationships.count
ModelA.find(1).relationships.count
=> 0
ModelA.find(1).node_memberships[0]
ModelA.find(1).node_memberships[0]
=> #<NodeMembership id: 8, node_id: 1, node_type: “ModelA”,
relationship_id: 60, created_at: “2009-10-27 01:03:56”, updated_at:
“2009-10-27 01:03:56”>
Relationship.find(60)
Relationship.find(60)
=> #<Relationship id: 60, name: nil, description: nil, created_at:
“2009-10-27 00:35:11”, updated_at: “2009-10-27 00:35:11”>
NodeMembership.find(5).relationship
NodeMembership.find(5).relationship
=> #<Relationship id: 60, name: nil, description: nil, created_at:
“2009-10-27 00:35:11”, updated_at: “2009-10-27 00:35:11”>
Relationship.find(60).nodes
Relationship.find(60).nodes
ActiveRecord::HasManyThroughAssociationPolymorphicError: Cannot have a
has_many :through association ‘Relationship#nodes’ on the polymorphic
object ‘Node#node’.
from
c:/ruby/lib/ruby/gems/1.8/gems/activerecord-2.3.4/lib/active_record/reflection.rb:297:in
check_validity_without_nested_has_many_through!' from C:/Users/KallDrexx/Documents/Scrawl/scrawl/vendor/plugins/nested_has_many_through/lib/nested_has_many_through.rb:8:in check_validity!’
from
c:/ruby/lib/ruby/gems/1.8/gems/activerecord-2.3.4/lib/active_record/associations/has_many_through_association.rb:5:in
initialize' from c:/ruby/lib/ruby/gems/1.8/gems/activerecord-2.3.4/lib/active_record/associations.rb:1297:in new’
from
c:/ruby/lib/ruby/gems/1.8/gems/activerecord-2.3.4/lib/active_record/associations.rb:1297:in
`nodes’
from (irb):11


To summarize, the database has the correct records in the
node_memberships and relationships table, yet for some reason rails
can’t connect from relationships to ModelA and vice-versa. Does
anything pop out at you on why?

Not immediately. I’ll look closer.

Have you read Programming Ruby yet?

No, I learned ruby via tutorials found on google, and most of them
didn’t go in depth into the array methods unfortunately.

Read Programming Ruby (there’s a free Web edition). It’s really a
prerequisite for doing anything serious with the language.

&:symbol is the same as :symbol.to_proc. This in turn is defined in
such a way that array.collect(&:fn) is equivalent to (but slightly
slower than) array.collect{|x| x.fn} .

Thanks for that explanation, that made sense.

Thanks again for all your help by the way.

You’re welcome!

Best,

Marnen Laibow-Koser
http://www.marnen.org
[email protected]

Ok so since I cannot figure out how why that error is occurring, I
commented out the membership stuff and went with the single table with 2
object method. So I have the following models:

class ObjectRelationship < ActiveRecord::Base
belongs_to :first_object, :class_name => ‘WritingObject’, :foreign_key
=> ‘first_object’, :polymorphic => true
belongs_to :second_object, :class_name => ‘WritingObject’,
:foreign_key => ‘second_object’, :polymorphic => true
end

class ModelA < ActiveRecord::Base
has_many :first, :as => ‘first_object’, :class_name =>
‘ObjectRelationship’
has_many :second, :as => ‘second_object’, :class_name =>
‘ObjectRelationship’
end

I have added 2 records into the object_relationships table (both using
ModelA as the types):

  1. first_object_id = 1, second_object_id = 5
  2. first_object_id = 11, second_object_id = 1

I figure to get a list of objects, I can just do 2 calls, which
shouldn’t be too bad.

However I have come up with the following issues with this method

First issue is creating a new record is kind of a pain. I can’t seem to
just do ModelA.find(1).first.create(:second_object => ModelA.find(2))
like I was hoping. The only way I was able to create records correctly
was to do ModelA.find(1).first.create(:second_object_id => 2,
:second_object_type => ‘ModelA’). Is there any way to do it without
having to use a string for the object_type at the very least?

The second issue seems to be retrieving a list of objects. With the 2
record setup in the database (as previously described), when I do
ModelA.find(1).first.collect(&:second_object) I get [nil] as the result.
The only way I can seem to get a non-nil result I have to do
ModelA.find(1).first.collect(&:second_object_id), but that only returns
the second_object’s id, not the object’s type. How can I get both bits
of information, or do I need to collect twice?

Thanks again. I’m learning a lot by trying these different
methodologies out!

Matthew Shapiro wrote:

Ok so since I cannot figure out how why that error is occurring, I
commented out the membership stuff

Why did you comment it out? You can always use your version control
system to bring it back.

and went with the single table with 2
object method.

Probably a bad idea. It’s worth taking the time to model your data
correctly.

Best,

Marnen Laibow-Koser
http://www.marnen.org
[email protected]

Marnen Laibow-Koser wrote:

Why did you comment it out? You can always use your version control
system to bring it back.

Good point, I should know better…

Probably a bad idea. It’s worth taking the time to model your data
correctly.

Why is the single table with a first object and second object
necessarily a bad idea? I’m not going to duplicate mostly likely (i.e.
create 2 records for 1 relationship) and instead just do 2 queries,
which it seems the other method is going to require. The
node_membership model does allow greater flexibility though.

Matthew Shapiro wrote:
[…]

However, it turns out that the relationships aren’t working how you set
them up, even with the nested_has_many plugin. Look at the following
output from ruby’s console:
[…]
ActiveRecord::HasManyThroughAssociationPolymorphicError: Cannot have a
has_many :through association ‘Relationship#nodes’ on the polymorphic
object ‘Node#node’.
[…]
To summarize, the database has the correct records in the
node_memberships and relationships table, yet for some reason rails
can’t connect from relationships to ModelA and vice-versa. Does
anything pop out at you on why?

Looking at the error message, it appears that :through and :polymorphic
are mutually incompatible. If that’s so, and if there’s no plugin or
other technique that fixes that incompatibility, then it’s easy enough
to fix by introducing another level of association. Note, though, that
you will need nested_has_many_through if you didn’t before…

…because what we’re going to do is remove the polymorphism from Node.
The new associations (still untested, though) will look like this:

class NodeMembership < AR::B # join model since Rails won’t do
polymorphic habtm
belongs_to :node
belongs_to :relationship
end

class Relationship < AR::B
has_many :node_memberships
has_many :nodes, :through => :node_memberships
end

class Node < AR::B # make this concrete since polymorphic :through
doesn’t appear to work
belongs_to :content, :polymorphic => true
has_many :node_memberships
has_many :relationships, :through => :node_memberships
end

class Model[A,B,C…] < AR::B

you will probably want to refactor this into an abstract base class

has_many :nodes, as => :content
has_many :node_memberships, :through => :nodes
has_many :relationships, :through => :node_memberships
end

See what we’ve done here? Since has_many :through needs to be to a
concrete (non-polymorphic) association, we’ve introduced Node as a
concrete type through which to proxy the association between
NodeMembership and Model*.

Let me know if this works.

Best,

Marnen Laibow-Koser
http://www.marnen.org
[email protected]

Matthew Shapiro wrote:

Marnen Laibow-Koser wrote:

Why did you comment it out? You can always use your version control
system to bring it back.

Good point, I should know better…

Probably a bad idea. It’s worth taking the time to model your data
correctly.

Why is the single table with a first object and second object
necessarily a bad idea?

For all the reasons discussed earlier in this thread – notably the
difficulty of querying when the value could be in either of two fields.

Also, correct data modeling is fundamental to good application
development. Take the time to get it right. Hold up the rest of
development for the data model if you have to – it’s that important.

I’m not going to duplicate mostly likely (i.e.
create 2 records for 1 relationship) and instead just do 2 queries,
which it seems the other method is going to require. The
node_membership model does allow greater flexibility though.

And through clever use of joins, it can get everything you need in 1
query. That alone is a strong sign that it’s the correct model. Use
it.

Best,

Marnen Laibow-Koser
http://www.marnen.org
[email protected]

Actually I think I just found a massively easier way.

I got the has_many_polymorphs plugin and it simplified my models. This
is what I have now:

class Relationship < ActiveRecord::Base
has_many_polymorphs :nodes, :from => [:topics, :notes], :through =>
:node_memberships
end

class NodeMembership < ActiveRecord::Base
belongs_to :node, :polymorphic => true
belongs_to :relationship
end

class ModelA < ActiveRecord::Base
end

This allows me to successfully do @node.relationships.collect(&:nodes).

Thanks a ton! :smiley: