Rails 5 Basics : Relationships

Objective

  • To learn relationships between models.

Steps

Step 1

Let's create a comment model by using the Rails generator command:

$rails g model comment commenter:string description:text article:references

Step 2

Open the db/migrate/xyz_create_comments.rb file in your IDE. You will see the create_table() method within change() method that takes comments symbol :comments as the argument and the description of the columns for the comments table.

What does references do? It creates the foreign key article_id in the comments table. We also create an index for this foreign key in order to make the SQL joins faster.

Step 3

Run :

$rake db:migrate
== 20151206232151 CreateComments: migrating ============
-- create_table(:comments)
   -> 0.0067s
== 20151206232151 CreateComments: migrated (0.0067s) ====

Let's install SQLiteManager Firefox plugin that we can use to open the SQLite database, query, view table structure etc.

Step 4

Install SqliteManager Firefox plugin SqliteManager Firefox plugin.

Step 5

Let's now see the structure of the comments table. In Firefox go to : Tools --> SQLiteManager

Step 6

Click on 'Database' in the navigation and select 'Connect Database', browse to blog/db folder.

Step 7

Change the file extensions to all files.

Step 8

Open the development.sqlite3 file. Select the comments table. You can see the foreign key article_id in the comments table.

You can also use the rails db command to view the tables, schema and work with database from the command line.

$ rails db
SQLite version 3.7.7 2011-06-25 16:35:41
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> .help
sqlite> .databases
seq  name             file                                                      
---  ---------------  ----------------------------------------------------------
0    main             /Users/zepho/temp/rails5/db/development.sqlite3           
sqlite> .tables
articles           comments           schema_migrations
sqlite> .schema comments
CREATE TABLE "comments" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "commenter" varchar, "description" text, "article_id" integer, "created_at" datetime NOT NULL, "updated_at" datetime NOT NULL);
CREATE INDEX "index_comments_on_article_id" ON "comments" ("article_id");

Step 9

Open the app/models/comment.rb file. You will see the line:

belongs_to :article

declaration. This means you have a foreign key article_id in the comments table. The belongs_to declaration in the model will not create or manipulate database tables. The belongs_to or references in the migration will manipulate the database tables. Since your models are not aware of the database relationships, you need to declare them.

Step 10

Open the app/models/article.rb file. Add the following declaration:

has_many :comments

This means each article can have many comments. Each comment points to it's corresponding article.

Step 11

Open the config/routes.rb and define the route for comments:

resources :articles do
  resources :comments
end

Since we have parent-children relationship between articles and comments we have nested routes for comments.

Step 12

Let's create the controller for comments.

$rails g controller comments

Readers can comment on any article. When someone comments we will display the comments for that article on the article's show page.

Step 13

Let's modify the app/views/articles/show.html.erb to let us make a new comment:

<h2>Add a comment:</h2>
<%= form_for([@article, @article.comments.build]) do |f| %>
  <p>
    <%= f.label :commenter %><br />
    <%= f.text_field :commenter %>
  </p>
  <p>
    <%= f.label :description %><br />
    <%= f.text_area :description %>
  </p>
  <p>
    <%= f.submit %>
  </p>
<% end %>

The app/views/show.html.erb file will now look like this:

<p>
  <%= @article.title %><br>
</p>

<p>
  <%= @article.description %><br>
</p>

<h2>Add a comment:</h2>
<%= form_for([@article, @article.comments.build]) do |f| %>
  <p>
    <%= f.label :commenter %><br />
    <%= f.text_field :commenter %>
  </p>
  <p>
    <%= f.label :description %><br />
    <%= f.text_area :description %>
  </p>
  <p>
    <%= f.submit %>
  </p>
<% end %>

Step 14

Go to http://localhost:3000/articles page and click on 'Show' for one of the article. You will now see the form for filling out the comment for this specific article.

Step 15

View the page source for the article show page by clicking any of the 'Show' link in the articles index page. You can see the URI pattern and the http method used when someone submits a comment by clicking the 'Create Comment' button.

<form class="new_comment" id="new_comment" action="/articles/5/comments" accept-charset="UTF-8" method="post">

Exercise 1

Take a look at the output of rake routes and find out the resource endpoint for the URI pattern and http method combination found in step 15.

Step 16

Run rake routes in the blog directory. You can see how the rails router takes the comment submit form to the comments controller, create action.

POST   /articles/:article_id/comments(.:format)          comments#create

Step 17

