Reciprocal Relationships using ActiveRecord

Reciprocal relationships are present in many problem domains, and it's important to be able to model them using your ORM. In this case we'll look at a reciprocal relationship involving a Company and a Person.

A Person can be an employee of a single Company, or the CEO of a single Company. If a Person is a CEO of a Company, that Company is known as the Person's empire. If a Person is employed by a Company, that Company is known as the Person's employer.

If a Person is employed by a Company, that Person is known to the Company as an employee. If a Person is a CEO of a company, that Person is known as the Company's ceo.

The UML for this particular relationship looks like this:

UML Representation

It is possible to express this type of relationship using ActiveRecord. Note that the foreign_key calls are to the Red Hill on Rails Core plugin, which is being used to enforce referential integrity in the database.

class Company < ActiveRecord::Base
  has_many :employees, :class_name => 'Person', :foreign_key => 'employer_company_id'
  belongs_to :ceo, :class_name => 'Person', :foreign_key => 'ceo_person_id'
end

class Person < ActiveRecord::Base
  belongs_to :employer, :class_name => 'Company', :foreign_key => 'employer_company_id'
  has_one :empire, :class_name => 'Company', :foreign_key => 'ceo_person_id'
end

class CreatePeople < ActiveRecord::Migration
  def self.up
    create_table :people do |t|
      t.column :employer_company_id, :integer
      t.timestamps
      t.foreign_key :employer_company_id, :companies, :id
    end
  end
  def self.down
    drop_table :people
  end
end

class CreateCompanies < ActiveRecord::Migration
  def self.up
    create_table :companies do |t|
      t.column :ceo_person_id, :integer
      t.timestamps
      t.foreign_key :ceo_person_id, :people, :id
    end
  end
  def self.down
    drop_table :companies
  end
end

To prove this works, we can fire up a console and create an imaginary company, with employees and a CEO:

>> megacorp = Company.create
=> #<Company id: 1, ceo_person_id: nil, created_at: "2009-11-15 00:34:07", updated_at: "2009-11-15 00:34:07">
>> bob = megacorp.employees.create
=> #<Person id: 1, employer_company_id: 1, created_at: "2009-11-15 00:34:15", updated_at: "2009-11-15 00:34:15">
>> joe = megacorp.employees.create
=> #<Person id: 2, employer_company_id: 1, created_at: "2009-11-15 00:34:22", updated_at: "2009-11-15 00:34:22">
>> megacorp.ceo = bob
=> #<Person id: 1, employer_company_id: 1, created_at: "2009-11-15 00:34:15", updated_at: "2009-11-15 00:34:15">
>> megacorp.save!
=> true
>> megacorp.employees
=> [#<Person id: 1, employer_company_id: 1, created_at: "2009-11-15 00:34:15", updated_at: "2009-11-15 00:34:15">,
    #<Person id: 2, employer_company_id: 1, created_at: "2009-11-15 00:34:22", updated_at: "2009-11-15 00:34:22">]
>> megacorp.ceo
=> #<Person id: 1, employer_company_id: 1, created_at: "2009-11-15 00:34:15", updated_at: "2009-11-15 00:34:15">
>> bob.employer
=> #<Company id: 1, ceo_person_id: 1, created_at: "2009-11-15 00:34:07", updated_at: "2009-11-15 00:34:33">
>> bob.empire
=> #<Company id: 1, ceo_person_id: 1, created_at: "2009-11-15 00:34:07", updated_at: "2009-11-15 00:34:33">
>> joe.employer
=> #<Company id: 1, ceo_person_id: 1, created_at: "2009-11-15 00:34:07", updated_at: "2009-11-15 00:34:33">
>> joe.empire
=> nil

As you can see it's possible to build up just the kind of relationships described. In practice you would probably want to extend the above example with some constraints and observers - so for example, making a Person the CEO of a company would automatically add that Person to the employees collection.