No Clean Feed - Stop Internet Censorship in Australia

Reciprocal relationships in ActiveRecord

Imagine you had a domain model 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 (excuse the crude layout; I hacked it up in a few minutes with Dia and I don't know how to resize elements) looks like this:

UML Representation

It is possible (although not entirely intuitive) to express this type of relationship in Ruby on Rails, using ActiveRecord. Note that the foreign_key calls are to the Red Hill on Rails Core plugin; if you're not using it or something like it to enforce referential integrity in your database, then you should probably look into it as a matter of urgency.

Anyhow, to the code:

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 we want. In practice you'd want some constraints and observers - so for example, making a Person the CEO of a company would automatically add that Person to the employees collection. But you can see in principle how it'd all work from the above example.

This issue has been nagging at me since I ran into it while coding yesterday evening. Now I've put it to rest, I'm off to enjoy the fabulous weather we've been experiencing recently. At some point, I've got to sort out some decent code formatting on this blog (in particular, Ruby syntax highlighting) but that's a concern for a later (perhaps rainier) day :-)

Riding