To zabawne, jak pytania, które pojawiają się proste może mieć skomplikowanych odpowiedzi. W tym przypadku implementacja refleksyjnej relacji rodzic/dziecko jest dość prosta, ale dodanie relacji ojciec/matka i rodzeństwo powoduje kilka zwrotów akcji.
Aby rozpocząć, tworzymy tabele do przechowywania relacji rodzic-dziecko. Związek ma dwa klucze obce, zarówno wskazując na Kontakt:
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
w modelu związku zwracamy ojciec i matka z powrotem Kontakt:
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
i zdefiniowanie skojarzenia odwrotnych do kontaktu:
class Contact < ActiveRecord::Base
has_many :relationships, :dependent => :destroy
has_one :father, :through => :relationships
has_one :mother, :through => :relationships
end
teraz relacje mogą być tworzone:
@bart = Contact.create(:name=>"Bart")
@homer = Contact.create(:name=>"Homer")
@bart.relationships.build(:relation_type=>"father",:father=>@homer)
@bart.save!
@bart.father.should == @homer
To nie jest tak wielka, co naprawdę chcemy jest budowanie relacji w jednej rozmowy:
class Contact < ActiveRecord::Base
def build_father(father)
relationships.build(:father=>father,:relation_type=>'father')
end
end
więc możemy zrobić:
@bart.build_father(@homer)
@bart.save!
Aby znaleźć dzieci z kontaktu, dodaj zakres do kontaktu i (dla wygody) metodę instancji:
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")]
Rodzeństwo jest trudną częścią. Możemy wykorzystać metodę Contact.children i manipulować wyniki:
def siblings
((self.father ? self.father.children : []) +
(self.mother ? self.mother.children : [])
).uniq - [self]
end
ta nie optymalne, ponieważ father.children i mother.children będą się pokrywać (stąd konieczność uniq
) i może być bardziej efektywnie zrobić przez opracowanie niezbędnego kodu SQL (pozostawionego jako ćwiczenie :)), ale pamiętając, że self.father.children
i self.mother.children
nie zachodzą na siebie w przypadku połowy rodzeństwa (ten sam ojciec, inna matka), a kontakt może nie mieć ojca lub matką.
Oto kompletne modele oraz niektóre specyfikacje:
# 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
Jesteś sir i potwory stackoverflow (Specs w twoich odpowiedzi !?) niesamowite !! gdybym mógł cię pocałować! Dzięki :) –
Ah .. jeden pomysł, ale czy nie działałoby dodawanie father_id i mother_id do modelu kontaktu, a następnie dodawanie has_many: children,: class_name => "Contact",: finder_sql => 'SELECT * FROM contacts WHERE .father_id = # {id} LUB contacts.mother_id = # {id} "i has_many: siblings,: class_name =>" Contact ",: finder_sql => 'SELECT * FROM contacts WHERE contacts.father_id = # {father_id} LUB kontaktów .mother_id = # {mother_id} '? Tylko jeden pomysł: P –
Możesz to zrobić w jednej tabeli, ale to ograniczyłoby się do relacji, które można określić za pomocą kluczy obcych.Z oddzielną tabelą masz elastyczność, aby określić inne typy relacji, takie jak "ojciec chrzestny" lub "wujek" – zetetic