5.4. Associations and Foreign Keys

An association is a logical relationship between two types of entities in a software architecture. For example, the previous CHIPS added a Moviegoer class to RottenPotatoes; we could now add a Review class to allow a moviegoer to write reviews of their favorite movies. Because each review is about exactly one movie, but a single movie can have many reviews, we say that there is a one-to-many association from reviews to movies. Similarly, there is a one-to-many association from moviegoers to reviews. Figure 5.9 shows these associations us- ing one type of Unified Modeling Language (UML) diagram. We will see more examples of UML in Chapter 11.

In Rails parlance, Figure 5.9 shows that:

  • A Moviegoer has many Reviews

  • A Movie has many Reviews

  • A Review belongs to one Moviegoer and to one Movie

In Rails, the “permanent home” for our model objects is the database, so we need a way to represent associations for objects stored there. Fortunately, associations are so common that relational databases provide a special mechanism to support them: foreign keys. A foreign key is a column in one table whose job is to reference the primary key of another table to establish an association between the objects represented by those tables. Recall that by default, Rails migrations create tables whose primary key column is called id. Figure 5.10 shows a Moviegoers table to keep track of different users and a Reviews table with foreign key columns moviegoer_id and movie_id, allowing each review to refer to the primary keys (ids) of the user who authored it and the movie it’s about.

5.10
Figure 5.10: In this figure, Alice has given 5 potatoes to Star Wars and 4 potatoes to Inception, Bob has given 3 potatoes to Inception, Carol hasn’t provided any reviews, and no one has reviewed It’s Complicated. For brevity and clarity, the other fields of the movies and reviews tables are not shown.

For example, to find all reviews for Star Wars, we would first form the Cartesian product of all the rows of the movies and reviews tables by concatenating each row of the movies table with each possible row of the reviews table. This would give us a new table with 9 rows (since there are 3 movies and 3 reviews) and 7 columns (3 from the movies table and 4 from the reviews table). From this large table, we then select only those rows for which the id from the movies table equals the movie_id from the reviews table, that is, only those movie-review pairs in which the review is about that movie. Finally, we select only those rows for which the movie id (and therefore the review’s movie_id) are equal to 41, the primary key ID for Star Wars. This simple example (called a join in relational database parlance) illustrates how complex relationships can be represented and manipulated using a small set of operations (relational algebra) on a collection of tables with uniform data layout. In SQL, the Structured Query Language used by substantially all relational databases, the query would look something like this:

# it would be nice if we could do this:
inception = Movie.where(:title => 'Inception')
alice,bob = Moviegoer.find(alice_id, bob_id)
# alice likes Inception, bob less so
alice_review = Review.new(:potatoes => 5)
bob_review   = Review.new(:potatoes => 3)
# a movie has many reviews:
inception.reviews = [alice_review, bob_review]
# a moviegoer has many reviews:
alice.reviews << alice_review
bob.reviews << bob_review
# can we find out who wrote each review?
inception.reviews.map { |r| r.moviegoer.name } # => ['alice','bob']
Figure 5.11: A straightforward implementation of associations would allow us to refer directly to associated objects, even though they’re stored in different database tables.
SELECT reviews.*
    FROM movies JOIN reviews ON movies.id=reviews.movie_id
    WHERE movies.id = 41;

If we weren’t working with a database, though, we’d probably come up with a design in which each object of a class has “direct references” to its associated objects, rather than constructing the query plan above. A Moviegoer object would maintain an array of references to Reviews authored by that moviegoer; a Review object would maintain a reference to the Moviegoer who wrote it; and so on. Such a design would allow us to write code that looks like Figure 5.11.

Rails’ ActiveRecord::Associations module supports exactly this design, as we’ll learn by doing. Apply the code changes in Figure 5.12 as directed in the caption, and you should then be able to start rails console and successfully execute the examples in Fig- ure 5.11.

# Run 'rails generate migration create_reviews' and then
#   edit db/migrate/*_create_reviews.rb to look like this:
class CreateReviews < ActiveRecord::Migration
    def change
        create_table 'reviews' do |t|
        t.integer    'potatoes'
        t.text       'comments'
        t.references 'moviegoer'
        t.references 'movie'
        end
    end
end
class Review < ActiveRecord::Base
    belongs_to :movie
    belongs_to :moviegoer
end
# place a copy of the following line anywhere inside the Movie class
#  AND inside the Moviegoer class (idiomatically, it should go right
#  after 'class Movie' or 'class Moviegoer'):
has_many :reviews
Figure 5.12: Top (a): Create and apply this migration to create the Reviews table. The new model’s foreign keys are related to the existing movies and moviegoers tables by convention over configuration. Middle (b): Put this new Review model in app/models/review.rb. Bottom (c): Make this one-line change to each of the existing files movie.rb and moviegoer.rb.

How does this work? Since everything in Ruby is a method call, we know that Line 8 in Figure 5.11 is really a call to the instance method reviews= on a Movie object. This instance method remembers its assigned value (an array of Alice’s and Bob’s reviews) in memory. Recall, though, that since a Review is on the “belongs to” side of the association (Review belongs to a Movie), to associate a review with a movie we must set the movie_id field for that review. We don’t actually have to modify the movies table. So in this simple example, the call to inception.reviews= isn’t actually updating the movie record for Inception at all: it’s setting the movie_id field of both Alice’s and Bob’s reviews to “link” them to Inception.

Figure 5.13 lists some of the most useful methods added to a movie object by virtue of declaring that it has_many reviews. Of particular interest is that since has_many implies a collection of the owned object (Reviews), the reviews method quacks like a collection. That is, you can use all the collection idioms of Figure 2.11 on it—iterate over its elements with each, use functional idioms like sort, map, and so on, as in lines 8, 10 and 13 of Figure 5.11.

What about the belongs_to method calls in review.rb? As you might guess, belongs_to :movie gives Review objects a movie instance method that looks up and returns the movie to which this review belongs. Since a review belongs to at most one movie, the method name is singular rather than plural, and returns a single object rather than an enumerable.

5.13
Figure 5.13: A subset of the association methods created by movie has_many :reviews and review belongs_to :movie, assuming m is an existing Movie object and r1,r2 are Review objects. Consult the ActiveRecord::Associations documentation13 for a full list. Method names of association methods follow convention over configuration based on the name of the associated mode

Self-Check 5.4.1. In Figure 5.12(a), why did we add foreign keys (references) only to the reviews table and not to the moviegoers or movies tables?

Since we need to associate many reviews with a single movie or moviegoer, the foreign keys must be part of the model on the “owned” side of the association, in this case Reviews.

Self-Check 5.4.2. In Figure 5.13, are the association accessors and setters (such as m.reviews and r.movie ) instance methods or class methods?

Instance methods, since a collection of reviews is associated with a particular movie, not with movies in general.