Fill out the comment form and click on 'Create Comment'. You will get a unknown action create for Comments controller error page.

Step 18

Define the create method in comments_controller.rb as follows:

def create

end

Step 19

Fill out the comment form and submit it again. You can see the comment values in the server log.

Started POST "/articles/5/comments" for ::1 at 2015-12-06 15:35:10 -0800
Processing by CommentsController#create as HTML
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"zHGSEB2JUXjwbXNrE5", "comment"=>{"commenter"=>"bugs", "description"=>"This is cool"}, "commit"=>"Create Comment", "article_id"=>"5"}
No template found for CommentsController#create, rendering head :no_content
Completed 204 No Content in 3ms (ActiveRecord: 0.0ms)

Step 20

Copy the entire parameters hash you see from the server log. Go to Rails console and paste it like this:

params =  {"comment"=>{"commenter"=>"test", "description"=>"tester"},  "commit"=>"Create Comment", "article_id"=>"5"}

Here you initialize the params variable with the hash you copied in the rails server log. You can find the value for comment model by doing: params['comment'] in the Rails console.

{"commenter"=>"test", "description"=>"tester"} 

You can extract the article_id from the parameters like this:

 > params['article_id']

Step 21

Let's create a comment for a given article by changing the create action as follows:

def create
  @article = Article.find(params[:article_id])
  permitted_columns = params[:comment].permit(:commenter, :description)
  @comment = @article.comments.create(permitted_columns)

  redirect_to article_path(@article)
end

The only new thing in the above code is this:

@article.comments.create

Since we have the declaration

has_many :comments

in the article model. We can navigate from an instance of article to a collection of comments:

@article.comments

We call the method create on the comments collection like this:

@article.comments.create

This will automatically populate the foreign key article_id in the comments table for us. The params[:comment] will retrieve the comment column values.

Step 22

Fill out the comment form and submit it.

Started POST "/articles/5/comments" for ::1 at 2015-12-06 15:40:53 -0800
Processing by CommentsController#create as HTML
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"OIx2Hx7", "comment"=>{"commenter"=>"bugs", "description"=>"cartoon"}, "commit"=>"Create Comment", "article_id"=>"5"}
  Article Load (0.2ms)  SELECT  "articles".* FROM "articles" WHERE "articles"."id" = ? LIMIT 1  [["id", 5]]
   (0.1ms)  begin transaction
  SQL (0.4ms)  INSERT INTO "comments" ("commenter", "description", "article_id", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?)  [["commenter", "bugs"], ["description", "cartoon"], ["article_id", 5], ["created_at", 2015-12-06 23:40:53 UTC], ["updated_at", 2015-12-06 23:40:53 UTC]]
   (82.1ms)  commit transaction
Redirected to http://localhost:3000/articles/5
Completed 302 Found in 89ms (ActiveRecord: 82.8ms)

You can now view the record in the MySQLite Manager or Rails db console. Let's now display the comments made for a article in the articles show page.

Step 23

Add the following code to the app/views/articles/show.html.erb:

<h2>Comments</h2>
<% @article.comments.each do |comment| %>
  <p>
    <strong>Commenter:</strong>
    <%= comment.commenter %>
  </p>

  <p>
    <strong>Comment:</strong>
    <%= comment.description %>
  </p>
<% end %>

Your app/views/articles/show.html.erb will now look like this:

<p>
  <%= @article.title %><br>
</p>

<p>
  <%= @article.description %><br>
</p>

<h2>Comments</h2>
<% @article.comments.each do |comment| %>
  <p>
    <strong>Commenter:</strong>
    <%= comment.commenter %>
  </p>

  <p>
    <strong>Comment:</strong>
    <%= comment.description %>
  </p>
<% end %>

<h2>Add a comment:</h2>
<%= form_for([@article, @article.comments.build]) do |f| %>
  <p>
    <%= f.label :commenter %><br />
    <%= f.text_field :commenter %>
  </p>
  <p>
    <%= f.label :description %><br />
    <%= f.text_area :description %>
  </p>
  <p>
    <%= f.submit %>
  </p>
<% end %>

Step 24

Reload the article show page or click on the 'Show' link for the article with comments by going to the articles index page. You will now see the existing comments for an article.

Summary

We saw how to create parent-child relationship in the database and how to use ActiveRecord declarations in models to handle one to many relationship. We learned about nested routes and how to make forms work in the parent-child relationship. In the next lesson we will implement the feature to delete comments to keep our blog clean from spam.


Related Articles


Create your own user feedback survey