8

Estoy escribiendo un programa de tutoría para nuestra iglesia en los carriles (im todavía farily nuevo a los rieles) ..has_many: a través de múltiples relaciones has_one?

y tengo que modelar este ..

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 

Así que, básicamente, un objetos "hermanos" necesita asignar a todos los niños tanto del padre como de la madre sin incluir el objeto en sí ...

¿Esto es posible?

Gracias

Daniel

Respuesta

9

Es curioso cómo las preguntas que aparecen simples pueden tener respuestas complejas. En este caso, implementar la relación reflexiva padre/hijo es bastante simple, pero agregar las relaciones padre/madre y hermanos crea algunos giros.

Para empezar, creamos tablas para mantener las relaciones padre-hijo. Relación tiene dos claves externas, tanto que señala en contacto:

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 

En el modelo de relación señalamos el padre y la madre de vuelta a Contacto:

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 

y definir las asociaciones inversas en contacto:

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

Ahora una relación pueden ser creados:

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

Esto no es tan grande, lo que realmente queremos es construir la relación en una sola llamada:

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

por lo que podemos hacer:

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

Para encontrar a los niños de un contacto, agregar un ámbito a Contacto y (por conveniencia) un método de instancia:

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

Los hermanos son la parte difícil. Podemos aprovechar el método Contact.children y manipular los resultados:

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

Esto no es óptima, ya que father.children y mother.children se solaparán (por lo tanto la necesidad de uniq), y podría hacerse más eficiente resolviendo el SQL necesario (dejado como ejercicio :)), pero teniendo en cuenta que self.father.children y self.mother.children no se superpondrán en el caso de medios hermanos (mismo padre, diferente madre), y un Contacto podría no tener un padre o una madre

Éstos son los modelos completos y algunas de las especificaciones:

# 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

Usted señor es un monstruo de rieles y stackoverflow (especificaciones en sus respuestas !?) impresionante !! si pudiera besarte! Gracias :) –

+0

Ah ... una idea, ¿no sería útil agregar father_id y mother_id al modelo de contacto y luego agregar has_many: children,: class_name => "Contact",: finder_sql => 'SELECT * FROM contacts WHERE contacts .father_id = # {id} O contacts.mother_id = # {id} "y has_many: brothers,: class_name =>" Contact ",: finder_sql => 'SELECT * FROM contacts WHERE contacts.father_id = # {father_id} O contactos .mother_id = # {mother_id} '? Solo una idea: P –

+0

Podrías hacerlo en una tabla, pero eso te limitaría a las relaciones que se pueden especificar a través de las claves foráneas. Con una tabla separada, tienes la flexibilidad de especificar otros tipos de relaciones, como 'padrino' o 'tío'. – zetetic

2

estoy totalmente de acuerdo con Zetetic. La pregunta parece mucho más simple que la respuesta y hay poco que podamos hacer al respecto. Agregaré mi 20c sin embargo.
Tablas:

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 

las relaciones de tabla no tiene un modelo correspondiente.

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 

Ahora tenemos assosiations de Rails regulares. Entonces podemos

alice.parents << bob 
alice.save 

bob.chidren << cindy 
bob.save 

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

y todo eso.

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

Utilicé este ejemplo pero tuve que agregar el: delete_sql para limpiar los registros de relaciones. Al principio usé comillas dobles alrededor de la cadena, pero encontré que eso causó errores. Cambiar a comillas simples funcionó.

Cuestiones relacionadas