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
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
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With