It's time to add a second model to the application. The second model will handle comments on articles.
Generating a Model
We're going to see the same generator that we used before when creating the Article model. This time we'll create a Comment model to hold reference to an article. Run this command in your terminal:
$ bin/rails generate model Comment commenter:string body:text article:references
This command will generate four files:
First, take a look at app/models/comment.rb:
class Comment < ApplicationRecordbelongs_to :articleend
This is very similar to the Article model that you saw earlier. The difference is the line belongs_to :article, which sets up an Active Record association. You'll learn a little about associations in the next section of this course.
The (:references) keyword used in the bash command is a special data type for models. It creates a new column on your database table with the provided model name appended with an _id that can hold integer values. To get a better understanding, analyze the db/schema.rb file after running the migration.
In addition to the model, Rails has also made a migration to create the corresponding database table:
class CreateComments < ActiveRecord::Migration[5.2]def changecreate_table :comments do |t|t.string :commentert.text :bodyt.references :article, foreign_key: truet.timestampsendendend
The t.references line creates an integer column called article_id, an index for it, and a foreign key constraint that points to the id column of the articles table. Go ahead and run the migration:
$ bin/rails db:migrate
Rails is smart enough to only execute the migrations that have not already been run against the current database, so in this case you will just see:
== 20180829015332 CreateComments: migrating ===================================-- create_table(:comments)-> 0.0529s== 20180829015332 CreateComments: migrated (0.0530s) ==========================
Associating Models
Active Record associations let you easily declare the relationship between two models. In the case of comments and articles, you could write out the relationships this way:
- Each comment belongs to one article.
- One article can have many comments.
In fact, this is very close to the syntax that Rails uses to declare this association. You've already seen the line of code inside the Comment model (app/models/comment.rb) that makes each comment belong to an Article:
class Comment < ApplicationRecordbelongs_to :articleend
You'll need to edit app/models/article.rb to add the other side of the association:
class Article < ApplicationRecordhas_many :commentsvalidates :title, presence: true,length: { minimum: 5 }end
These two declarations enable a good bit of automatic behavior. For example, if you have an instance variable @article containing an article, you can retrieve all the comments belonging to that article as an array using @article.comments.
For more information on Active Record associations, see the Active Record Associations guide.
Adding a Route for Comments
As with the welcome controller, we will need to add a route so that Rails knows where we would like to navigate to see comments. Open up the config/routes.rb file again, and edit it as follows:
resources :articles doresources :commentsend
This creates comments as a nested resource within articles. This is another part of capturing the hierarchical relationship that exists between articles and comments.
For more information on routing, see the Rails Routing guide.
Generating a Controller
With the model in hand, you can turn your attention to creating a matching controller. Again, we'll use the same generator we used before:
$ bin/rails generate controller Comments
This creates five files and one empty directory:
Like with any blog, our readers will create their comments directly after reading the article, and once they have added their comment, will be sent back to the article show page to see their comment now listed. Due to this, our CommentsController is there to provide a method to create comments and delete spam comments when they arrive.
So first, we'll wire up the Article show template (app/views/articles/show.html.erb) to let us make a new comment:
<p><strong>Title:</strong><%= @article.title %></p><p><strong>Text:</strong><%= @article.text %></p><h2>Add a comment:</h2><%= form_with(model: [ @article, @article.comments.build ], local: true) do |form| %><p><%= form.label :commenter %><br><%= form.text_field :commenter %></p><p><%= form.label :body %><br><%= form.text_area :body %></p><p><%= form.submit %></p><% end %><%= link_to 'Edit', edit_article_path(@article) %> |<%= link_to 'Back', articles_path %>
This adds a form on the Article show page that creates a new comment by calling the CommentsController create action. The form_with call here uses an array, which will build a nested route, such as /articles/1/comments.
Let's wire up the create in app/controllers/comments_controller.rb:
class CommentsController < ApplicationControllerdef create@article = Article.find(params[:article_id])@comment = @article.comments.create(comment_params)redirect_to article_path(@article)endprivatedef comment_paramsparams.require(:comment).permit(:commenter, :body)endend
You'll see a bit more complexity here than you did in the controller for articles. That's a side-effect of the nesting that you've set up. Each request for a comment has to keep track of the article to which the comment is attached, thus the initial call to the find method of the Article model to get the article in question.
In addition, the code takes advantage of some of the methods available for an association. We use the create method on @article.comments to create and save the comment. This will automatically link the comment so that it belongs to that particular article.
Once we have made the new comment, we send the user back to the original article using the article_path(@article) helper. As we have already seen, this calls the show action of the ArticlesController which in turn renders the show.html.erb template. This is where we want the comment to show, so let's add that to the app/views/articles/show.html.erb.
<p><strong>Title:</strong><%= @article.title %></p><p><strong>Text:</strong><%= @article.text %></p><h2>Comments</h2><% @article.comments.each do |comment| %><p><strong>Commenter:</strong><%= comment.commenter %></p><p><strong>Comment:</strong><%= comment.body %></p><% end %><h2>Add a comment:</h2><%= form_with(model: [ @article, @article.comments.build ], local: true) do |form| %><p><%= form.label :commenter %><br><%= form.text_field :commenter %></p><p><%= form.label :body %><br><%= form.text_area :body %></p><p><%= form.submit %></p><% end %><%= link_to 'Edit', edit_article_path(@article) %> |<%= link_to 'Back', articles_path %>
Now you can add articles and comments to your blog and have them show up in the right places.