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