Flexible model associations

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

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

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,

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,

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: