corner imagecorner image
FeaturesPluginsDocs & SupportCommunityPartners

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

Content on this page applies to NetBeans IDE 6.1

To complete this tutorial, you need the following software.

Software or Resource Version Required
NetBeans IDE Version 6.1
MySQL database server Version 5.0
Rails framework 2.0

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.

  1. 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.
  2. In the Projects window, right-click the rubyweblog node, and choose Generate from the pop-up menu.

    Generate Pop-Up Menu Item

    The Rails Generator dialog box opens.

  3. Select resource from the Generate drop-down list.
  4. Type Comment post:references comment:text in the Arguments text box, as shown next.

    Entering resource arguments in the Generate dialog box
  5. 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.

  1. In the Output window, click the link for the 003_create_comments.rb file.

    Clicking the link for the 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.

  2. 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.

  1. Expand the Models node and double-click the post.rb node to open the file in the editor.
  2. 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".

  3. 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.

  1. In the Projects window, expand Configuration and double-click the routes.rb node to open the file in the editor.
  2. Delete the following line.

      map.resources :comments
        
  3. 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
        
  4. 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.

  5. Save your changes.

  6. (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.

  1. Expand the Controllers node and open comments_controller.rb.

  2. 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.

  1. Expand Views > posts and double-click show.html.erb to open it in the editor.

  2. 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.

  3. Save your files and return to the browser.

  4. 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.

    Success Message in Browser

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 .

  1. In the Projects window, expand the Public node, expand stylesheets, and double-click scaffold.css to open it in the editor.
  2. Add the following style definition to the bottom of the file.

    div.dateline {
      color: #999;
      font-size: 8pt;
    }
            
  3. Close the file and save the changes.
  4. 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.

  5. 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.

    View of Comment Model, With Comments

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


Companion
Projects:
MySQL Database Server   GlassFish Community: an Open Source Application Server   Open Solaris  Open JDK: an Open SourceJDK   Mobile & Embedded Community     Sponsored by 
Sponsored by Sun Microsystems