Building Relationships Between Rails
Models
Contributed by Brian Leonard and Chris Kutler
June 2008 [Revision number: 6.1-1]
The Ruby on Rails framework has a powerful model
subframework known as Active Record.
The Active Record framework analyzes the database schema
and automatically provides most of the methods that you
need to work with your data. In this tutorial you learn how to use
Active Record methods to specify the one-to-many
relationship between two model classes.
This tutorial also shows how to use the
Representational State Transfer (REST) support for nested
resources.
Contents
To complete this tutorial, you need the following software.
This tutorial builds on the
Creating a Ruby Weblog in 10 Minutes
tutorial.
You must first complete that tutorial before proceeding with
this tutorial.
Adding the Comment Resource
This tutorial enhances the
rubyweblog project
by enabling
readers to add comments to a blog post.
You begin by using the generate resource task to
add the Comment model and its associated controller.
-
If the rubyweblog project is not already opened, open
the project by choosing File > Open Project
from the main menu. Browse to and select the
rubyweblog project, then click Open Project.
-
In the Projects window,
right-click the rubyweblog node, and
choose Generate from the pop-up menu.
The Rails Generator
dialog box opens.
-
Select resource from the Generate drop-down list.
-
Type
Comment post:references comment:text
in the Arguments text box, as shown next.
-
Click OK.
The Rails Generator creates the following files:
-
app/controllers/comments_controller.rb. A
controller
class that coordinates the interaction between
the Comment resource's model, actions, and views.
-
db/migrate/migrate/003_create_comments.rb.
A migration class for adding the Comments table to the
database.
This file is version 003 because the project already
has two migration files,
001_create_posts.rb and
002_add_body.rb, which create and
modify the posts table.
- app/models/comment.rb. An
Active Record
class that provides an object-relational mapping
to the Comments database table.
This file is opened in the editing area.
- test/unit/comment_test.rb. A unit test for
checking the model.
-
test/fixtures/comments.yml. A test fixture for
populating the model.
The Rails Generator also adds a map.resources :comments
call to the routes.rb file. The map.resources
method adds routes and route helpers to the application. You
work with this method later in this tutorial.
Migrating the Database
The file that you work with next is the migration file,
003_create_comments.rb.
In the Output window, click the link for the
003_create_comments.rb file.
The file opens to show the self.up method, which
uses the
create_table method to create a table
called comments, and the self.down method,
which uses the
drop_table method to tear the
comments
table down, as shown in the following code sample.
class CreateComments < ActiveRecord::Migration
def self.up
create_table :comments do |t|
t.references :post
t.text :comment
t.timestamps
end
end
def self.down
drop_table :comments
end
end
Note: The call to t.references :post
creates the post_id column. The call to
t.timestamps creates the created_at and updated_at
columns.
In the Projects window, right-click the rubyweblog node and choose
Migrate Database > To Current Version
from the pop-up menu.
This action updates the database to include
the comments table. The Output window indicates when the
migration is complete.
You can also press Shift+F6 run the class to create the table.
Defining the Relationship Between the Post
and Comment Models
You now have two models in the application: the Post model,
which contains blog posts, and the Comment model, which contains
comments belonging to a blog post.
Here you define a one-to-many relationship between the two models:
A comment is associated with a single post,
and a post can contain multiple comments.
- Expand the Models node and
double-click the post.rb node to open
the file in the editor.
-
Add to post.rb the following
has_many association
shown in bold.
Typing the trigger hm and
pressing Tab expands
into
the code template has_many :objects.
class Post < ActiveRecord::Base
validates_presence_of :title, :body
has_many :comments
end
The
has_many method indicates that the
post can have
zero, one, or more comment records associated
with it. The :comments symbol is the
name of the association. Because of naming conventions,
Active Record infers
that the :foreign_key is post_id
and that the
:class_name is "Comment".
-
Open Models > comment.rb and add the
belongs_to association,
as shown in the following code sample.
The bt trigger expands to belongs_to :object.
class Comment < ActiveRecord::Base
belongs_to :post
end
The
belongs_to method
indicates that
a comment can be associated with only one post.
The :post symbol is the name of
the association. Because of naming conventions,
Active Record infers that the
:foreign_key is :post_id,
and that the :class_name is "Post".
By default, Active Record uses the
post_id column to associate a comment with
the post that has a matching post.id.
Adding Nested Routes for the Comment Resource
When you generated the Comment resource, the IDE added
a line to the routes.rb to call
map.resources :comments.
This method call generates default routes for the
seven (7) default controller actions for viewing,
creating, updating, and deleting comments. You can see these
routes by right-clicking the rubyweblog node in the Projects
window and choosing Run Rake Task > routes. The Output window
shows the named routes. An excerpt from the output
is shown in the following listing.
comments GET /comments {:controller=>"comments", :action=>"index"}
formatted_comments GET /comments.:format {:controller=>"comments", :action=>"index"}
POST /comments {:controller=>"comments", :action=>"create"}
POST /comments.:format {:controller=>"comments", :action=>"create"}
new_comment GET /comments/new {:controller=>"comments", :action=>"new"}
formatted_new_comment GET /comments/new.:format {:controller=>"comments", :action=>"new"}
edit_comment GET /comments/:id/edit {:controller=>"comments", :action=>"edit"}
formatted_edit_comment GET /comments/:id/edit.:format {:controller=>"comments", :action=>"edit"}
comment GET /comments/:id {:controller=>"comments", :action=>"show"}
formatted_comment GET /comments/:id.:format {:controller=>"comments", :action=>"show"}
PUT /comments/:id {:controller=>"comments", :action=>"update"}
PUT /comments/:id.:format {:controller=>"comments", :action=>"update"}
DELETE /comments/:id {:controller=>"comments", :action=>"destroy"}
DELETE /comments/:id.:format {:controller=>"comments", :action=>"destroy"}
Because this application accesses comments in the context
of a post, you must modify the resource mapping to
create a set of nested subroutes that qualify the
Comment resource by a specific post.
After you
complete the steps in this section, the URLs for the comment actions
will include the post's ID, as shown in the following listing.
post_comments GET /posts/:post_id/comments {:controller=>"comments", :action=>"index"}
formatted_post_comments GET /posts/:post_id/comments.:format {:controller=>"comments", :action=>"index"}
POST /posts/:post_id/comments {:controller=>"comments", :action=>"create"}
POST /posts/:post_id/comments.:format {:controller=>"comments", :action=>"create"}
new_post_comment GET /posts/:post_id/comments/new {:controller=>"comments", :action=>"new"}
formatted_new_post_comment GET /posts/:post_id/comments/new.:format {:controller=>"comments", :action=>"new"}
edit_post_comment GET /posts/:post_id/comments/:id/edit {:controller=>"comments", :action=>"edit"}
formatted_edit_post_comment GET /posts/:post_id/comments/:id/edit.:format {:controller=>"comments", :action=>"edit"}
post_comment GET /posts/:post_id/comments/:id {:controller=>"comments", :action=>"show"}
formatted_post_comment GET /posts/:post_id/comments/:id.:format {:controller=>"comments", :action=>"show"}
PUT /posts/:post_id/comments/:id {:controller=>"comments", :action=>"update"}
PUT /posts/:post_id/comments/:id.:format {:controller=>"comments", :action=>"update"}
DELETE /posts/:post_id/comments/:id {:controller=>"comments", :action=>"destroy"}
DELETE /posts/:post_id/comments/:id.:format {:controller=>"comments", :action=>"destroy"}
The columns in the routes task output show the following information.
-
The first column shows the route's name. You prepend
this name to either _url or _path to call
a helper method to generate the URL for that route.
You use the name_url helper method to
generate the entire URL, including protocol and domain.
You use the name_path helper
method to generate just the path part.
-
The second column indicates which verb
(HTTP method) to use in the request.
When a form is submitted, the default verb is POST. In
all other cases, the default verb is GET. The DELETE
verb must be specified explicitly, for example link_to 'Destroy',
post_comment_path(@post), :method=> :delete.
- The third column shows the URL for the named route. You can also
use this column to determine which arguments to pass to the
_url and _path helper methods. For example,
if you look at the post_comment route, you see that
the URL requires both the post's ID and the comment's ID,
so you would call post_comment_url(@post.id, @comment.id)
to generate the URL.
- The last column shows the controller and action that the
application invokes when the request that matches the URL pattern
is received. Notice how post_comments_path combined with
the GET verb invokes the index action, whereas
the post_comments_path combined with
the POST verb invokes the create action.
Later in this tutorial, you use the post_comments_path
method in a form submission to invoke the create action
in the comments controller.
Note: To learn more about routing
and REST support,
see the following resources.
-
In the Projects window, expand Configuration and double-click
the routes.rb node to open the file in the editor.
-
Delete the following line.
map.resources :comments
-
Add the following code shown in bold to the call to
map.resources :posts
to generate
routes for manipulating comments in the context of
a post.
map.resources :posts, :has_many=>:comments
-
Remove or comment out the following lines.
map.connect ':controller/:action/:id'
map.connect ':controller/:action/:id.:format'
These are the default routes. If you leave
these routes in, one could access the create
action in the comments controller using a URL
such as http://localhost:3000/comments/create/44,
which does not provide the Post's ID.
-
Save your changes.
-
(Optional) Right-click the rubyweblog project
node and choose Run Rake Task > routes to see
all the named routes for the application.
Modifying the Controller
Next you work with the comments controller,
which maps requests to actions.
Because a comment is always accessed in the context of
a post, you add a filter to obtain the post for
the ID that is passed in the URL.
-
Expand the Controllers node and open
comments_controller.rb.
-
Replace the contents of the file with the following code.
class CommentsController < ApplicationController
before_filter(:get_post)
def create
# Create a comment object that has
# been instantiated with attributes
# and linked to the post object
@comment = @post.comments.build(params[:comment])
if @comment.save
flash[:notice] = 'Comment was successfully created.'
respond_to do |format|
format.html { redirect_to post_url(@post) }
format.xml { render :xml => @comment,
:status => :created, :location => @post }
end
else
flash[:notice] = 'Comment was not created.'
respond_to do |format|
format.html { redirect_to post_url(@post) }
format.xml { render :xml => @comment.errors,
:status => :unprocessable_entity }
end
end
end
private
def get_post
@post = Post.find(params[:post_id])
end
end
The
before_filter method enables you to
intercept calls to the action methods
and perform preprocessing before the action
methods are invoked. Here, the before_filter
is used to get the Post object for
the post's ID that was passed in the URL.
The create action is called when
the user
clicks the Create button to submit a comment.
The code then
creates a new Comment
object that is associated with that post_id,
consisting of the time created and the actual comment.
The Rails framework passes the submitted parameters
from the form as a hash (params[:comment]).
Comment is an Active Record class, so calling its
save method saves the
comment record to the database.
A status message is then put in the flash.
The code the calls posts_url method
using the default GET verb, which maps
to the show action in the posts controller.
The show action loads the
show.html.erb
page. This page reloads the post and displays the
status message
from the flash. Later, after you modify the
page, it will show all of the post's comments,
including the new one.
Modifying the View to Add Comments
Here you edit the show.html.erb file,
which displays
an individual blog entry, to enable users to add
comments to a post.
-
Expand Views > posts and double-click
show.html.erb to open
it in the editor.
-
Add the following code at the end of
the show.html.erb file.
<hr>
<h4>Comments</h4>
<% form_for(:comment, :url => post_comments_path(@post)) do |f| %>
<p>Comment:<br/>
<%= f.text_area :comment %></p>
<%= f.submit "Create" %>
<%end %>
This code produces a form that includes a text input
area for
writing the comment, and a Submit button labeled Create,
as shown in the next figure. When used in
a form, the post_comments_path
maps to the
create action in the
comments controller.
When you pass an Active Record
model object to a _url or _path helper
method, the method calls the object's
to_param
method to get the object's URL parameter, which is the
value from the id column by default.
In this case,
post_comments_path(@post) is equivalent
to post_comments_path(@post.id). Passing
the model object instead of the id enables you later
to override the to_param method without breaking this
code.
-
Save your files and return to the browser.
-
Click permalink to view the details for
one of the blog entries.
Try adding a comment in the text area, but note that
the blog does not yet display comments when you click
the Create button.
If the comment is added successfully, you see a message at the
top of the view, as shown in the following figure.
In the next steps you add code
to collect and display the comments.
Displaying the Comments
The blog does not yet display the comments that a
reader adds, so here you fix that problem. First,
you add a style for displaying date information, then
you add code to the show.html.erb file
to display each of the post's comments.
To keep this tutorial short, you add the
style definition to the existing scaffold.css
style sheet. Typically, you would create and use
your own style sheet, as explained in
How to Correctly Use Stylesheets in Your Templates
.
-
In the Projects window, expand the Public node,
expand stylesheets, and double-click
scaffold.css to
open it in the editor.
-
Add the following style definition to the bottom
of the file.
div.dateline {
color: #999;
font-size: 8pt;
}
-
Close the file and save the changes.
-
Return to the show.html.erb file
and paste the contents of the following
<ul> element below the
<h4>Comments</h4> line.
<ul>
<% @post.comments.each do |comment| %>
<li>
<%= h comment.comment %><br>
<div class = "dateline">
Posted on <%= comment.created_at.strftime("%B %d, %Y at %I:%M %p") %>
</div>
</li>
<% end %>
</ul>
This displays
the comments in a bulleted
list, and includes the date and time each
comment was created. The date and time
are shown using the .dateline
style that you added to the style sheet.
Because you added the has_many :comments
relationship
to the Post model, you can access all the comments
for a post
by calling @post.comments.
-
Choose File > Save All, then refresh
your browser.
The comments now appear in the blog in a bulleted
list, as shown in the following figure.
Applying What You Have Learned
Using the skills that you have learned in this tutorial, modify
the tasklist web project that is described in
the the Applying What You
Have Learned section of the Creating a Ruby Weblog in 10 Minutes
tutorial.
Use the resource generator to add a Note resource that contains a
note:text field and references a task. Build a one-to-many relationship
between task and notes. That is, a task has zero, one, or more notes
and a note is associated with exactly one task. Modify the
notes controller and the task's show.html.erb file to
add and display the notes for a task.
Next Steps
>> More NetBeans Ruby Documentation