Monday, October 3, 2011

Option 2

Moving on to option 2.  Let's try to make one model that will contain all of the family relationships whether they're bidirectional (marriages) or unidirectional (children/parents).

create_table :person do |t|
  t.string :first_name
  t.string :last_name
end
create_table :relationships do |t|
  t.integer :parent_id
  t.integer :child_id
  t.string :name
  t.string :relationships
end

We don't want to have to have the reciprocal relationships like we did in our last modeling option, so let's create some data but not add the reciprocal relationships for now to see if we can get it to work.  We still have to add dual entries for each child though.

We know from the last exercise that we need to specify a foreign_key and two different relationships for children and parents in our Person model, but we're not sure about the Relationships model, so let's leave that vanilla for now and we'll see what we need after we've created the first version of the models.


class Person < ActiveRecord::Base
  has_many :children, :class_name => "Relationship", :foreign_key => "parent_id"
  has_many :parents, :class_name => "Relationship", :foreign_key => "child_id"
end

class Relationships < ActiveRecord::Base
  belongs_to :parent, :class_name => "Person"
  belongs_to :child, :class_name => "Person
end

 Let's add some data:
03_fd - MySQL dump of version 3 of the family_development example database (people and relationships, duplicate child relationships, but non-reciprocal marriages).  Note that marriages are coded with the male first (this is standard pedigree convention).

Starting up our rails console we can test our previous methods:

> brian = Person.find_by_first_name("Brian")
> brian.parents.each do |parent|
     puts parent.get_parent_info.first_name
   end
Bob
Betty
=>  [#<Relationship id: 1, parent_id: 1, child_id: 3, name: "son", relationship_type: "child">, #<Relationship id: 3, parent_id: 2, child_id: 3, name: "son", relationship_type: "child"

Great so parents works just as we'd expect it!

>brian.children.each do |child|
    puts child.get_child_info.first_name
  end
Dan
Daphne
 => [#<Relationship id: 6, parent_id: 3, child_id: 7, name: "son", relationship_type: "child">, #<Relationship id: 10, parent_id: 3, child_id: 4, name: "marriage", relationship_type: "spouse"]

Uh oh!  Now we're getting that Daphne is Dan's child because the :children association doesn't filter on the type of relationship.  Let's fix that.


class Person < ActiveRecord::Base
  has_many :children, :class_name => "Relationship", :foreign_key => "parent_id", :conditions => { :relationship_type => "child" }
  has_many :parents, :class_name => "Relationship", :foreign_key => "child_id", :conditions => { :relationship_type => "child" }
end


>brian.children.each do |child|
    puts child.get_child_info.first_name
  end
Dan
 => [#<Relationship id: 6, parent_id: 3, child_id: 7, name: "son", relationship_type: "child">]

If we try this with Daphne, let's see what we get

> daphne = Person.find_by_first_name("Daphne")
> daphne.children.each do |child|
       puts child.get_child_info.first_name
   end
Dan
 => [#<Relationship id: 6, parent_id: 3, child_id: 7, name: "son", relationship_type: "child">]

> daphne.parents.each do |parent|
     puts parent.get_parent_info.first_name
   end
=> []

So Daphne doesn't have any parents currently.  We could add two child relationships between her and Bob and Betty and indicate that the relationship_type was 'child' and the name was 'daughter-in-law', but for marriages it's probably easier to just get to Bob and Betty via something like daphne.husband.parents.  For adopted children, you'd want to add another field to the model that would be adopted_status and then have a method called is_adopted that would return that flag.

Husbands and Wives

 Ok, so now parents and children are working fine, but as we can see we need to be able to do daphne.husband and brian.wife.  Since we're not adding the duplicate relationships into the database and we have a convention where the husband is always the parent_id and the wife is always the child_id, then we can do this with two has_many relationships:

class Person < ActiveRecord::Base
  has_many :parents, :class_name => "Relationship", :foreign_key => "child_id", :conditions => { :relationship_type => "child" }
  has_many :children, :class_name => "Relationship", :foreign_key => "parent_id", :conditions => { :relationship_type => "child" }
  has_many :husbands, :class_name => "Relationship", :foreign_key => "child_id", :conditions => { :relationship_type => "spouse" }
  has_many :wives, :class_name => "Relationship", :foreign_key => "parent_id", :conditions => { :relationship_type => "spouse" }
end

We know that we need to specify the relationship_type is a spouse.  We know that the foreign_key for husbands is child_id and that the foreign_key for wives is parent_id.  If we wanted to have a generic method to call on a person to see if that person has a spouse (either husband or wife) then we could create a method called spouses:

  def spouses
    husbands = self.husbands
    wives = self.wives
    spouses = husbands + wives
    return spouses
  end

However the caveat with this is that you have to know what kind of a relationship it is in order to get the information about the person from the Person table.  What happens if Barbie divorced Max and entered into a civil union with a woman named Tammy?  If you're not careful about which woman gets entered in the parent_id and which in the child_id, then you can end up with two different kinds of relationships - one where Barbie's ID is in the parent_id column and one where her ID is in the child_id column.

The relationship with her ID in the parent_id column would get her spouse using .wives and the one with her ID in the child_column would get her spouse using .husbands.  Then in order to get the information about the person you'd have to call get_child_info and get_parent_info respectively, which makes it hard to do a loop over the resulting array and get sensible information without a lot of code caveats.  So the conclusion here is that a spouses method is a bad idea because you don't know which type of relationship you're getting returned and you don't have reciprocal relationships.

So what about adding the reciprocal relationships in and condensing husbands and wives into spouses where you always check for one of the ids.  You can set it up so that it either checks the child_id or the parent_id as long as you're consistent in which one it checks and you use the correct method (get_child_info or get_parent_info) depending on which way you set it up (or create a get_spouse_info that is appropriate).

class Person < ActiveRecord::Base
  has_many :parents, :class_name => "Relationship", :foreign_key => "child_id", :conditions => { :relationship_type => "child" }
  has_many :children, :class_name => "Relationship", :foreign_key => "parent_id", :conditions => { :relationship_type => "child" }
   has_many :spouses, :class_name => "Relationship", :foreign_key => "parent_id", :conditions => { :relationship_type => "spouse" }
end

class Relationship < ActiveRecord::Base
  belongs_to :get_parent_info, :class_name => "Person", :foreign_key => "parent_id"
  belongs_to :get_child_info, :class_name => "Person", :foreign_key => "child_id"
  belongs_to :get_spouse_info, :class_name => "Person", :foreign_key => "child_id"
end

04_fd - MySQL dump of version 3 of the family_development example database (people and relationships, duplicate child relationships, with reciprocal marriages). 


With this set up you can do:

> barbie = Person.find_by_first_name("Barbie")
> barbie.spouses.map(&:get_spouse_info)
=> [#<Person id: 9, first_name: "Tammy", last_name: "Saint">, #<Person id: 6, first_name: "Max", last_name: "Payne">, <Person id: 8, first_name: "Terry", last_name: "Jones">]

Conclusion

So you don't have to do include reciprocal relationships in relationships table, but if you don't then you end up either doing a lot of code on the data entry part (CRUD) or the display part (husbands and wives calls) to display the information.  If you do include the reciprocal relationships then you need to do code on the data entry (CRUD) but not on the display.  So is it easier to write code to always put the person in the correct parent_id or child_id field or is it easier to write code that creates, deletes, or updates a reciprocal relationship?  Either way you need to write tests to make sure that the database isn't corrupted (wrong information).  

Is there another way?  Is this easier than the two models from option 1?