通过多个has_one关系实现has_many :through?

9

我正在为我们教堂编写一个基于rails的导师计划(我对rails还比较陌生)...

我需要对此进行建模...

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

基本上,一个对象的“兄弟姐妹”需要映射来自父亲和母亲的所有子项,不包括该对象本身。这是否可能?
谢谢, 丹尼尔
3个回答

10

有时候,看似简单的问题却可能有复杂的答案。在这种情况下,实现自反父子关系相对简单,但添加父母和兄弟关系则会产生一些问题。

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

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) # => [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

您是一位 Rails 和 Stack Overflow 的专家(您的回答中有规格!?)太棒了!!如果可以的话,我会亲吻您!谢谢 :) - Daniel Upton
啊...不过,有一个想法,是否可以将 father_id 和 mother_id 添加到联系人模型中,并添加 has_many:children,:class_name =>“Contact”,:finder_sql =>'SELECT * FROM contacts WHERE contacts.father_id = #{id} OR 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 - Daniel Upton
你可以在一个表中完成它,但这将限制你只能通过外键指定的关系。使用单独的表,你就有了灵活性来指定其他关系类型,比如“教父”或“叔叔”。 - zetetic
太棒了,谢谢 :) 我想我会在午餐时间将两者结合起来,并向模型添加3个外键father_figure_id、mother_figure_id和emergency_contact_id,并将其用于父母样式的东西。然后再添加一个多对多的样式,用于其他联系关系,如叔叔、表兄弟和朋友...感谢您的建议!哦,最后一个问题..如果关系是多对多的,has_one :father, :through => :relationships 如何工作?谢谢!:D - Daniel Upton
has_onehas_many的模式是相同的:一个表与第二个表相关联,第二个表有一个指向第一个表的外键。或者在:through的情况下,将键存储在连接表中。has_one基本上是has_many的特例,它不会通过关联添加多个对象。当然,这假定您使用has_one添加对象的方法--除非您向数据库添加约束,否则没有任何阻止您使用SQL添加多个fathers - zetetic

2

我完全同意zetetic的观点。这个问题看起来比答案简单得多,我们几乎无能为力。虽然如此,我还是会补充我的意见。
表格:

    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 以清理关系记录。一开始我在字符串周围使用了双引号,但发现那会导致错误。改为使用单引号就可以了。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接