03 January 2011

Bi-directional relationships in Rails

Really that should be “Bi-directional self-referential relationships in ActiveRecord models”, but that’s just too much to say!

A common example for this would be if you had a Person model and a Friendship model, and you wish the friendship between two people to be a two-way road, like on Facebook. The project I’m working on is a genealogy application that has partnerships (eg. marriage) between two people, but it’s the same deal.

Now typically, :has_many relationships in ActiveRecord are uni-directional. You have to do a little bit of trickery to get it to happen both ways. Here’s the code.

# Tables

create_table "partnerships" do |t|
  t.integer "person_id"
  t.integer "partner_id"

create_table "people" do |t|
  t.string   "name"

# Models

class Person < ActiveRecord::Base
  has_many :partnerships, :dependent => :destroy
  has_many :partners, :through => :partnerships, :source => :person

class Partnership < ActiveRecord::Base
  belongs_to :person, :foreign_key => :partner_id
  after_create do |p|
    if !Partnership.find(:first, :conditions => { :partner_id => p.person_id })
      Partnership.create!(:person_id => p.partner_id, :partner_id => p.person_id)
  after_destroy do |p|
    reciprocal = Partnership.find(:first, :conditions => { :partner_id => p.person_id })
    reciprocal.destroy unless reciprocal.nil?

So what we’re doing here essentially is creating two uni-directional Partnership record, and using the after_create and after_destroy hooks to create and delete them as a pair.

There’s another layer of detail here which is very useful: by using a model Partnership instead of Parner, and then adding the :has_many :partners, :through => :partnerships line, you get to have an attribute called person.partners that returns an array of people, but you also get to call person.partnerships for more details about that particular relationship. For instance, by adding a start_date to the partnerships table you could sort the relationships chronologically.