9

I'm writing a mentorship program for our church in rails (im still farily new to rails)..

And i need to model this..

contact
has_one :father, :class_name => "Contact"
has_one :mother, :class_name => "Contact"
has_many :children, :class_name => "Contact"
has_many :siblings, :through <Mother and Father>, :source => :children

So basically an objects "siblings" needs to map all the children from both the father and mother not including the object itself..

Is this possible?

Thanks

Daniel

Daniel Upton
  • 5,561
  • 8
  • 41
  • 64

3 Answers3

10

It's funny how questions that appear simple can have complex answers. In this case, implementing the reflexive parent/child relationship is fairly simple, but adding the father/mother and siblings relationships creates a few twists.

To start, we create tables to hold the parent-child relationships. Relationship has two foreign keys, both pointing at Contact:

create_table :contacts do |t|
  t.string :name
end

create_table :relationships do |t|
  t.integer :contact_id
  t.integer :relation_id
  t.string :relation_type
end

In the Relationship model we point the father and mother back to Contact:

class Relationship < ActiveRecord::Base
  belongs_to :contact
  belongs_to :father, :foreign_key => :relation_id, :class_name => "Contact",
  :conditions => { :relationships => { :relation_type => 'father'}}
  belongs_to :mother, :foreign_key => :relation_id, :class_name => "Contact",
  :conditions => { :relationships => { :relation_type => 'mother'}}
end

and define the inverse associations in Contact:

class Contact < ActiveRecord::Base
  has_many :relationships, :dependent => :destroy
  has_one :father, :through => :relationships
  has_one :mother, :through => :relationships
end

Now a relationship can be created:

@bart = Contact.create(:name=>"Bart")
@homer = Contact.create(:name=>"Homer")
@bart.relationships.build(:relation_type=>"father",:father=>@homer)
@bart.save!
@bart.father.should == @homer

This is not so great, what we really want is to build the relationship in a single call:

class Contact < ActiveRecord::Base
  def build_father(father)
    relationships.build(:father=>father,:relation_type=>'father')
  end
end

so we can do:

@bart.build_father(@homer)
@bart.save!

To find the children of a Contact, add a scope to Contact and (for convenience) an instance method:

scope :children, lambda { |contact| joins(:relationships).\
  where(:relationships => { :relation_type => ['father','mother']}) }

def children
  self.class.children(self)
end

Contact.children(@homer) # => [Contact name: "Bart")]
@homer.children # => [Contact name: "Bart")]

Siblings are the tricky part. We can leverage the Contact.children method and manipulate the results:

def siblings
  ((self.father ? self.father.children : []) +
   (self.mother ? self.mother.children : [])
   ).uniq - [self]
end

This is non-optimal, since father.children and mother.children will overlap (thus the need for uniq), and could be done more efficiently by working out the necessary SQL (left as an exercise :)), but keeping in mind that self.father.children and self.mother.children won't overlap in the case of half-siblings (same father, different mother), and a Contact might not have a father or a mother.

Here are the complete models and some specs:

# app/models/contact.rb
class Contact < ActiveRecord::Base
  has_many :relationships, :dependent => :destroy
  has_one :father, :through => :relationships
  has_one :mother, :through => :relationships

  scope :children, lambda { |contact| joins(:relationships).\
    where(:relationships => { :relation_type => ['father','mother']}) }

  def build_father(father)
    # TODO figure out how to get ActiveRecord to create this method for us
    # TODO failing that, figure out how to build father without passing in relation_type
    relationships.build(:father=>father,:relation_type=>'father')
  end

  def build_mother(mother)
    relationships.build(:mother=>mother,:relation_type=>'mother')
  end

  def children
    self.class.children(self)
  end

  def siblings
    ((self.father ? self.father.children : []) +
     (self.mother ? self.mother.children : [])
     ).uniq - [self]
  end
end

# app/models/relationship.rb
class Relationship < ActiveRecord::Base
  belongs_to :contact
  belongs_to :father, :foreign_key => :relation_id, :class_name => "Contact",
  :conditions => { :relationships => { :relation_type => 'father'}}
  belongs_to :mother, :foreign_key => :relation_id, :class_name => "Contact",
  :conditions => { :relationships => { :relation_type => 'mother'}}
end

# spec/models/contact.rb
require 'spec_helper'

