8

Piszę program mentoringu dla naszego kościoła w szynach (IM wciąż farily nowy na szynach) ..has_many: przez wiele relacji has_one?

i muszę tego modelu ..

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 

Więc w zasadzie obiektów „rodzeństwo” musi odwzorować wszystkie dzieci z ojca i matki, nie uwzględniając samego obiektu ..

Czy to możliwe?

Dzięki

Daniel

Odpowiedz

9

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 
+0

Jesteś sir i potwory stackoverflow (Specs w twoich odpowiedzi !?) niesamowite !! gdybym mógł cię pocałować! Dzięki :) –

+0

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 –

+0

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

2

I całkowicie zgadzam się z zetetic. Pytanie jest o wiele prostsze niż odpowiedź i niewiele możemy z tym zrobić. Dodam jednak moje 20c.
Stoły:

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 

Tabela stosunki nie mają odpowiedniego wzoru.

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 

Teraz mamy standardowe asocjacje Rails. Tak więc możemy i wszystkie takie rzeczy.

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

Użyłem tego przykładu, ale musiałem dodać: delete_sql, aby wyczyścić rekordy relacji. Najpierw użyłem podwójnego cudzysłowu wokół ciągu, ale okazało się, że spowodowało błędy. Zmiana na pojedyncze cytaty zadziałała.

Powiązane problemy