Update: This article is now superceded by a new version that is updated for Rails 2.0 changes.
Here we go. Rick Olson helped me figure out how to do bi-directional, self-referential associations using has_many :through
. It's not obvious (until you know the trick), so here's how it's done.
This example is for modeling digraphs.
create_table "nodes" do |t|
t.column "name", :string
t.column "capacity", :integer
end
create_table "edges" do |t|
t.column "source_id", :integer, :null => false
t.column "sink_id", :integer, :null => false
t.column "flow", :integer
end
class Edge < ActiveRecord::Base
belongs_to :source, :foreign_key => "source_id", :class_name => "Node"
belongs_to :sink, :foreign_key => "sink_id", :class_name => "Node"
end
class Node < ActiveRecord::Base
has_many :edges_as_source, :foreign_key => 'source_id', :class_name => 'Edge'
has_many :edges_as_sink, :foreign_key => 'sink_id', :class_name => 'Edge'
has_many :sources, :through => :edges_as_sink
has_many :sinks, :through => :edges_as_source
end
A few pictures would probably be helpful, but I don't have time to be artistic this morning so you'll have to use your imagination and visualize things on your own. Each edge connects a source node to a sink node, and each node can have any number of incoming or outgoing edges.
The tricky bit is using the main has_many
associations to distinguish the direction of the edge. Use the :foreign_key
option to specify whether the edge refers to the node as its source or sink. Then the node can refer to other nodes as its sources or sinks by going through the appropriate has_many
association.
And that's it.
This is very cool and just what I have been looking for. Thanks!
Lets say each Node has_many :boxes. Is there a way to grab all the boxes that belong to a Node's sinks (or sources) in a single query?
The model graph would look like this
create_table "nodes" do |t|
t.column "name", :string
t.column "capacity", :integer
end
create_table "edges" do |t|
t.column "source_id", :integer, :null => false
t.column "sink_id", :integer, :null => false
t.column "flow", :integer
end
create_table "boxes" do |t|
t.column "node_id", :integer, :null => false
t.column "label", :string
end
class Edge < ActiveRecord::Base
belongsto :source, :foreignkey => "sourceid", :classname => "Node"
belongsto :sink, :foreignkey => "sinkid", :classname => "Node"
end
class Node < ActiveRecord::Base
hasmany :edgesassource, :foreignkey => 'sourceid', :classname => 'Edge'
hasmany :edgesassink, :foreignkey => 'sinkid', :classname => 'Edge'
hasmany :sources, :through => :edgesas_sink
hasmany :sinks, :through => :edgesas_source
has_many :boxes
end
Thank you for the many informative articles. I wish I had the foggiest notion what a digraph is, since then I would be able to actually understand how to create self-referential has_many :through associations. Without knowing how nodes, edges, sources, and sinks interrelate in the real world, I find that my feeble brain simply cannot grok what's going on here in the Rails world. :(
I wonder if you could attach a polymorphic to this so you could also just get all edges associated, rather than just sources or sinks.. :)
You can do bi-directional self-referencial associations with habtm as well, if that suits your needs.
I'm maintaining an act/plugin called acts_as_graph:
http://dev.buildpatterns.com/trac/browser/rails/plugins/acts_as_graph
It's based on habtm, but i'm considering basing it on hm :through.
aslak
Nice article, though I can't get this to work in my case. Extremely grateful for any help. I want to represent a not-necessarily-symmetrical friendship relation with attributes on the friendship relation. Code, followed by error message:
There are also fixtures that have populated the friendships table with some data. Error message:
I should add that user.friendships, user.befriendships, friendship.friendshipped and friendship.befriendshipped work fine - but user.friends and user.befrienders errors.
After restarting the server and the console entirely, I get ActiveRecord::HasManyThroughSourceAssociationNotFoundError: ActiveRecord::HasManyThroughSourceAssociationNotFoundError instead. Still an error, though.
Sorry for the repeated comments. This should be the last one.
Solved it. Hope this helps someone else. Apparently you should do this:
Thanks! I was trying to work out how to do exactly this and I think you may have just saved me a lot of time.
So I started trying to figure out how my models would interact, and I ended up with something that looked a little like an icosahedron, with all of the edges being two-way habtm relationships. I just assumed I was doomed.
I don't yet understand what you wrote above, but if I get the gist of it, it looks like what I need. Maybe.
Are :source, :sink, :edgesas_source, and :edgesas_sink the join models?
How is eager loading affected, when there are multiple ':through's going on?
I.e.
A has_many :B, :through E (and possibly vice versa)
B has_many :C, :through F (and possibly vice versa)
C has_many :D, :through G (and possibly vice versa)
Aren't I still hosed if I want to look at A.B.C.D? Or C.B.A? Or does your trick above make it work?
It sounds like you don't need a self-referential scheme, just a lot of associations. Unless you need to have an A refer to an A (by whatever association name), then you don't need this trick.
Eager loading works with :through associations, and it shouldn't matter that you have multiple associations in your model. I haven't used eager loading through multiple steps of :through associations though, so I can't say with confidence that works.
I'm scared of your icosahedron!
Josh, many thanks for the excellent article. I'd struggled for a couple of days to get this working -- works like a charm now, even thru STI! I missed the source->sink switch on first read: hasmany :sources, :through => :edgesas_sink hasmany :sinks, :through => :edgesas_source
Just wanted to highlight that part in case it causes someone else trouble...
Henrik N, Thank you for posting your information and resolution. This is exactly what I was looking for and was having the same issue.
This way has one helpful service. That art has this considerable craps casino online. Conscious head is this severe ground. This primitive party fit the country extensively. Obviously, the specified family royally grabbed in spite of this favourable language.
I thank you for this Great explanation of how to set up models self-referentially, and the db, BUT. . .can you give examples of how you access the various objects. . .once this is all set up? that's where i am now stuck. . .
I thank you for this Great explanation of how to set up models self-referentially, and the db, BUT. . .can you give examples of how you access the various objects. . .once this is all set up? that's where i am now stuck. . .
this is simply a shortcut not a direct soln check the pdfs has_many :friends, :through => :friendships, :source => :befriendshipped
has_many :befrienders, :through => :befriendships, :source => :friendshipped
I've been racking my braing trying to get a self-referential friends list working. Here's what I have.
Notice that 'userid' is trying to be set as 2 (which is what the friendid should be) which leads me to believe that I suck at has_many :through ... x=\
I've tried pretty much copying and pasting some of the examples here and I get pretty much the same result. I've tried to move the foreignkey's around to different places. I've tried (in my eyes) just about everything. What else could be wrong? I know it has been some time since this original post and now, so things might have changed. Anyone know something I don't? I'm very new to hasmany :through but I just can't figure this out for the life of me. Any help would be GREATLY appreciated.
Just one more added piece of information for my above problem. I created a row in the friendships database to link 2 users, and it seems that FINDING the users friends (User.find(1).friends.find(2)) works fine. It's just creating and deleting that isn't working. I know that I could just manually create these things, but I'd really like to get it to work properly so I don't have worry about anything breaking. Thanks again.
Big, big thanks to this lovely article from my part. This was cracking my brain (I am quite new to RoR) and to find such concentrated knowledge here is wonderful (although it took me a while to find it with Google...). However, the presented solution did not save me the relationship. I suppose, because the relationship is itself a model that may have to be saved explicitly...? Either way, Joel Hayhurst's solution worked like a charm in my case (because, although my relationship can be clearly labelled, it has no additional attributes so far), so many thanks to Joel as well. Take care...
Has anyone a link or an idea how to apply this for a not directed graph? (Self-referential, many-to-many, and with attributes for the edges). Many thanks!