8

我在写轨道指导方案为我们的教会(IM仍然farily新轨)..has_many:通过多个has_one关系?

,我需要模拟这种..

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 

所以基本上一个对象“兄弟姐妹”需要映射来自父亲和母亲的所有孩子,不包括对象本身。

这可能吗?

感谢

丹尼尔

回答

9

这很有趣,看似简单的问题怎么能有复杂的答案。在这种情况下,实施反身父母/子女关系相当简单,但添加父母/兄弟姐妹关系会产生一些曲折。

首先,我们创建表来保存父子关系。关系有两个外键,都指向联系人:

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 

在关系模型中,我们点了父亲和母亲回联系:

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 

,并在联系定义逆协会:

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

现在可以创建一个关系:

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

这是没有那么大,我们真正想要的是建立一个单一的调用关系:

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

,所以我们可以这样做:

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

要查找联系人的孩子,添加一个范围,以联系和(为方便起见)的实例方法:

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")] 

兄弟姐妹是棘手的部分。我们可以利用Contact.children方法和处理结果:

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

这不是最优的,因为father.children和mother.children将不重复(从而为uniq的需要),并且可以更有效地完成通过制定必要的SQL(留作练习:)),但请记住self.father.childrenself.mother.children在半兄弟姐妹(同一个父亲,不同的母亲)的情况下不会重叠,并且联系人可能没有父亲或一位母亲。

下面是完整的模型和一些规格:

# 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

你先生是一个轨道和stackoverflow怪物(规格在你的答案!?)真棒!如果我能我会吻你!感谢:) –

+0

啊..一个想法,但它不会工作添加到联系人模型father_id和mother_id,然后添加has_many:children,:class_name =>“联系人”,:finder_sql =>'选择*从联系人在哪里联系人.father_id =#{id}或contacts.mother_id =#{id}“和has_many:siblings,:class_name =>”Contact“,::finder_sql =>'SELECT * FROM contacts WHERE contacts.father_id =#{father_id} OR contacts .mother_id =#{mother_id}'?只是一个想法:P –

+0

您可以在一张表中完成,但这会限制您通过外键指定的关系。使用单独的表格,您可以灵活地指定其他关系类型,如'教父'或'叔叔' – zetetic

2

我完全同意探究性。这个问题看起来要简单得多,我们对此也没有什么可以做的。我会加上我的20c。
表:

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 

表的关系并没有相应的模型。

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 

现在我们有定期的Rails协会。所以我们可以

alice.parents << bob 
alice.save 

bob.chidren << cindy 
bob.save 

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

和所有类似的东西。

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}' 

我用这个例子,但不得不添加:delete_sql来清理关系记录。起初我在字符串周围使用双引号,但发现导致错误。切换到单引号工作。