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? -- Posted via http://www.ruby-forum.com/.
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? -- Posted via http://www.ruby-forum.com/.
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) -- Posted via http://www.ruby-forum.com/.
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 marnen-sbuyVjPbboAdnm+yROfE0A@public.gmane.org http://www.marnen.org -- Posted via http://www.ruby-forum.com/.
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! :) -- Posted via http://www.ruby-forum.com/.
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 > endWhat 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. -- Posted via http://www.ruby-forum.com/.
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 marnen-sbuyVjPbboAdnm+yROfE0A@public.gmane.org -- Posted via http://www.ruby-forum.com/.
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, -- Posted via http://www.ruby-forum.com/.
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 marnen-sbuyVjPbboAdnm+yROfE0A@public.gmane.org -- Posted via http://www.ruby-forum.com/.
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 > marnen-sbuyVjPbboAdnm+yROfE0A@public.gmane.org-- Posted via http://www.ruby-forum.com/.
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.countModelA.find(1).node_memberships.count => 1>> ModelA.find(1).relationships.countModelA.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).relationshipNodeMembership.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).nodesRelationship.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?> 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. -- Posted via http://www.ruby-forum.com/.
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 marnen-sbuyVjPbboAdnm+yROfE0A@public.gmane.org -- Posted via http://www.ruby-forum.com/.
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! -- Posted via http://www.ruby-forum.com/.
Matthew Shapiro wrote:> Ok so since I cannot figure out how why that error is occurring, I > commented out the membership stuffWhy 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 marnen-sbuyVjPbboAdnm+yROfE0A@public.gmane.org -- Posted via http://www.ruby-forum.com/.
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. -- Posted via http://www.ruby-forum.com/.
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 marnen-sbuyVjPbboAdnm+yROfE0A@public.gmane.org -- Posted via http://www.ruby-forum.com/.
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 marnen-sbuyVjPbboAdnm+yROfE0A@public.gmane.org -- Posted via http://www.ruby-forum.com/.
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! :D -- Posted via http://www.ruby-forum.com/.