For our blog app, we want to first display a list of all post titles. And now that we have a database with a few posts in it, we'll actually have something to display here!
So to get started, we'll recall that with our hello_world
action we needed three things:
We'll need each of those here as well, so let's kick things off by defining a route for /list_posts
:
config/routes.rb
Rails.application.routes.draw do
get '/list_posts' => 'application#list_posts'
end
the application#list_posts
action points to:
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
def list_posts
connection = SQLite3::Database.new 'db/development.sqlite3'
connection.results_as_hash = true
posts = connection.execute("SELECT * FROM posts")
render 'application/list_posts', locals: { posts: posts }
end
end
Inside our list_posts
action we see our DB initialization logic from before, followed by the SELECT
query to grab all of our posts as hashes. After that, we're just passing those along as locals
to the view.
A view which isn't defined yet, so let's do that next:
<!-- app/views/application/list_posts.html.erb -->
<html>
<body>
<div class="posts">
<% posts.each do |post| %>
<div class="post">
<h2 class="title">
<%= post['title'] %>
</h2>
<small class="meta">
<span class="author">by <%= post['author'] %> -</span>
<em class="created_at"><%= post['created_at'] %></em>
</small>
</div>
<hr />
<% end %>
</div>
</body>
</html>
There's quite a bit of markup here, but most of it should be familiar, with the exception of this:
<% posts.each do |post| %>
<!-- ... -->
<% end %>
Here we see how we're able to iterate over an array inside of an ERB view. Above we loop over posts
with posts.each
, building a <div>
for each post
. (This post
variable is available anywhere before the <% end %>
for the block.)
An important thing to note here is the use of <% %>
(without an =
) as opposed to <%= %>
. This allows us to execute Ruby without placing the return value into the page.
Besides that, you can see that we simply place the proper post
values into different locations in the post <div>
with lines like <%= post['title'] %>
.
And with that, if you visit /list_posts
, you'll see the titles of our three posts, along with their respective authors' names and dates of creation.
Now that we have /list_posts
working, let's implement something for /show_post/:id
, which is where users will go to read a given post.
First, we'll need a route:
### config/routes.rb ###
Rails.application.routes.draw do
get '/list_posts' => 'application#list_posts'
get '/show_post/:id' => 'application#show_post'
end
You can see here we set up a URL pattern to capture id
as a parameter that we'll use in our controller action.
Then we'll need a show_post
action:
### app/controllers/application_controller.rb ###
class ApplicationController < ActionController::Base
# ...
def show_post
connection = SQLite3::Database.new 'db/development.sqlite3'
connection.results_as_hash = true
post = connection.execute("SELECT * FROM posts WHERE posts.id = ? LIMIT 1", params['id']).first
render 'application/show_post', locals: { post: post }
end
end
In this show_post
action, we first need to get a post from the database by a particular ID. We again run some SQL against the DB with connection.execute
, but this time we call .first
on the result. The reason here is that connection.execute
is always going to return an array, even if it's only ever going to contain a single element, as is the case here with the LIMIT 1
.
The way we use this with the database is interesting though, so let's take a look:
connection.execute("SELECT * FROM posts WHERE posts.id = ? LIMIT 1", params['id'])
For our purposes, this line has the same effect as:
connection.execute("SELECT * FROM posts WHERE posts.id = #{params['id']} LIMIT 1")
The ?
character in the original statement is a placeholder that will be replaced by the value of the second parameter, which is params['id']
. This is a safer way to include user's input (in this case, from the URL) in a SQL statement. Rails makes sure that the statement is safe. The second statement is not safe because we included user's input directly in the statement. This technique is called protection against SQL Injection Attacks. We'll get to this topic later in the book.
But getting back to the last line of our action, we see that we simply pass our post
along to our show_post
view. Speaking of, let's define that now:
<!-- app/views/application/show_post.html.erb -->
<html>
<body>
<div class="post">
<h2 class="title">
<%= post['title'] %>
</h2>
<small class="meta">
<span class="author">by <%= post['author'] %> -</span>
<em class="created_at"><%= post['created_at'] %></em>
</small>
<p class="body"><%= post['body'] %></p>
</div>
<br />
</body>
</html>
This view is basically the same as our list_posts
view, except that it doesn't have a loop and does render the post body.
Hit /show_post/1
and you'll see the post details from before, plus the post body.
Finally, let's make some links between these two pages:
<!-- app/views/application/show_post.html.erb -->
<html>
<body>
<!-- link to list of posts -->
<a href="/list_posts">Back to Posts</a>
<div class="post">
<!-- ... -->
</div>
<br />
</body>
</html>
<!-- app/views/application/list_posts.html.erb -->
<html>
<body>
<div class="posts">
<% posts.each do |post| %>
<div class="post">
<h2 class="title">
<!-- link to post -->
<a href="/show_post/<%= post['id'] %>"><%= post['title'] %></a>
</h2>
<!-- ... -->
</div>
<hr />
<% end %>
</div>
</body>
</html>
The link from the show view to the list view is a simple, static link:
<a href="/list_posts">Back to Posts</a>
While the one in the loop in the list view is more involved:
<a href="/show_post/<%= post['id'] %>"><%= post['title'] %></a>
Here we dynamically build the href
using the post ID and then set the link text to the post title for each post.
You might have noticed in show_post
that we repeated the same two lines to connect to the database that we used in list_posts
. Let's move this logic to its own connection
method in the controller and adjust both actions to make use of it:
class ApplicationController < ActionController::Base
def list_posts
posts = connection.execute("SELECT * FROM posts")
render 'application/list_posts', locals: { posts: posts }
end
def show_post
post = connection.execute("SELECT * FROM posts WHERE posts.id = ? LIMIT 1", params['id']).first
render 'application/show_post', locals: { post: post }
end
private
def connection
db_connection = SQLite3::Database.new 'db/development.sqlite3'
db_connection.results_as_hash = true
db_connection
end
end
This is great because if we need to change things about our database connection later, we can just make the change in a single place. Additionally, if any other actions need to make use of a database connection (they will), then they'll be able to make use of connection
too.