5.6. RESTful Routes for Associations

How should we RESTfully refer to actions associated with movie reviews? In particular, at least when creating or updating a review, we need a way to link it to a moviegoer and a movie. Presumably the moviegoer will be the @current_user we set up in Section 5.2. But what about the movie?

Chapter 7 discusses Behavior-Driven Design, which emphasizes that development should be driven by scenarios that describe actual user behaviors. According to this view, since it only makes sense to create a review when you have a movie in mind, most likely the “Create Review” functionality will be accessible from a button or link on the Show Movie Details page for a particular movie. Therefore, at the moment we display this control, we know what movie the review is going to be associated with. The question is how to get this information to the new or create method in the ReviewsController.

# in routes.rb, change the line 'resources :movies' to:
resources :movies do
resources :reviews
end
5.17
Figure 5.17: Specifying nested routes in routes.rb (top) also provides nested URI helpers (bottom), analogous to the simpler ones provided for regular resources.

One method we might use is that when the user visits a movie’s Detail page, we could use the session[], which persists across requests, to remember the ID of the movie whose details have just been rendered as the “current movie.” When ReviewsController#new is called, we’d retrieve that ID from the session[] and associate it with the review by populating a hidden form field in the review, which in turn will be available to ReviewsController#create. However, this approach isn’t RESTful, since the movie ID—a critical piece of information for creating a review—is “hidden” in the session.

A more RESTful alternative, which makes the movie ID explicit, is to make the RESTful routes themselves reflect the logical “nesting” of Reviews inside Movies, as the top part of Figure 5.17 shows. Since Movie is the “owning” side of the association, it’s the outer resource. Just as the original resources :movies provided a set of RESTful URI helpers for CRUD actions on movies, this nested resource route specification provides a set of RESTful URI helpers for CRUD actions on reviews that are owned by a movie. The bottom part of Figure 5.17 summarizes the new routes, which are provided in addition to the basic RESTful routes on Movies that we’ve been using all along. Note that via convention over configuration, the URI wildcard :id will match the ID of the resource itself—that is, the ID of a review—and Rails chooses the “outer” resource name to make :movie_id capture the ID of the “owning” resource. The ID values will therefore be available in controller actions as params[:id] (the review) and params[:movie_id] (the movie with which the review will be associated).

Figure 5.18 shows a simplified example of using such nested routes to create the views and actions associated with a new review. Of particular note is the use of a before-filter in ReviewsController to ensure that before a review is created, two conditions are true:

  1. @current_user is set (that is, someone is logged in and will “own” the new review).

  2. The movie captured from the route (Figure 5.17) as params[:movie_id] exists in the database.

 1class ReviewsController < ApplicationController
 2    before_filter :has_moviegoer_and_movie , :only => [:new, :create]
 3    protected
 4    def has_moviegoer_and_movie
 5        unless @current_user
 6            flash[:warning] = 'You must be logged in to create a review.'
 7            redirect_to login_path
 8        end
 9        unless (@movie = Movie.where(:id => params[:movie_id]))
10            flash[:warning] = 'Review must be for an existing movie.'
11            redirect_to movies_path
12        end
13    end
14
15    public
16    def new
17        @review = @movie.reviews.build
18    end
19
20    def create
21    # since moviegoer_id is a protected attribute that won't get
22    # assigned by the mass-assignment from params[:review], we set it
23    # by using the << method on the association. We could also
24    # set it manually with review.moviegoer = @current_user.
25    @current_user.reviews << @movie.reviews.build(params[:review])
26    redirect_to movie_path(@movie)
27    end
28end
<h1> New Review for <%= @movie.title %> </h1>

<%= form_tag movie_reviews_path(@movie), class: 'form' do %>
    <label class="col-form-label"> How many potatoes:</label>
    <%= select_tag 'review[potatoes]', options_for_select(1..5), class: 'form-control' %>
    <%= submit_tag 'Create Review', :class => 'btn btn-success' %>
<% end 
Figure 5.18: Top (a): a controller that manipulates Reviews that are “owned by” both a Movie and a Moviegoer, using before-filters to ensure the “owning” resources are properly identified in the route URI. Bottom (b): A possible view template for creating a new review, that is, app/views/reviews/new.html.erb.

If either condition is not met, the user is redirected to an appropriate page with an error message explaining what happened. If both conditions are met, the controller instance variables @current_user and @movie become accessible to the controller action and view.

The view uses the @movie variable to create a submission path for the form using the movie_review_path helper (Figure 5.17 again). When that form is submitted, once again movie_id is parsed from the route and checked by the before-filter prior to calling the create action. Similarly, we could link to the page for creating a new review by calling link_to with the route helper new_movie_review_path(@movie) as its URI argument.

Self-Check 5.6.1. Why must we provide values for a review’s movie_id and moviegoer_id to the new and create actions in ReviewsController , but not to the edit and update actions?

Once the review is created, the stored values of its movie_id and moviegoer_id fields tell us the associated movie and moviegoer.