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:

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




Recent comments
1 week 1 day ago
3 weeks 3 days ago
4 weeks 27 min ago
11 weeks 1 day ago
15 weeks 5 days ago
31 weeks 23 hours ago
31 weeks 1 day ago
37 weeks 3 days ago
37 weeks 4 days ago
37 weeks 4 days ago