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" end create_table "people" do |t| t.string "name" end # Models class Person < ActiveRecord::Base has_many :partnerships, :dependent => :destroy has_many :partners, :through => :partnerships, :source => :person end 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) end end after_destroy do |p| reciprocal = Partnership.find(:first, :conditions => { :partner_id => p.person_id }) reciprocal.destroy unless reciprocal.nil? end end
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.