describe Contact do
  before(:each) do
    @bart = Contact.create(:name=>"Bart")
    @homer = Contact.create(:name=>"Homer")
    @marge = Contact.create(:name=>"Marge")
    @lisa = Contact.create(:name=>"Lisa")
  end

  it "has a father" do
    @bart.relationships.build(:relation_type=>"father",:father=>@homer)
    @bart.save!
    @bart.father.should == @homer
    @bart.mother.should be_nil
  end

  it "can build_father" do
    @bart.build_father(@homer)
    @bart.save!
    @bart.father.should == @homer
  end

  it "has a mother" do
    @bart.relationships.build(:relation_type=>"mother",:father=>@marge)
    @bart.save!
    @bart.mother.should == @marge
    @bart.father.should be_nil
  end

  it "can build_mother" do
    @bart.build_mother(@marge)
    @bart.save!
    @bart.mother.should == @marge
  end

  it "has children" do
    @bart.build_father(@homer)
    @bart.build_mother(@marge)
    @bart.save!
    Contact.children(@homer).should include(@bart)
    Contact.children(@marge).should include(@bart)
    @homer.children.should include(@bart)
    @marge.children.should include(@bart)
  end

  it "has siblings" do
    @bart.build_father(@homer)
    @bart.build_mother(@marge)
    @bart.save!
    @lisa.build_father(@homer)
    @lisa.build_mother(@marge)
    @lisa.save!
    @bart.siblings.should == [@lisa]
    @lisa.siblings.should == [@bart]
    @bart.siblings.should_not include(@bart)
    @lisa.siblings.should_not include(@lisa)
  end

  it "doesn't choke on nil father/mother" do
    @bart.siblings.should be_empty
  end
end
zetetic
  • 47,184
  • 10
  • 111
  • 119
  • You sir are a rails and stackoverflow monster (Specs in your answers!?) awesome!! if i could i'd kiss you! Thanks :) – Daniel Upton Feb 14 '11 at 13:10
  • Ah.. one idea though, would it not work to add father_id and mother_id to the contact model and then add has_many :children, :class_name => "Contact", :finder_sql => 'SELECT * FROM contacts WHERE contacts.father_id = #{id} OR contacts.mother_id = #{id}" and has_many :siblings, :class_name => "Contact", :finder_sql => 'SELECT * FROM contacts WHERE contacts.father_id = #{father_id} OR contacts.mother_id = #{mother_id}' ? Just an idea :P – Daniel Upton Feb 14 '11 at 13:54
  • You could do it in one table, but that would limit you to the relationships that can be specified through the foreign keys. With a separate table you have the flexibility to specify other relationship types, like 'godfather' or 'uncle'. – zetetic Feb 14 '11 at 17:31
  • Cool thanks :) i think i'm gonna hack together a combination of the 2 over lunch.. and add 3 foreign keys to the model father_figure_id mother_figure_id and emergency_contact_id and use those for the father and mother style stuff.. and then add in a many to many style thing for other contact relationships like uncles cousins and freinds... Thanks for the great advise! Oh one last thing.. How does the has_one :father, :through => :relationships work if relationships are many to many? Thanks! :D – Daniel Upton Feb 15 '11 at 09:55
  • The schema is the same for `has_one` and `has_many`: a table relating to a second table which has a foreign key pointing at the first. Or in the case of `:through`, storing the keys in join table. `has_one` is basically a special case of `has_many` which won't add more than one object through the association. Of course this assumes you use the methods added by `has_one` to build the objects -- nothing prevents you from adding multiple `fathers` by using SQL, unless you add constraints to the database. – zetetic Feb 15 '11 at 17:19
2

I totally agree with zetetic. The question looks far more simpler then the answer and there is little we could do about it. I'll add my 20c though.
Tables:

    create_table :contacts do |t|
      t.string :name
      t.string :gender
    end
    create_table :relations, :id => false do |t|
      t.integer :parent_id
      t.integer :child_id
    end

Table relations does not have corresponding model.

class Contact < ActiveRecord::Base
  has_and_belongs_to_many :parents,
    :class_name => 'Contact',
    :join_table => 'relations',
    :foreign_key => 'child_id',
    :association_foreign_key => 'parent_id'

  has_and_belongs_to_many :children,
    :class_name => 'Contact',
    :join_table => 'relations',
    :foreign_key => 'parent_id',
    :association_foreign_key => 'child_id'

  def siblings
    result = self.parents.reduce [] {|children, p| children.concat  p.children}
    result.uniq.reject {|c| c == self}
  end

  def father
    parents.where(:gender => 'm').first
  end

  def mother
    parents.where(:gender => 'f').first
  end
end  

Now we have regular Rails assosiations. So we can

alice.parents << bob
alice.save

bob.chidren << cindy
bob.save

alice.parents.create(Contact.create(:name => 'Teresa', :gender => 'f')

and all stuff like that.

Art Shayderov
  • 5,002
  • 1
  • 26
  • 33
0
  has_and_belongs_to_many :parents,
    :class_name => 'Contact',
    :join_table => 'relations',
    :foreign_key => 'child_id',
    :association_foreign_key => 'parent_id',
    :delete_sql = 'DELETE FROM relations WHERE child_id = #{id}'

  has_and_belongs_to_many :children,
    :class_name => 'Contact',
    :join_table => 'relations',
    :foreign_key => 'parent_id',
    :association_foreign_key => 'child_id',
    :delete_sql = 'DELETE FROM relations WHERE parent_id = #{id}'

I used this example but had to add the :delete_sql to clean up the relations records. At first I used double quotes around the string but found that caused errors. Switching to single quotes worked.

seehad
  • 161
  • 7