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