Monday, 29 March 2010

Rails - Day 12 -ActiveRecord Relationships Part I

Rails  - Day 1 - The RubyGem Love Story
Rails -  Day 10 - Testing Rails Controllers
Rails -  Day 13 -ActiveRecord Relationships Part II
Rails - Day 15 - Rails Forms Part II
In the last post, I touched briefly on unit testing and ActiveRecord.

I also mentioned that we are driving the model from the UI that has been created before the model:

I also confessed that the tight coupling to the database which is a result of the ActiveRecord pattern did not make me feel too good but is something I have learned to swallow in the name of productivity.  I will illustrate some of those productivity gains in this post that make the coupling compelling.

In the last post I created a migration and a test to create the expense object.

The Expense model object only encapsulates the Paid by Director, Expense Date, External Doc Ref and Posting Description from the above screenshot.

In this post, we will also create an ExpenseType model object and in the next post we will create an ExpensePayment model object that will be used to record our expense details. 

This is how our database schema should look when when the relationships have been defined:

If you look at the screenshot of the UI at the beginning of the post, you can see that there is an Expense Type dropdown.  Every expense that is created must have an expense type associated with it.  An example of an expense type might be travel costs or office equipment.

The first step is to create the association between expense and expense type.

The characteristics of the  Expense and ExpenseType relationship are:

  • Every Expense will have one ExpenseType associated with it.
  • Each ExpenseType can have many Expenses associated with it.

If you are familiar with Nhibernate, it can be put into the following terms:

  • An expense has a many-to-one relationship with ExpenseType.
  • An ExpenseType will have a one_to_many relationship with a Expenses.

We now need to create an ExpenseType model object.  In order to generate the appropriate model files, the following command is entered into the console:

ruby script/generate model ExpenseType which creates the following files:

Note that it is a model being generated and not a resource, no controller has been created for us and no routing has been updated.

The newly updated migration is updated to incorporate the attributes that will be required to record an expense type:

The db:migrate command will create the expense_types table.  I am not going to show the tests for this creation process, you will just have to take my word for it!! 

In a previous post, I mentioned how it was possible to seed the database with start up data by writing ruby script in the db/seeds.rb file.  In order to seed our database with some expense types, the following code is added to the db/seeds.rb file:

You can see here we are using the ActiveRecord find_or_create_by_name which will check for the existence an ExpenseType with the name that is passed to the function as an argument and if none is found, the record is created.  What you might not realise is that this method does not actually exist and is in fact an example of Ruby/ActiveRecord magic.  ActiveRecord supplies some convenience methods to find records by their attribute values.  These methods are called dynamic finders.  There are two forms of dynamic finder, find_by_xxxx and find_or_create_by_xxxx to create a record if you cannot find it.  If the ExpenseType class had an attribute named code, we could call a method named find_or_create_by_code.  We could also define the two fields to call a method named find_by_name_and_code.  This is fairly mind blowing stuff but when you actually look into the mechanics it seems less surreal.  If you are familiar with Ruby then it probably won't come as a great surprise to learn that this magic is carried out in the ruby catch-all for unknown methods called method_missing:

These posts are not about Ruby but ActiveRecord uses this technique to create many aesthetically pleasing convenience methods that we can utilise in order to both query and execute modifications against the database.  Much of the magic that occurs in Ruby takes place in method_missing.

Getting back to the seeds.rb file, this pseudo method, will create the record in the database if it encounters an unknown value for the name value.  What should not escape your attention that this is all happening in one line of code.  All the plumbing is happening in the framework.  It is examples like this that make me feel less queasy about the tight coupling between the database and the model.

The easiest way to see what records are in the database is to spark up the excellent rails console that runs in the context of our application.  If we enter the following command ruby script/console, we can execute ruby commands in the context of our current project.  Below, you can see I am typing ExpenseType.all to query the expese_types table and display the data that has been added via the seeds.rb file:

ActiveRecord Relationships

So far, we have created the Expense object, and also created and seeded the ExpenseType object but how is the relationship defined between the two objects?  Remember, we are saying that an Expense has one associated Expense Type.

In rails ActiveRecord, the relationship is expresssed as saying that an Expense belongs_to an ExpenseType.  belongs_to specifies a one to one association with another class.  This method should only be used if the class belonging to the other class contains the foreign key.

There is of course a convention for this foreign key relationship that negates the need to specify it anywhere in configuration or code.  The convention is that the foreign key takes the form of the name of the model followed by an underscore and id.  So in this case we will add a new field to the expenses table called expense_type_id.

As I mentioned in the last post, we are driving out our model test first using TDD like the good little ALT.NETters that we are and create the following test:

Obviously, this test will fail as the database schema has not been updated and the relationship has not been defined.

The first step is to create a new migration.  A migration is create with the following command: 

ruby script/generate migration add_expense_type_to_expense

The following code is added to the newly created migration file:

I run the migration with the rake db:migrate command.

There is one gotcha that has caught me out more than once and left me howling at the moon and to the Gods.  In a previous post, I mentioned rails environments  When we run the comand rake db:migrate, the migration is ran against the environment we are currently in which more than likely is not the test environment.  To run the migration against the test environment, we can either change environments by entering the command set RAILS_ENV = test or we can complete the task in one line as we are doing below with the command:

 rake environment RAILS_ENV=test db:migrate:

We are not done yet and we need to add some code to our model classes to specify the relationship.

The Expense model class is updated to the following:

The ExpenseType model class is updated to the following:

As aesthetics are really important with rails, the above belongs_to and has_many macros leave little to the imagination as to their true meaning.  

The above test can now be run with the command rake test:units or by by pressing ctrl + F9 in RubyMine.

In RubyMine we get the nice green bar that we all love and cherish:

In the next post, I will illustrate the relationship between the Expense model object and the yet to be created ExpensePayment model object.

No comments:

Post a Comment