The other frequent relationship between tables or models is many-to-many. A student, for example, can be taking zero or more courses, while a course can have zero or more students. (Students with zero courses might not yet have registered for anything, while courses with zero students might be awaiting registration or just unpopular.)
The relationship between the two is, from a modeling standpoint, even, so there won’t be any need for nested resources, just a lot of connections. As usual, it makes sense to move up from the database through models to controllers and views to produce the code in ch09/students003. And also as usual, while Rails provides you with a foundation, you’re still going to need to add a lot to that foundation.
Remember, don’t name a table “classes,” or you will have all kinds of strange Rails disasters because of name conflicts. “Courses” is a safer option.
Building a many-to-many relationship requires creating tables—not just a single table, but a many-to-many relationship that will require adding two tables beyond the student table already in the application. One will be the actual course list, and the other the table that joins courses to students, as shown in Figure B-3 of Appendix B. Creating the course list—which will need a full set of scaffolding—is simple:
script/generate scaffold course name:string
Creating the join table requires an extra few steps. Start by creating a migration:
script/generate migration CreateCoursesStudents
Doing this will create a migration file in db/migrate with a name that ends in create_courses_students.rb. Unfortunately, when you open it, all you’ll see is:
class CreateCoursesStudents < ActiveRecord::Migration def self.up end def self.down end end
Once again, you’ve reached the boundaries of what autogenerated code will do for you, for the present. Creating the connecting table will require coding the migration directly. A simple approach, building just on what you’ve seen in previous generated migrations, looks like:
class CreateCoursesStudents < ActiveRecord::Migration def self.upcreate_table :courses_students, :id => false do |t|
t.integer :course_id, :null => false
t.integer :student_id, :null => false
end
end def self.downdrop_table :courses_students
end end
All of this depends on meeting Rails’ expectations for naming
conventions. The table name is the combination of the two models being
joined in alphabetical order, and the fields within the table are
id
values for each of the other
models. Like all good migrations, it has a self.up
method for creating the table and a
self.down
that removes it.
There is one performance-related issue to consider here. Rails
has used the id
value for tables as
its main approach for getting data into and out of them rapidly. The
id
value, which you don’t have to
specify, is automatically indexed. If you want your application to be
able to move through the many course_id
and student_id
values in this table, however,
you’ll need to add an index, as in:
class CreateCoursesStudents < ActiveRecord::Migration def self.up create_table :courses_students, :id => false do |t| t.integer :course_id, :null => false t.integer :student_id, :null => false end# Add index to speed up looking up the connection, and ensure
# we only enrol a student into each course once
add_index :courses_students, [:course_id, :student_id], :unique => true
end def self.downremove_index :courses_students, :column => [:course_id, :student_id]
drop_table :courses_students end end
Indexes will be explained in greater detail in Chapter 13. Before moving on to the next steps,
run rake db:migrate
to build your
tables.
Like has_many
and belongs_to
, has_and_belongs_to_many
is a declaration that goes in the model. In app/models/student.rb, add:
# a student can be on many courses, a course can have many students
has_and_belongs_to_many :courses
And in app/models/course.rb, add:
# a student can be on many courses, a course can have many students
has_and_belongs_to_many :students
That’s all you need to do to establish the connection. Rails
will automatically—thanks to naming conventions—use the courses_students
table you built to keep
track of the connections between students and courses.
You may find it useful to add some convenience methods to the
model, depending on what you need in your interfaces. In the students
model, it makes sense to add some
logic that answers basic questions and returns some information that
Rails won’t provide automatically. These build, of course, on the
courses
object that Rails did add
to the model. First, a convenience method checks to see whether a
given student is enrolled in a specified course:
def enrolled_in?(course) self.courses.include?(course) end
The enrolled_in?
method uses
the include?
method of courses
to check whether a particular course
is included in the list. If it is, then the student is enrolled, and
include?
and enrolled_in?
will both return true
. Otherwise, they return false
.
The enrolled_in?
convenience method will get called many times as the number of
courses grows, executing the same query repeatedly. For now, its
clarity is probably more important than its performance, but as you
get more familiar with how Rails interacts with databases, you will
want to optimize this method for better performance.
A similarly useful convenience method returns the list of courses that a student is not yet enrolled in, making it easy to create logic and forms that will let them enroll:
def unenrolled_courses Course.find(:all) - self.courses end
This one-liner does some tricky set arithmetic. First, it calls
Course.find(:all)
to get a full
list of all the courses available. Then it calls self.courses
to get a list of the courses
that already apply to this particular student. Finally, it does
subtraction—set subtraction—removing the courses in self.courses
from the full list. The
-
doesn’t just have to mean
subtracting one number from another.
Many-to-many relationships don’t demand the kinds of controller change that nested resources did. You don’t need to change method calls inside of the generated code, but you may want to add some additional methods to support functionality for both courses and students. While the added methods in the models focused on data manipulation, the methods in the controllers will add logic supporting interfaces to that data. The basic RESTful interfaces will remain, and the new interfaces will supplement them with some extra functionality specific to the combination of the two models.
In app/controllers/courses_controller.rb, the currently simple application only needs one extra method:
# GET /courses/1/roll def roll @course = Course.find(params[:id]) end
The roll
method, which will
need a roll.html.erb view, will
just provide a list of which students are in a given course, for roll
call. The :id
parameter will
identify which course needs a list.
There’s more to add in app/controllers/students_controller.rb, as we need a way to add students to and remove them from courses. First, though, it makes sense to create a means of listing which courses a student is in:
# GET /students/1/courses def courses @student = Student.find(params[:id]) @courses = @student.courses end
As the :get_student
method
did for awards, the courses method takes an id
value given it by the routing and turns
it into an object—in this case a pair of objects, representing a given
student and the courses he or she is taking.
The next two methods are pretty different from the controller
methods the book has shown so far. Instead of passing data to a view,
they collect information from the routing and use it to manipulate the
models, and then redirect the user to a more ordinary page with the
result. The first, course_add
,
takes a student_id
and a single
course_id
and adds the student to
that course:
# POST /students/1/course_add?course_id=2 # (note no real query string, just # convenient notation for parameters) def course_add#Convert ids from routing to objects
@student = Student.find(params[:id]) @course = Course.find(params[:course])unless @student.enrolled_in?(@course)
#add course to list using << operator
@student.courses << @course
flash[:notice] = 'Course was successfully added' else flash[:error] = 'Student was already enrolled' endredirect_to :action => :courses, :id => @student
end
The course_add
method
uses the enrolled_in?
method
defined earlier in the model to check if the student is already in the
course. If not, it adds the appropriate course object to the list of
courses for that student and reports that all went well using flash[:notice]
. If the student was already
enrolled, it blocks the enrollment and reports the problem using
flash[:error]
. Then it redirects to
a list of courses for the student, which will show the flash message
as well as the list.
The remove
method, for
demonstration purposes, is a little bit different. It accepts a list
of courses to remove the student from. It then tests the list to see
if the student was actually enrolled and deletes the record connecting
the student to the course if so. It also logs the removal to the info
log of the application, and then redirects to the same page as
course_add
, listing the courses for
a student:
# POST /students/1/course_remove?courses[]= def course_remove#Convert ids from routing to object
@student = Student.find(params[:id])#get list of courses to remove from query string
course_ids = params[:courses] unless course_ids.blank? course_ids.each do |course_id| course = Course.find(course_id) if @student.enrolled_in?(course)logger.info "Removing student from course #{course.id}"
@student.courses.delete(course)
flash[:notice] = 'Course was successfully deleted' end end end redirect_to :action => :courses, :id => @student end
Making those controllers work requires telling Rails that they exist
and how they should be called. Again, Chapter 13 will explain routing in greater
depth, but you can add extra methods to an existing REST resource
through its :member
named
parameter. To add the roll
method
to the routing the scaffolding created, add a :member
parameter to the line in config/routes.rb:
map.resources :courses, :member => { :roll => :get }
For students, there are more methods, so the arguments are a bit more complicated, though generally similar:
map.resources :students, :has_many => [ :awards ], :member => { :courses => :get, :course_add => :post, :course_remove => :post}
At this point, Rails knows how to find the extra methods. All that’s left is adding support for them to the views.
Cementing the relationship between students and courses requires giving users access to the functionality provided by the controllers and models. This can happen on several levels—application-wide navigation, showing counts in related views, and view support for the newly created controllers.
The views created by the scaffolding give basic access to both the students and the courses, but there’s no user-interface connection, or even a navigation connection, between them. A first step might add links to both the student pages and the course pages, letting users move between them. As this is moving toward navigation for the application and as it will get used across a lot of different pages, it makes sense to create a navigation partial for easy reuse.
To do that, create a new file, app/views/_navigation.html.erb. Its contents are simple, creating links to the main lists of students and courses:
<p> <%= link_to "Students", students_url %> | <%= link_to "Courses", courses_url %> </p> <hr />
You could reference this partial from every view, but that’s
an inconvenient way to reference a partial that was meant to reduce
the amount of repetition needed in the first place. Instead, add it
to the layouts for each model in app/views/layouts. In each file, insert
the boldfaced code below the body
tag and above the paragraph for flash
notices:
<body>
<%= render :partial => '/navigation' %>
<p style="color: green"><%= flash[:notice] %></p>
Every page in the application will now have links to the Students and Courses main index page, as shown in Figure 9-14.
The index page for students, app/views/students/index.html.erb,
currently lists a count for awards, and you can add a count for
courses the same way. You need to insert a heading, <th>Courses</th>
, in the first
tr
element, just before <th>Awards</th>
, and then
insert:
<td><%=h student.courses.count %></td>
just before the count of awards. Figure 9-15 shows what this looks like, though the header names are abbreviated a bit to make the table fit better. Note that there aren’t any students in courses yet—the interface for adding them hasn’t yet been built.
Although the app/views/courses/index.html.erb file has
less in it, you can add <th>Enrolled</th>
in the
first tr
element and
insert:
<td><%=h course.students.count %></td>
Figure 9-16 shows the courses list, which hasn’t been shown previously, though the RESTful interface made it easy to add the courses used in Appendix B.
Again, no one is registered for any courses yet, so adding that functionality is a natural next step.
The critical piece for connecting students to courses is, of course, the form for adding courses to students. That form could be linked from the main list if you wanted, but for now we’ll update the app/views/students/show.html.erb form so that it acts as the gateway to a student’s awards and courses. There are two pieces to this. First, add a list of courses, perhaps in place of the awards list:
<p> <b>Courses:</b> <%if @student.courses.count > 0
%> <%=@student.courses.collect {|c| link_to(c.name, c)}.join(", ")
%> <% else %> Not enrolled on any courses yet. <% end %> </p>
This is much more compact than the table of awards. The
if
checks to see whether the
student is registered for any courses. If so, it builds a compact
list of courses using the collect
method. If not, it just says
so.
Second, add a link in the cluster of link_to
calls at the bottom of the
file:
<%= link_to 'Edit', edit_student_path(@student) %> |
<%= link_to 'Courses', courses_student_path(@student) %> |
<%= link_to 'Awards', student_awards_path(@student) %> |
<%= link_to 'Back', students_path %>
Bear in mind that where the navigation partial called courses_url
, this calls courses_student_path
with a specific student. That will take the user to a page such as
http://localhost:3000/students/1/courses—which
hasn’t been created yet. To create that page, create a courses.html.erb file in the app/views/students directory. Example 9-4 shows one
possible approach to creating a form for registering and
unregistering students from courses.
Example 9-4. A courses.html.erb view for registering and removing students from courses
<h1><%= @student.name %>'s courses</h1> <% if @courses.length > 0 %><% form_tag(course_remove_student_path(@student)) do %>
<table> <tr> <th>Course</th> <th>Remove?</th> </tr><% for course in @courses do %>
<tr>
<td><%=h course.name %></td>
<td><%= check_box_tag "courses[]", course.id %></td>
</tr>
<% end %>
</table> <br /><%= submit_tag "Remove checked courses" %>
<% end %> <% else %> <p>Not enrolled in any courses yet.</p> <% end %> <h2>Enroll in new course</h2><% if @student.courses.count < Course.count then %>
<% form_tag(course_add_student_path(@student)) do %>
<%= select_tag(:course,
options_from_collection_for_select(@student.unenrolled_courses,
:id, :name)) %>
<%= submit_tag 'Enroll' %>
<% end %>
<% else %>
<p><%=h @student.name %> is enrolled in every course.</p>
<% end %>
<p><%=link_to "Back", @student %></p>
This view contains two forms. Unlike most of the previous
forms, these are created with the form_tag
rather than the form_for
method because they aren’t bound
to a particular model. The first form appears if the student is
already enrolled in any courses, allowing the user to remove them
from those courses. The second form appears if there are courses
that the student hasn’t yet enrolled in. (More sophisticated program
logic might set a different kind of limit.) Each of the forms
connects to a controller method on students—course_remove
for the first one and
course_add
for the second.
The form for removing courses uses a list of checkboxes
generated from the list of courses, while the form for adding them
uses the somewhat more opaque but very powerful options_from_collection_for_select
method.
This helper method takes a collection—here, the list of courses
returned by @student.unenrolled_courses
, and two
values. The first, :id
, is the
value to return if a line in the select form is chosen, and the
second, :name
, is the value the
user should see in the form.
For information on the many more helper methods available
for creating select lists, see FormHelper
, FormTagHelper
, and FormOptionsHelper
in Appendix D.
Figure 9-17 shows the page before a student has registered for any courses, while Figure 9-18 shows the confirmation and removal options available once the student has signed up for their first course.
The checkboxes will create the parameters for course_remove
and are a good choice when
you want to operate on multiple objects at once. The select box is
much slower and produces the results needed for the single-parameter
course_add
. You will, of course,
want to choose interface components that match your users’
needs.
There’s one last component in need of finishing: the view that
corresponds to the roll
method on
the courses controller. In app/views/courses/show.html.erb, add this
link between the scaffolding’s link_to
calls for Edit and Back:
<%= link_to 'Roll', roll_course_path(@course) %> |
That will add the link shown in Figure 9-19, which will let users get to the list of students.
The actual roll call list code, shown in Example 9-5, is another simple table.
Example 9-5. Generating a roll call list through the connections from courses to students
<h1>Roll for <%=h @course.name %></h1> <% if @course.students.count > 0 %> <table> <tr> <th>Student</th> <th>GPA</th> </tr><% for student in @course.students do %>
<tr> <td><%=link_to student.name, student %>
</td> <td><%=h student.grade_point_average %>
</td> </tr> <% end %> </table> <% else %> <p>No students are enrolled.</p> <% end %> <p><%= link_to "Back", @course %></p>
The list of students is accessible from the @course
object that the roll
method in the controller exposed.
That method didn’t have anything specific to do with students, but
because the students for the course are included in the course
object, all of their information is
available for display in the table, as shown in Figure 9-20. The links
that link_to
generated let you go
directly to the student’s record, making it easy to modify students
who are in a particular course.