The connections between students and awards are workable, but the way that
the two models are handled by the web application doesn’t reflect their
actual roles in the data models. Depending on your application and your
preferences, this may be perfectly acceptable. There is, however, a
better way to represent the awards
model that more clearly reflects its relationship to students,
implemented in ch09/students002.
The models will stay the same, and the views will stay almost the same. The main things that will change are the routing and the controller logic. Chapter 13 will explain routing in much greater depth, but for now it’s worth exploring the ways that routing can reflect the relationships of your data models.
If the work involved in creating a nested resource seems
overwhelming, don’t worry. It’s not mandatory Rails practice, though
it is certainly a best practice. Unfortunately, it’s just complicated
enough that it’s hard to automate—but maybe someday this will all
disappear into a friendlier script/generate
command.
Near the top of the config/routes.rb file are the lines:
map.resources :awards map.resources :students
Delete them, and replace them with:
map.resources :students, :has_many => [ :awards ]
It’s another has_many
relationship, which is this time expressed as a parameter to map.resources
. You don’t need to specify the
belongs_to
relationship. Yes, this
is kind of a violation of “Don’t Repeat Yourself,” but at the same
time it expresses a resource relationship, not simply a data model
relationship.
You’ll still be able to visit http://localhost:3000/students/, but http://localhost:3000/awards/ will return an error. The
routing support that the link_to
methods expected when the original scaffolding was built has been
demolished. The views in the app/views/awards directory are now visible
only by going through students, and this change of position requires
some changes to the views.
Instead of the old URLs, which looked like:
http://localhost:3000/awards/2
the URLs to awards now follow a more complicated route:
http://localhost:3000/students/3
/awards/2
That added students/3
reflects that the award with the id
of 2
belongs to the student with
the id
of 3
.
While changing the routing is a one-line exercise, the impact on the controller is much more complicated. Most of it reflects the need to limit the awards to the specified student. Example 9-1 shows the new controller, with all changes bolded and commented. Most of the changes simply add the student object as context.
Example 9-1. Updating a controller to represent a nested resource
class AwardsController < ApplicationControllerbefore_filter :get_student
# :get_student is defined at the bottom of the file,
# and takes the student_id given by the routing and
# converts it to an @student object.
def index@awards = @student.awards
# was @awards = Award.find(:all)
respond_to do |format| format.html # index.html.erb format.xml { render :xml => @awards } end end def show@award = @student.awards.find(params[:id])
# was Award.find(params[:id])
respond_to do |format| format.html # show.html.erb format.xml { render :xml => @award } end end def new@award = @student.awards.build
# was @award = Award.new
respond_to do |format| format.html # new.html.erb format.xml { render :xml => @award } end end # GET /awards/1/edit def edit@award = @student.awards.find(params[:id])
# was @award = Award.find(params[:id])
end # POST /awards # POST /awards.xml def create@award = @student.awards.build(params[:award])
# was @award = Award.new(params[:award])
respond_to do |format| if @award.save flash[:notice] = 'Award was successfully created.' format.html {redirect_to([@student, @award])
}# was redirect_to(@award)
format.xml { render :xml => @award, :status => :created, :location => @award } else format.html { render :action => "new" } format.xml { render :xml => @award.errors, :status => :unprocessable_entity } end end end def update@award = @student.awards.find(params[:id])
# was @award = Award.find(params[:id])
respond_to do |format| if @award.update_attributes(params[:award]) flash[:notice] = 'Award was successfully updated.' format.html {redirect_to([@student, @award])
}# was redirect_to(@award)
format.xml { head :ok } else format.html { render :action => "edit" } format.xml { render :xml => @award.errors, :status => :unprocessable_entity } end end end def destroy@award = @student.awards.find(params[:id])
# was @award = Award.find(params[:id])
@award.destroy respond_to do |format| format.html {redirect_to(student_awards_path(@student))
}# was redirect_to(awards_url)
format.xml { head :ok } end endprivate
# get_student converts the student_id given by the routing
# into an @student object, for use here and in the view.
def get_student
@student = Student.find(params[:student_id])
end
end
Most of these changes, in some form or another, convert a
reference to awards generally to a reference to an award that applies
to a particular student. You’ll see some naming inconsistencies as
that context forces different syntax: find(:all)
simply disappears, new
becomes build
, and awards_url
becomes student_awards_path
. These new, different
methods are created automatically by Rails thanks to the routing
changes made earlier. Eventually these shifts will feel normal to
you.
The new AwardsController
uses
one new technique. It starts with a before_filter
, a call to code that will get executed before everything
else does. In this case, the before_filter
calls the get_student
method, which helps reduce the
amount of repetition in the controller. The controller will receive
the student_id
value from routing,
taking from the URL. Practically all of the time, though, it makes
more sense to work with the corresponding Student
object. The get_student
method takes the student_id
and uses it
to retrieve the matching object and place it in the @student
variable. That simplifies the
methods in the controller and will also be used in the views.
If users visit the new URLs at this point, they’ll get some strange results. Rails routing originally defined one set of methods to support the old approach, and not only the results but also the method names and parameters need to change.
In the old version of app/views/awards/index.html.erb, the Show/Edit/Destroy links looked like Example 9-2, while the updated version looks like Example 9-3. Updates are marked in bold.
Example 9-2. Code for displaying awards before nesting by student
<h1>Listing awards</h1> <table> <tr> <th>Name</th> <th>Year</th> <th>Student</th> </tr> <% for award in @awards %> <tr> <td><%=h award.name %></td> <td><%=h award.year %></td> <td><%=h award.student.name %></td> <td><%= link_to 'Show', award %></td> <td><%= link_to 'Edit', edit_award_path(award) %></td> <td><%= link_to 'Destroy', award, :confirm => 'Are you sure?', :method => :delete %></td> </tr> <% end %> </table> <br /> <%= link_to 'New award', new_award_path %>
Example 9-3. Displaying the awards on a student-by-student basis
<h1>Awards for <%=h @student.name %>
</h1><% if @student.awards.count > 0 %>
<table> <tr> <th>Name</th> <th>Year</th> </tr> <% for award in @awards %> <tr> <td><%=h award.name %></td> <td><%=h award.year %></td> <td><%= link_to 'Show',[@student, award]
%></td> <td><%= link_to 'Edit',edit_student_award_path(@student, award)
%></td> <td><%= link_to 'Destroy',[@student, award]
, :confirm => 'Are you sure?', :method => :delete %></td> </tr> <% end %> </table> <br /><% else %>
<p><%=h @student.given_name %> hasn't won any awards yet.</p>
<% end %>
<p><%= link_to 'New award', new_student_award_path(@student) %> |
<%= link_to 'Back', @student %>
</p>
In the new version, Example 9-3, the additional
information about the student informs nearly every interaction. The
headline (h1
) has acquired the name
of a specific student, rather than just being “Awards” generally.
There’s extra logic—the if
and
else
statements—to make sure that
awards are only displayed for students who have awards, presenting a
polite message for students without award.
The largest changes, however, are in the logic that creates
links. The Show and Destroy links change arguments, from just award
to [@student,
award]
, reflecting the additional information link_to
will need to create a proper link.
The links for Edit and New Award call a different method, new_student_award_path
, which will work
through the nested resource routing to generate a link pointing to the
right place. Given an argument for both a student and an award, it
will generate a link to edit that award; given just a student
argument, it will generate a link to create a new award for that
student.
There’s also a new Back link that goes back to the student’s page. That’s completely new navigation, necessary because of the extra context this page now has. Figure 9-8 shows what all of this looks like for Jules Miller, with his two awards, while Figure 9-9 shows the result for Milletta Stim, who hasn’t won any yet.
The changes to show.html.erb are smaller, turning the links from:
<%= link_to 'Edit',edit_award_path(@award)
%> | <%= link_to 'Back',awards_path
%>
to:
<%= link_to 'Edit',edit_student_award_path(@student, @award)
%> | <%= link_to 'Back',student_awards_path(@student)
%>
The information displayed is the same, and context has little effect except on the links. Everything still looks like Example 9-3, except that the URL is different and you’d see a different link in the status bar if you rolled over Edit or Back.
There are also some minor changes to new.html.erb and edit.html.erb. Both of them get new headlines:
<h1>New award for <%=h @student.name %></h1>
and:
<h1>Editing award for <%=h @student.name %></h1>
Both of them change their form_for
call from:
<% form_for(@award
) do |f| %>
to:
<% form_for([@student, @award]
) do |f| %>
Given an array of arguments instead of a single argument,
form_for
can automatically adjust
to get the routing right for its data. The rest of the form fields
look the same, except that the select
call to create the picklist for
students disappears completely, as that information comes from
context.
And yet again, the links at the bottom change (though only the second line applies to new.html.erb):
<%= link_to 'Show',[@student, @award]
%> | <%= link_to 'Back',student_awards_path(@student)
%>
Figure 9-10 shows the form for entering a new award in use, and Figure 9-11 shows the form for editing an existing award.
There’s one last set of things to address: adding links from the student views to the awards views. Awards used to have their own independent interface, but now they’re deeply dependent on students. There are only two places where adding links makes clear sense, though: in the index listing and in the view that shows each student.
In show.html.erb, add a link to the awards for the student between Edit and Back with:
<%= link_to 'Awards', student_awards_path(@student) %> |
As shown in Figure 9-12, that’ll give you a path to the awards for a student. (You might drop the existing list of awards there, too.)
That may actually be all the interface you want, but sometimes it’s easier to look at a list of students and click on an Awards button for them. To add that, you need to add a column to the table displayed in index.html.erb. Between the links for Edit and Destroy, add:
<td><%= link_to 'Awards', student_awards_path(student) %></td>
The result will look like Figure 9-13. If users click on the Awards links, that will bring them to pages like Figures 9-8 and 9-9.
Shifting awards from having their own interface to an interface
subordinate to students was a lot of work. It’s fairly clear why
nesting resources is the “right” approach in Rails—it makes the has_many
/belongs_to
relationship explicit on every
level, not just in the model. The work in the routing and the
controller establishes the changes necessary for both the regular web
user interface and the RESTful web services interface to work this
way. The views, unfortunately, take some additional effort to bring in
line, and you may have had a few ideas of your own while reading this
about how you’d like them to work.
In the abstract, nesting is a great idea, but at the same time, it requires a lot of careful work to implement correctly in the views layer. That work may or may not be your first priority, though if you’re going to nest resources, it’s easier done earlier in the implementation process rather than later.
If you’ve built nested resources, you may find situations where you need to build additional interfaces. Sometimes the supposedly subordinate model is the main one people want to work with. In the awards example, most of the time people might want to know what awards a student has received, or add an occasional award, and the nested interface will work just fine. However, if lots of awards are given out across an entire school at the end of the year, and one person has the task of entering every award into the system, that person might want a more direct interface rather than walking through the nesting. This situation could be addressed with an extra view that looked more like the ones earlier in the chapter.
Whether or not you decide to nest your own resources, you now have the information you need to do so, and you’ll know what you’re working with should you encounter Rails applications built using nested resources.