有时候,看似简单的问题却可能有复杂的答案。在这种情况下,实现自反父子关系相对简单,但添加父母和兄弟关系则会产生一些问题。
首先,我们需要创建表来存储父子关系。关系有两个外键,都指向联系人:
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
并在 Contact 中定义反向关联:
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)
@homer.children
兄弟姐妹是棘手的部分。我们可以利用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.children
和
self.mother.children
不会重叠,并且一个联系人可能没有父亲或母亲。
这里是完整的模型和一些规格说明:
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)
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
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
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
has_one
和has_many
的模式是相同的:一个表与第二个表相关联,第二个表有一个指向第一个表的外键。或者在:through
的情况下,将键存储在连接表中。has_one
基本上是has_many
的特例,它不会通过关联添加多个对象。当然,这假定您使用has_one
添加对象的方法--除非您向数据库添加约束,否则没有任何阻止您使用SQL添加多个fathers
。 - zetetic