The most important component of the Rails MVC stack is the model layer.
ActiveRecord, Rails’ built-in Object-Relational Mapper, makes working with databases easy. Let’s look at the basic relationships in a traditional database and how they’re implemented in
There are three core relationships:
The simplest but most under-utilized relationship is one-to-one. It is best applied when you have an object that has more than one role in the system.
For instance, imagine an e-commerce site. We have customers, and for each of those customers we want to track information like:
- First Name
- Last Name
- Home City
- Email Address
Filtering that into a table
customers would result in these columns:
It’s most common to drop all these attributes into a single model, but does that really make sense?
Imagine we want to display a message like "Hello, John" in the header of each page. In our controller we would say
Customer.find(17) and use
@customer.first_name in our view. Rails will run a SQL statement like this:
And, of course, that’s going to fetch the entire row from our
customers table. We’re trying to get the
first_name, so why bother fetching the
home_city every request? Instead, we could create a one-to-one relationship between a customer and their "details".
We create a new table named details with just these columns:
Now that table can hold any small, infrequently used bits of information and we’ll improve application performance.
To explain this relationship to Rails we use the methods
belongs_to. Which is which? The side that holds a foreign key, here the
customer_id, is effectively the child which
belongs_to the parent.
1 2 3 4 5 6 7
Using the Objects
Now when we could find a customer by the ID 17 with
Customer.find(17). That object would just have the essential fields.
To access the details, we could do something like
Customer.find(17).detail.birthday and it would work just fine.
In this kind of scenario, we want every customer to have an associated
In pseudocode, what we want to do is "create an attached
Detail object whenever a
Customer object is initialized". To implement that in code we can use the
1 2 3 4 5 6
Now when we call
Customer.new it will automatically build a
Detail and associate them in memory. Once the
Customer is saved it will get an ID from the database and that ID will be stored in the
customer_id field of the
Detail. We only want to build a Detail object if we don’t already have one, so we add the if detail.nil? condition to avoid replacing an existing Detail object.
Given the current setup, when we destroy a
Customer it is going to leave an orphaned
Detail object in the database. Instead, we want the child object destroyed automatically when the parent is destroyed. That’s accomplished with this change to the
Hiding the Child Object
Reaching through an object, talking to the object’s child, and calling methods is a violation of the "Law of Demeter" (http://avdi.org/devblog/2011/07/05/demeter-its-not-just-a-good-idea-its-the-law/). Instead, the parent object should present an interface to the child object. From the outside, we shouldn’t know the
Detail object exists at all!
To do that we use Rails’
1 2 3 4 5 6 7 8 9
Then when we call
Customer.find(17).city it will proxy the call to the associated
Detail object, fetch it from the database if it hasn’t already been loaded, and return us the value returned from the
Detail object’s method. All of this is transparent from the outside.
Simplifying the Proxy
One small catch here is that
delegate only handles the listed methods, so if you want to have full read/write access to the child’s attributes you’d need:
The list starts to get long, and if we add methods to
Detail we need to remember to add them to the delegation. Programmers don’t remember things, so here’s one solution:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
The element that you’re likely to notice here is the use of
*, Ruby’s splat operator. It breaks apart the array into a list of parameters, resulting in identical functionality to when we listed them all right in the call to
One-to-many is the most common relationship in Rails. One customer connects to many orders. One article connects to many comments. The relationship is asymmetric because the parent has many children, but the child has only one parent.
In the Database
At the database level it is very similar to a one-to-one: the "child" record holds a foreign key pointing back to the "parent". The parent record or row actually has no idea how many children are out there and only the child links back to the parent.
In Your Models
Let’s say we’re going to connect our
Customer object with multiple
Order objects. In our Rails models:
1 2 3 4 5 6 7
has_many :orders tells Rails to expect a model named
Order that has a foreign key named
belongs_to :customer tells Rails that
customer_id points to the ID field of a class named
Customer or table named
customers. All these expectations can be overridden as we’ll see soon.
Building Child Objects
When you create a
Customer it won’t have any child
Order objects. Here are three ways to create one, assuming we have a
Order.new(customer_id: customer.id)– least preferred. It has no future flexibility if we change details like the foreign key name
Order.new(customer: customer)– better. It created the object through the
ActiveRecordrelationship, so we can handle the details in that relationship.
customer.orders.new– best. The order is built directly off the relationship, hiding all the details. We can add things like a validation on customer that they don’t have more than X open orders or whatever else applies to our domain. Note that
customer.orders.buildis equivalent to calling
Just like the
has_one relationship, we frequently want the child objects to be destroyed when the parent is destroyed. We add the same
dependent option to the
1 2 3
Strictly speaking, there is no such thing as a many-to-many relationship. It is only achieved through the composition of two one-to-many relationships via a join model.
In the early days of Rails we used
has_and_belongs_to_many to handle this relationship. A typical example might imagine a publisher of magazines: a customer connects to many magazines and a magazine connects to many customers.
Using HABTM, we would have modeled it like this:
1 2 3 4 5 6 7
This told Rails that there was a table
magazines, a table
customers, and a table
customers_magazines (named by alphabetizing the two table names). Neither
customers hold any foreign keys. Each row of the join table,
customers_magazines, has a foreign key connecting to one row from
magazines and one row from
Magazine related to many rows in
magazine_customers which, in turn, each related to one
Customer. The reverse is also true, completing the illusion of many magazines connecting to many customers.
For all later examples in this chapter, assume
@magazine is an instance of
@customer an instance of
Customer. We could then hop between them like this:
1.9.2p320 :001> 1.9.2p320 :002>
These days, through, HABTM is deprecated and should not be used.
Has Many Through
As our experience with HABTM grew, it became apparent that, in almost every circumstance, the join model should not just be an implementation detail. Instead, it is usually the sign of a missing domain model.
Consider this example of
Magazine. What is their connection? It should be a
Subscription. It deserves promotion to a proper model:
1 2 3 4 5 6 7 8 9 10 11 12
Why does it deserve to be a model in its own right? As apps grow we often want to store data in the connection. What day did the customer subscribe to the magazine? When does the subscription expire? What promotion code did they use to sign up? This data could probably be hacked into the
customers table, but it really belongs in the join – the
What about hopping between the models? We can do this:
1.9.2p320 :001> 1.9.2p320 :002> 1.9.2p320 :003> 1.9.2p320 :004>
But we’ve lost the elegance of hopping directly from a
Magazine to its associated
The solution is to add a second relationship to each of the primary models:
1 2 3 4 5 6 7 8 9
Using "has many through", Rails can hop across the intermediary relationship. We can now call
@customer.subscriptions when we want to work with the join, and
@customer.magazines when we don’t. Similarly,
@magazine.customers. We have the elegance of HABTM, but the ability to put logic and intelligence in the join.
- Ruby’s splat operator: http://kconrails.com/2010/12/22/rubys-splat-operator/