has_many :through multiple has_one relationships?

前端 未结 3 1792
盖世英雄少女心
盖世英雄少女心 2020-12-28 11:15

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         


        
相关标签:
3条回答
  • 2020-12-28 11:48

    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.

    0 讨论(0)
  • 2020-12-28 11:50
      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.

    0 讨论(0)
  • 2020-12-28 11:55

    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
    
    0 讨论(0)
提交回复
热议问题