Now that we have all the CRUD actions working for posts and we have a Post
model, let's add on to our web app's functionality by adding post comments.
Each post will be able to have many comments, so let's start off by giving ourselves some comments in our DB to play with. This process will be the same as it was with posts, we'll:
Our comments will also have id
, body
, author
, and created_at
columns, just like our posts. But there's another thing we'll have to keep track of for each comment: what post does the comment belong to?
In relational databases, we keep track of this using a foreign key.
For both tables, our id
column holds the primary key for each row, that is, a value that uniquely identifies that row within that table. A foreign key is called "foreign" because it references a row in a different ("foreign") table.
Since our comments belong to a post, we'll follow Rails' convention and name this foreign key column post_id
. The integer held in this column will be the ID of the post row that a given comment row belongs to.
But our process for creating the table and populating it with data remains the same as it was with posts. First we have some SQL to create the table:
-- db/comments.sql --
CREATE TABLE "comments" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
"body" text,
"author" varchar,
"post_id" integer,
"created_at" datetime NOT NULL);
And some CSV we'll import to create some comments, three for each post:
db/comments.csv
1,Lorem ipsum dolor sit amet.,Matz,1,2014-12-09
2,Lorem ipsum dolor sit amet.,DHH,1,2014-12-09
3,Lorem ipsum dolor sit amet.,tenderlove,1,2014-12-09
4,Lorem ipsum dolor sit amet.,Matz,2,2014-12-09
5,Lorem ipsum dolor sit amet.,DHH,2,2014-12-09
6,Lorem ipsum dolor sit amet.,tenderlove,2,2014-12-09
7,Lorem ipsum dolor sit amet.,Matz,3,2014-12-09
8,Lorem ipsum dolor sit amet.,DHH,3,2014-12-09
9,Lorem ipsum dolor sit amet.,tenderlove,3,2014-12-09
Notice the post_id
values for each comment. These are after the author name and before the date.
First we run the SQL against our db/development.sqlite3
database:
$ sqlite3 db/development.sqlite3 < db/comments.sql
Then we can use the sqlite3
console to see that we have a comments
table now, but no rows within:
$ sqlite3 db/development.sqlite3
sqlite> .tables
comments posts
sqlite> SELECT * FROM comments;
sqlite>
And lastly, we'll import our comments CSV:
sqlite> .mode csv
sqlite> .import db/comments.csv comments
sqlite> SELECT * FROM comments;
8,"Lorem ipsum dolor sit amet.",DHH,3,2014-12-09
7,"Lorem ipsum dolor sit amet.",Matz,3,2014-12-09
6,"Lorem ipsum dolor sit amet.",tenderlove,2,2014-12-09
5,"Lorem ipsum dolor sit amet.",DHH,2,2014-12-09
4,"Lorem ipsum dolor sit amet.",Matz,2,2014-12-09
3,"Lorem ipsum dolor sit amet.",tenderlove,1,2014-12-09
2,"Lorem ipsum dolor sit amet.",DHH,1,2014-12-09
1,"Lorem ipsum dolor sit amet.",Matz,1,2014-12-09
And now we have comments in our database!
Now that we have rows of comments in a comments
table in our database, let's show the comments that belong to a post in our show_post
view.
We'll do this in two steps:
Let's start with the show_post
action changes:
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# ...
def show_post
post = Post.find(params['id'])
comments = connection.execute('SELECT * FROM comments WHERE comments.post_id = ?', params['id'])
render 'application/show_post', locals: { post: post, comments: comments }
end
# ...
end
Here we simply find our comments by specifying in our query:
WHERE comments.post_id = ?
Where params['id']
(the post's ID) is substituted for the ?
. This where clause is how we only pull comments that are "associated" to a post.
Then we pass the resulting hash along into our view using locals
, so let's now make use of comments
in our show_post
view:
app/views/application/show_post.html.erb
<html>
<body>
<div class="post">
<!-- post title, meta data, and body displayed here -->
<br /><br />
<div class="comments">
<h3>Comments:</h3>
<hr />
<% comments.each_with_index do |comment, index| %>
<div class="comment">
<small class="comment_meta">
<span class="comment_author">#<%= index+1 %> by <%= comment['author'] %> -</span>
<em class="comment_created_at"><%= comment['created_at'] %></em>
</small>
<p class="comment_body"><%= comment['body'] %></p>
</div>
<hr />
<% end %>
</div>
</div>
<br />
<a href="/list_posts">Back to Posts</a>
</body>
</html>
Here we loop again, using <% %>
instead of <%= %>
, just like we did with our post titles in list_posts
, but this time we use each_with_index
so that we can number our comments.
Just as a quick Ruby-only illustration of how this works, here's what each_with_index
can do:
['a','b','c'].each_with_index do |char, idx|
puts "##{idx+1}: #{char}"
end
# prints:
#
# #1: a
# #2: b
# #3: c
So now if we visit /show_post/1
, we'll see the comments for the post as well.
NOTE: As a reminder, if you ended up deleting any of the three original posts in a previous assignment, you will not be able to see the comments associated with the deleted post(s).
Just like the way we have a Post
model for the posts, let's create a Comment
model for the comments:
app/models/comment.rb
class Comment
attr_reader :id, :body, :author, :post_id, :created_at
def initialize(attributes={})
@id = attributes['id'] if new_record?
@body = attributes['body']
@author = attributes['author']
@post_id = attributes['post_id']
@created_at ||= attributes['created_at']
end
def new_record?
@id.nil?
end
end
Now we have the beginning of a Comment
model. The file app/models/comment.rb
contains an initialize
and new_record?
method similar to those found in Post
.