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: