Thursday, September 29, 2011

Option 1

Option 1 is to separate out the relationships by the type of relationship it is.

If we want to be able to use a tree gem, such as acts_as_tree, on the data then we may want to include the child relationship in the Person class.  However, this won't work because the child has two parents, not one.  You could try to force the model to work by making all children depend on their mother or father, but then if you wanted to draw only the side of the family that the child didn't depend on you'd have to write extra complicated code going through the Spouse model. So for now let's say that we're going to have one entry in the child table for each parent of the child (Bob -> Brian and Betty -> Brian).  Since your knowledge of which person is a child's mother or father may change it's good to have the ability to change these relationships independently (for example, if it was discovered that Bob wasn't Brian's father, but that a second man, Ted, was).

So we have one table called Person that contains all of the person's information.  Then we have one table called Spouse that contains the spouses of a Person (or people who were spouses; if Brian and Daphne eventually get divorced they'd still have a relationship in the Spouses table).  Lastly we have one table called Child that contains the children of a Person.

create_table :person do |t|
  t.string :first_name
  t.string :last_name
end
create_table :child do |t|
  t.integer :person_id
  t.integer :child_id
end
create_table :spouse do |t|
  t.integer :person_id
  t.integer :spouse_id
  t.string :spouse_type
end



So let's model those tables in this way:

class Person < ActiveRecord::Base

  has_many :spouses
  has_many :children
end

class Spouse < ActiveRecord::Base
  belongs_to :person

end


class Child < ActiveRecord::Base
  belongs_to :person

end

Add some data:
01_fd - MySQL dump of version 1 of the family_development example database


So there are two different models to model the two different kinds of relationships.

1)  Spouses are undirected relationships.  Bob is Betty's spouse and Betty is Bob's spouse. They also have a relationship_type because the model needs to store whether or not they're currently married or were married (if we want to track and display that information; which you could argue that a genealogical system may not care about social relationships.  People tend to get upset if their adopted children aren't displayed on their family tree though...).
2)  Children are directed relationships.  Brian is Bob's child.  Brian is Betty's child.

So you could make a tree structure out of the Person and Child models, but you couldn't include the Spouse model.



Self-Referencing

In order to get to the information in the Person model from our relationship classes we need to make them self-referential.  Alter the belongs_to statements as follows.

class Person < ActiveRecord::Base
  has_many :spouses
  has_many :children
end

class Spouse < ActiveRecord::Base
  belongs_to :person, :foreign_key => "spouse_id"

end

class Child < ActiveRecord::Base
  belongs_to :person,  :foreign_key => "child_id"

end

You have to specify the foreign_key here because ActiveRecord will automatically look for a column named <class_name>_id.  In this case because we're joining to Person, it looks for person_id.  We can either create our Spouse model so that person_id is named something different or specify the foreign key that AR should use to join back to Person.


> bob = Person.find(1)
> bob.spouses.each do |spouse|
      puts spouse.person.first_name
   end
Betty
=> [#<Spouse id: 1, person_id: 1, spouse_id: 2 >]

If we knew that Bob only had one spouse then we could do:
>bob.spouses.first.person.first_name
>bob.spouses.first.person
=> #<Person id: 2, first_name: "Betty", last_name: "Smith" >

If we wanted information about Bob's children then we could do:
> bob.children.each do |child|
     puts child.person.first_name
   end
Brian
Daphne
=> [#<Child id: 1, person_id: 1, child_id: 3 >, #<Child id: 2, person_id: 1, child_id: 4 >]


Reciprocal Relationships

The question that then comes up when you're creating the data in the database (after doing rake db:migrate to create your tables), is do you include the reciprocal relationship in the Spouse table?  If Bob is Betty's husband is Betty also Bob's wife?

If you don't include the reciprocal relationship then you can only find the relationship from one person.
> bob = Person.find(1)
> bob.spouses
[ #<Spouse id: 1, person_id: 1, spouse_id: 2> ]
(notice that spouses returns an array)

> betty = Person.find(2)
> betty.spouses
[]

So then the Spouse model has to have reciprocal relationships in it, which seems like a bad idea because if Max and Barbie get divorced then you have to remember to update two entries in the Spouse table.

02_fd - MySQL dump of version 2 of the family_development example database (now including reciprocal relationships).

Parents

So we have children and spouses working, but now we want parents.  To do that we need to alter the Person model

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

> brian = Person.find_by_name("Brian")
> brian.parents
=> [#<Child id: 1, person_id: 1, child_id: 3 >, #<Child id: 3, person_id: 2, child_id: 3>]

Parent Person Information

Things seem to be going pretty good here.  So let's see if we can get Brian's relationships from the current set up.  Brian is the son of Bob and the father of Dan.  Let's see if we can find Dan.

> brian = Person.find_by_name("Brian")
> brian.children.each do |child|
      puts child.person.first_name
   end
Dan
=> [ #<Child id:6, person_id: 3, child_id: 7>]

Ok that works!  So now let's try Bob.

> brian.parents.each do |parent|
      puts parent.person.first_name
   end
Brian
Brian
=> [#<Child id: 1, person_id: 1, child_id: 3 >, #<Child id: 3, person_id: 2, child_id: 3>]

Uh oh.  So we're getting the right child objects, but we're not getting the correct person.  That's because we set up the join between Child and Person to be able to work for the children method (join on child_id).   It isn't possible to have two different relationships that are called the same thing, so we need to have two different methods to get the person information.

Since we need to split it into two methods, let's rename the original method to make it more clear what we're getting with that call.

class Child < ActiveRecord::Base
  belongs_to :get_child_info,  :foreign_key => "child_id"
  belongs_to :get_parent_info, :foreign_key => "person_id"
end

> brian.parents.each do |parent|
      puts parent.get_parent_info.first_name
   end
Bob
Betty
=> [#<Child id: 1, person_id: 1, child_id: 3 >, #<Child id: 3, person_id: 2, child_id: 3>]

> brian.children.each do |child|
      puts child.get_child_info.first_name
   end
Dan
=> [ #<Child id:6, person_id: 3, child_id: 7>]


Huzzah, it works!

Conclusion

Our final models are

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

class Spouse < ActiveRecord::Base
  belongs_to :person, :foreign_key => "spouse_id"
end
 

class Child < ActiveRecord::Base
  belongs_to :get_child_info,  :foreign_key => "child_id"
  belongs_to :get_parent_info, :foreign_key => "person_id"

end

These models let us do relationship.person.first_name to get the child's first name and relationship.info.first_name to get the parent's first name.  You can name the :person and :info methods in Child and Spouse anything that you want to if doing person.children.first.get_child_info.first_name or person.parents.first.get_parent_info.first_name is not intuitive to you.  The idea is that you want the methods to chain together in a readable way though, so make sure that reading a chain of methods together makes sense if you speak it out loud.

So this seems to be a pretty good option for modeling this particular family tree.  The downside is that you have to have reciprocal relationships in the database and you have two models for the relationships (spouse and child).  So if you wanted to draw one family tree then you'd need to get all of the child information for every individual in the tree and then merge the trees based on the information in the spouse model.  That could be done, but since there are several drawbacks to this modeling of the relationships, let's see what other options we can come up with.

No comments:

Post a Comment