Has_many: through multiple has_one relationships?

I am writing a mentoring program for our church on rails (they are still very new for rails).

And I need to simulate 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 

Thus, basically the objects of โ€œbrothers and sistersโ€ need to match all the children from the father and mother, not including the object itself.

Is it possible?

thanks

Daniel

+8
sql ruby-on-rails relational-database many-to-many self-reference
source share
3 answers

It's funny how simple questions can have complex answers. In this case, the implementation of the parent / child reflexive relationship is quite simple, but adding the relationship between father / mother and siblings creates a few twists.

To get started, we create tables to hold parent-child relationships. Relations have two foreign keys, both indicate 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 Relationships model, we point the father and mother back to the 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 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 you can create the relationship:

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

It's not so great that we really want to build relationships in one 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 contact children, add a contact area and (for convenience) 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 hard part. We can use the Contact.children method and manage the results:

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

This is not optimal, since fathers and children will overlap (thus uniq ) and can be done more efficiently by generating the necessary SQL (left as an exercise :)), but given that self.father.children and self.mother.children will not overlap in the case of half-point brothers (same father, different mother), and the contact may not have a father or mother.

Here are the complete models and some specifications:

 # 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 
+9
source share

I totally agree with zetetic. The question looks much simpler than the answer, and there is little we can do about it. I will add 20c. 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 ratios do not have an appropriate 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 associations. So we can

 alice.parents << bob alice.save bob.chidren << cindy bob.save alice.parents.create(Contact.create(:name => 'Teresa', :gender => 'f') 

etc.

+2
source share
  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 I had to add: delete_sql to clear the relationship records. At first I used double quotes around the string, but found that it was causing errors. Switch to single quotes.

0
source share

All Articles