Let's first look at the current controller code for updating a post:
class ApplicationController < ActionController::Base
# ...
def update_post
update_query = <<-SQL
UPDATE posts
SET title = ?,
body = ?,
author = ?
WHERE posts.id = ?
SQL
connection.execute(update_query, params['title'], params['body'], params['author'], params['id'])
redirect_to '/list_posts'
end
end
Updating an existing post is essentially saving it back to the database with new values. So ideally, we'd like to be able to do the following:
# ...
def update_post
post = Post.find(params['id'])
post.set_attributes('title' => params['title'], 'body' => params['body'], 'author' => params['author'])
post.save
redirect_to '/list_posts'
end
# ...
We first retrieve the post from the database with its id
passed through the URL, then set its attribute values with user inputs, and in the end save the post back to the database with updated values.
However, if we call save
on an existing Post
, it will create a new post in the database… with the INSERT
SQL command. This is not what we want. When we update a record, it's always an existing record already in the database. When we try to update the record in the database, we should use the UPDATE
SQL command instead of INSERT
.
The way to tell if a record is a new record or not is to check its id
column - if it's nil
then it's a new record that has never been saved into the database; if it's not nil
then it's an existing record since the id
column is automatically generated by the database.
We'll adjust our model code to either INSERT
or UPDATE
records based on whether the post is a new or existing one.
### app/models/post.rb ###
class Post
attr_reader :id, :title, :body, :author, :created_at
# ...
def new_record?
id.nil?
end
def save
if new_record?
insert
else
update
end
end
def insert
insert_query = <<-SQL
INSERT INTO posts (title, body, author, created_at)
VALUES (?, ?, ?, ?)
SQL
connection.execute insert_query,
title,
body,
author,
Date.current.to_s
end
def update
update_query = <<-SQL
UPDATE posts
SET title = ?,
body = ?,
author = ?
WHERE posts.id = ?
SQL
connection.execute update_query,
title,
body,
author,
id
end
# ...
end
First, we change Post#save
to react conditionally, based on whether or not it's a new_record?
. If it is, it calls insert
, if not, update
.
We also need to implement the set_attributes
method for the Post
class, and it's pretty straightforward. We can even refactor the the initialize
method to call set_attributes
method to remove some duplication.
app/models/post.rb
class Post
attr_reader :id, :title, :body, :author, :created_at
def initialize(attributes={})
set_attributes(attributes)
end
def set_attributes(attributes)
@id = attributes['id'] if new_record?
@title = attributes['title']
@body = attributes['body']
@author = attributes['author']
@created_at ||= attributes['created_at']
end
# ...
end
Note that we have made some changes within set_attributes
. The id
is only set if this Post
is a new record. We always set the attributes for title
, body
and author
, but we only conditionally set created_at
. We do this because we only want to set created_at
once: when the Post
object is first created.
The rest of this chapter will comprise of code related to deleting and listing posts. We want to write code that allows us to delete and list records using our models. The business logic and interactions with the database should be done through our model. The workflow will be similar to code we previously worked on in this chapter.
First we'll write the code for deleting a post. Remember, we want to have database logic inside our model; this applies to deleting a post as well.
# app/models/post.rb
class Post
attr_reader :id, :title, :body, :author, :created_at
# ...
# used in application#delete_post
def destroy
connection.execute "DELETE FROM posts WHERE posts.id = ?", id
end
end
Finally, we'll use that code and our Post
model in the delete_post
action.
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# ...
def delete_post
post = Post.find(params['id'])
post.destroy
redirect_to '/list_posts'
end
# ...
end
Now that we have updated our code for deleting a post, let's update our code for listing all posts. The various methods we'll need for grabbing those records from the DB (through our model) should also be implemented here.
To accomplish this task, we'll define a class method in the Post
class that executes the SQL command to return all the posts in a hash, then we'll use map
to create an array of Post
objects from that.
# app/models/post.rb
class Post
attr_reader :id, :title, :body, :author, :created_at
# ...
def self.all
post_hashes = connection.execute("SELECT * FROM posts")
post_hashes.map do |post_hash|
Post.new(post_hash)
end
end
# ...
end
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
def list_posts
posts = Post.all
render 'application/list_posts', locals: { posts: posts }
end
end
Remember, we'll now want to use our Post model methods in this view. That includes the getters for the various states of a Post model.
<!-- app/views/application/list_posts.html.erb -->
<html>
<body>
<div class="posts">
<% posts.each do |post| %>
<div class="post">
<h2 class="title">
<a href="/show_post/<%= post.id %>">
<%= post.title %>
</a>
</h2>
<small class="meta">
<span class="author">by <%= post.author %> -</span>
<em class="created_at"><%= post.created_at %></em>
<a href="/edit_post/<%= post.id %>">Edit</a>
<form method="post" action="/delete_post/<%= post.id %>" style='display: inline'>
<input type="submit" value="Delete" />
</form>
</small>
</div>
<hr />
<% end %>
</div>
<a href="/new_post">New Post</a>
</body>
</html>
Now that we've done all that, our blog app has all the conveniences that a model gives us, at least for our posts. In the next chapter, we'll add in some validations, and we'll show how they work in the process.