8

Sto scrivendo un programma di tutoraggio per la nostra chiesa a rotaie (im ancora farily nuovo a rotaie) ..has_many: attraverso più has_one relazioni?

e ho bisogno di modellare questo ..

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 

Quindi, in pratica un oggetti "fratelli" ha bisogno di mappare tutti i bambini sia del padre che della madre, escluso l'oggetto stesso ..

È possibile?

Grazie

Daniel

risposta

9

E 'strano come le domande che appaiono semplici possono avere risposte complesse. In questo caso, l'implementazione della relazione genitore/figlio riflessiva è abbastanza semplice, ma l'aggiunta delle relazioni padre/madre e fratelli crea alcuni colpi di scena.

Per iniziare, creiamo tabelle per mantenere le relazioni padre-figlio. Relazione ha due chiavi esterne, sia che punta a contatto:

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 

Nel modello di rapporto si segnala il padre e la madre torna a contatto:

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 

e definire le associazioni inverse in contatto:

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

Ora un rapporto può essere creato:

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

Questo non è così grande, ciò che realmente vogliamo è quello di costruire il rapporto in una singola chiamata:

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

modo che possiamo fare:

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

di trovare i figli di un contatto, aggiungi uno scope a Contact e (per praticità) un metodo di istanza:

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

I fratelli sono la parte difficile. Possiamo sfruttare il metodo Contact.children e manipolare i risultati:

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

Questo non è ottimale, in quanto father.children e mother.children si sovrapporranno (così la necessità di uniq), e potrebbe essere fatto in modo più efficiente elaborando l'SQL necessario (lasciato come esercizio :)), ma tenendo presente che self.father.children e self.mother.children non si sovrappongono nel caso di fratellastri (stesso padre, madre diversa) e un contatto potrebbe non avere un padre o una madre.

Qui ci sono i modelli completi e alcune specifiche:

# 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

Tu signore sei un mostro e un mostro stackoverflow (Specifiche nelle tue risposte !?) fantastico !! se potessi, ti bacerei! Grazie :) –

+0

Ah .. una idea però, non funzionerebbe aggiungere padre_id e mother_id al modello di contatto e quindi aggiungere has_many: children,: class_name => "Contact",: finder_sql => 'SELECT * FROM contacts WHERE contacts .father_id = # {id} OR contacts.mother_id = # {id} "e has_many: fratelli,: class_name =>" Contatto ",: finder_sql => 'SELECT * FROM contatti WHERE contacts.father_id = # {father_id} O contatti .mother_id = # {mother_id} '? Solo un'idea: P –

+0

Puoi farlo in una tabella, ma questo ti limiterà alle relazioni che possono essere specificate attraverso le chiavi esterne. Con una tabella separata hai la flessibilità di specificare altri tipi di relazioni, come 'padrino' o 'zio'. – zetetic

2

Sono totalmente d'accordo con Zetetic. La domanda sembra molto più semplice della risposta e c'è poco che potremmo fare al riguardo. Aggiungerò comunque il mio 20c.
Tabelle:

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 

rapporti tabella non dispone di modello corrispondente.

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 

Ora abbiamo assunzioni Rails regolari. Quindi possiamo

alice.parents << bob 
alice.save 

bob.chidren << cindy 
bob.save 

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

e tutte le cose del genere.

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

Ho usato questo esempio ma ho dovuto aggiungere: delete_sql per pulire i record delle relazioni. All'inizio ho usato doppie virgolette attorno alla stringa ma ho scoperto che causava errori. Passando alle virgolette singole funzionate.

Problemi correlati