I’ve been trying to do some additional work on my
ember.js extension for data management.
At the same time though, I’ve been trying (to learn and) build a simple
Ruby on Rails web demo application using the
new JavaScript library. There have been more than a few things that have mystified
me about the framework and the structuring of an application. One aspect in
particular was how to best manage foreign keys and join tables with the ActiveRecord
class (and the corresponding SQL tables). So many tutorials have the same lame
example of: a CART, an ORDER, a CUSTOMER …, that it’s often difficult to apply the
same patterns to a more interesting system.
I started simple this time.
I wanted a PERSON class and a GIFT class.
A Person has been given gifts and may give gifts (and a few other common
attributes).
class CreatePeople < ActiveRecord::Migration
def change
create_table :people do |t|
t.string :first_name
t.string :last_name
t.date :date_of_birth
t.string :email_address
t.timestamps
end
end
end
One of the things that I can’t decide if I like is the automatic pluralization of
words, especially People/Person. I would have been content with a Persons table, but
when creating a model, by default (as I’m aware it can be overridden), a Person is
mapped to a table called “People.”
The second table, Gifts is very simple:
class CreateGifts < ActiveRecord::Migration
def change
create_table :gifts do |t|
t.string :description
t.integer :from_person_id
t.integer :to_person_id
t.timestamps
end
end
end
As I thought I might want a richer structure for the Gift class in the future, I did
not use the more standard “person_id” name for the foreign key column that maps a
gift to a Person. I wanted the column name to be more obvious what it was.
Additionally, I needed two columns that both mapped to a “Person", so I
couldn’t have both be called “person_id” anyway.
By deviating from the normal pattern, there are a few expectations when defining the
ActiveRecord class. It was these expectations that weren’t clear to me (especially
with examples).
The Ruby class for Gift is defined like so:
class Gift < ActiveRecord::Base
belongs_to :from_person, :class_name => "Person", :foreign_key => "from_person_id"
belongs_to :to_person, :class_name => "Person", :foreign_key => "to_person_id"
end
and the Person:
class Person < ActiveRecord::Base
has_many :gifts_given , :class_name => "Gift", :foreign_key => "from_person_id"
has_many :gifts, :foreign_key => "to_person_id"
end
The key (and the ‘ah ha’ moment for me) was the use of the foreign_key parameter to
the on the has_many and belongs_to associations.
In the Gift class, I included the belongs_to association macro. In
this case, :from_person is the name of the rich accessor method
(which looks like a property in other languages) that will be added to the Gift
class. Using the symbol :class_name is like a class finding
assistant. Without it, the Rails framework assumes that there would be a class named
“FromPerson.” Of course, that would fail. By specifying “Person,” I’ve indicated to
Rails that the class it should map to is called “Person” which I defined earlier.
The :foreign_key symbol and value indicates which column in the
backing table has the value which will map to an instance of a Person. In the SQL
table, I added a “from_person_id” column and this points at that as
the “from_person_id” column is the foreign key to the People table. (The same is
true for :to_person.)
Looking at the Person class, it is using another common association macro,
:has_many. :Has_many when used here, is indicating
that a Person may have zero or more “gifts.” The new accessor method is named
gifts (by using :gifts). Here, too, you’ll specify
the name of the foreign_key. Again, repeat this for the
:gifts_given automatically added accessor method. One interesting
thing is that only :gifts_given requires the :class_name
to be specified. The reason is that Rails automatically maps
:gifts to the Gifts class (by way of naming). The :gifts_given
cannot be automatically mapped, so you need to give (sigh) it a little help.
Here’s a little test:
>> jason = Person.find(1)
Person Load (18.0ms) SELECT "people".* FROM "people" WHERE "people"."id" = ? LIMIT 1 [["id", 1]]
#<Person id: 1, first_name: "Jason", last_name: "Bourne", date_of_birth: nil, email_address: nil, created_at: "2012-01-06 13:47:40", updated_at: "2012-01-07 03:10:29">
>> jason.gifts_given
Gift Load (0.0ms) SELECT "gifts".* FROM "gifts" WHERE "gifts"."from_person_id" = 1
[#<Gift id: 1, description: "Machine Gun", from_person_id: 1, to_person_id: 4, created_at: "2012-01-07 14:39:17", updated_at: "2012-01-07 14:39:17">]
>> jason.gifts_given[0].to_person.first_name
"Magnum"
Person Load (0.0ms) SELECT "people".* FROM "people" WHERE "people"."id" = 4 LIMIT 1
>> jason.gifts_given[0].to_person.gifts
[#<Gift id: Gift Load (1.0ms)1 SELECT "gifts".* FROM "gifts" WHERE "gifts"."to_person_id" = 4
, description: "Machine Gun", from_person_id: 1, to_person_id: 4, created_at: "2012-01-07 14:39:17", updated_at: "2012-01-07 14:39:17">]
>> jason.gifts_given[0].to_person.gifts[0].from_person.first_name
Person Load (0.0ms) SELECT "people".* FROM "people" WHERE "people"."id" = 1 LIMIT 1
"Jason"
I added two people: Jason, and Magnum, and one gift before executing the code above.
Jason, as you should be able to follow, gave a wonderful gift to Magnum. As you can
see, by using the automatically added accessor methods by way of the association
macros described above, I was able to traverse the database structure very easily
when mapped to a few simple objects.
One plus of experimenting and testing with the console while using Rails/Ruby in
this case is that the output includes the SQL commands that are executed when the
various method calls are made. Here’s an example where I rolled multiple calls into
one chained call:
>> jason = Person.find(1).gifts_given[0].to_person.first_name
Person Load (19.0ms) SELECT "people".* FROM "people" WHERE "people"."id" = ? LIMIT 1 [["id", 1]]
Gift Load (0.0ms) SELECT "gifts".* FROM "gifts" WHERE "gifts"."from_person_id" = 1
"Magnum"
Person Load (0.0ms) SELECT "people".* FROM "people" WHERE "people"."id" = 4 LIMIT 1