Self-referential has_many :through associations

— April 21, 2006 at 11:50 PDT


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.

22 commentsassociations, rails

Comments
  1. Jamie Quint2006-04-21 14:03:30

    This is very cool and just what I have been looking for. Thanks!

  2. Andrew Viterbi2006-04-21 14:52:17

    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

  3. Justin2006-04-22 10:05:46

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

  4. Peter Cooper2006-04-22 12:43:15

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

  5. Joel Hayhurst2006-04-23 11:19:05

    You can do bi-directional self-referencial associations with habtm as well, if that suits your needs.

    create_table 'parents_children', :id => false do |t|
        t.column 'parent_id', :integer, :null => false
        t.column 'child_id', :integer, :null => false
    end
    
    class Node < ActiveRecord::Base
        has_and_belongs_to_many :children,
            :class_name => 'Node',
            :join_table => 'parents_children',
            :foreign_key => 'parent_id',
            :association_foreign_key => 'child_id'
    
        has_and_belongs_to_many :parents,
            :class_name => 'Node',
            :join_table => 'parents_children',
            :foreign_key => 'child_id',
            :association_foreign_key => 'parent_id'
    end
    
  6. Aslak Hellesoy2006-04-23 13:35:37

    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

  7. Henrik N2006-04-24 02:53:50

    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:

    create_table "friendships", :id => false, :force => true do |t|
      t.column "user_id", :integer, :null => false
      t.column "friend_id", :integer, :null => false
      t.column "created_at", :datetime
    end
    
    
    class Friendship < ActiveRecord::Base
    
      belongs_to :friendshipped,
        :foreign_key => "user_id",
        :class_name => "User"
      belongs_to :befriendshipped,
        :foreign_key => "friend_id",
        :class_name => "User"
    
    end
    
    
    class User < ActiveRecord::Base
    
      has_many :friendships,
        :foreign_key =>       'user_id',
        :class_name =>        'Friendship'
      has_many :befriendships,
        :foreign_key =>       'friend_id',
        :class_name =>        'Friendship'
    
      has_many :friends,
        :through =>         :friendships
      has_many :befrienders,
        :through =>         :befriendships
    
    end
    

    There are also fixtures that have populated the friendships table with some data. Error message:

    >> User.find(1).friends
    ActiveRecord::HasManyThroughSourceAssociationMacroError: ActiveRecord::HasManyTh
    roughSourceAssociationMacroError
            from c:/Program/Ruby/lib/ruby/gems/1.8/gems/activerecord-1.14.2/lib/acti
    ve_record/reflection.rb:181:in `check_validity!'
            from c:/Program/Ruby/lib/ruby/gems/1.8/gems/activerecord-1.14.2/lib/acti
    ve_record/associations/has_many_through_association.rb:6:in `initialize'
            from c:/Program/Ruby/lib/ruby/gems/1.8/gems/activerecord-1.14.2/lib/acti
    ve_record/associations.rb:876:in `friends'
            from (irb):82
    >> "friendship".pluralize
    => "friendships"
    
  8. Henrik N2006-04-24 03:13:52

    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.

  9. Henrik N2006-04-24 03:23:51

    Sorry for the repeated comments. This should be the last one.

    Solved it. Hope this helps someone else. Apparently you should do this:

    has_many :friends,
        :through =>     :friendships,
        :source =>  :befriendshipped
    
    has_many :befrienders,
        :through =>     :befriendships,
        :source =>  :friendshipped
    
  10. Bill2006-04-26 02:40:41

    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.

  11. befuddled2006-05-15 17:12:31

    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?

  12. Josh Susser2006-05-15 17:31:14

    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!

  13. Dan Kirkwood2006-07-05 10:38:51

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

  14. Skiz2006-07-16 11:22:20

    Henrik N, Thank you for posting your information and resolution. This is exactly what I was looking for and was having the same issue.

  15. online craps betting2006-09-12 11:04:16

    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.

  16. tobywan2006-09-25 17:58:57

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

  17. tobywan2006-09-25 17:59:01

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

  18. greatcpy2006-11-15 04:54:55

    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

  19. Steve Ehrenberg2006-11-24 22:05:08

    I've been racking my braing trying to get a self-referential friends list working. Here's what I have.

     create_table :friendships, :id => false do |t|
       t.column :user_id,    :integer, :null => false
       t.column :friend_id,  :integer, :null => false
       t.column :status,     :integer, :null => false, :default => 0
       t.column :created_at, :datetime
     end
    
     class Friendship < ActiveRecord::Base
       belongs_to :user, 
           :class_name => 'User', 
           :foreign_key => 'user_id'
       belongs_to :friend, 
           :class_name => 'User',
           :foreign_key => 'friend_id'
     end
    
     class User < ActiveRecord::Base
       has_many :friendships
       has_many :friends, :through => :friendships
     end
    
     class FriendsController < ApplicationController
       def add
         user = User.find(params[:user_id])
         friend = User.find(params[:id])
         user.friends << friend
         friend.friends << user
       end
     end
    
     http://localhost:3000/users/1/friends/2;add
    
     ActiveRecord::StatementInvalid (Mysql::Error: Column 'friend_id' cannot be null: INSERT INTO friendships ('status', 'user_id', 'created_at', 'friend_id') VALUES(0, 2, '2006-11-24 22:11:58', NULL)):
    

    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.

  20. Steve Ehrenberg2006-11-24 23:00:44

    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.

  21. Frank Quednau2007-01-03 09:08:22

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

  22. Maledictus2007-01-23 13:11:12

    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!

Sorry, comments for this article are closed.