Our application works now with data access patterns that directly embed SQL statements in the application code (the controller actions). This works, and in fact, this is how many applications were built about 20 years ago before the emergence of modern web development frameworks. The problem is as the application grows bigger, the SQL statements get scattered all around the application, then for simple data related changes (such as changing the table name of the posts
to blog_posts
in the database) you'd have to scan the entire application code base to make changes to all SQL statements that touch that data.
The answer to this problem is encapsulation, to move all SQL commands about a piece of data into a single place, so they can be easily updated when necessary.
Let's do this now with a Post
model.
app/models/post.rb
class Post
attr_reader :id, :title, :body, :author, :created_at
def initialize(attributes={})
@id = attributes['id']
@title = attributes['title']
@body = attributes['body']
@author = attributes['author']
@created_at = attributes['created_at']
end
end
Be sure to notice that a model is just a Ruby class, and in Rails these model classes live in the app/models
directory. Thus, we create our Post
class in app/models/post.rb
.
This is a very simple class - in fact it is nothing more than a container for a bunch of attributes (such as title
, body
etc) at this point. With the initialize
method, we allow data to be passed in as a hash with Post.new
when a Post
object is instantiated and then assign the values to the instance variables.
You'll notice that we have a default value of an empty hash for our attributes
parameter:
def initialize(attributes={})
This is so that we can instantiate an empty Post
, without passing any arguments to it. So now:
post = Post.new
is equivalent to
post = Post.new({})
Then later in the class, we define reader methods to get to each of these attributes by using attr_reader
.
And remember:
attr_reader :title
is just a shorthand for defining a method like this
def title
@title
end
Now we're ready to start moving our database calls out of ApplicationController
and into our Post
model.
We're going to first move the SQL commands from create_post
in our controller to the Post
class. We are going to wrap the database access steps in a save
method.
class Post
attr_reader :id, :title, :body, :author, :created_at
# ...
def save
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 connection
db_connection = SQLite3::Database.new('db/development.sqlite3')
db_connection.results_as_hash = true
db_connection
end
end
Then our controller action for create_post
becomes:
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# ...
def create_post
post = Post.new('title' => params['title'],
'body' => params['body'],
'author' => params['author'])
post.save
redirect_to '/list_posts'
end
end
The logic in the controller action is much simpler now - we are just going to instantiate a post object from the params, which has all of the user's inputs, and tell the post object to save itself into the database.
Next, let's move our logic for looking up a post to a Post.find
method. This is not an operation that we do on an instance of a Post (we don't have one yet, we are finding one!) so we'll put it as a class method.
app/models/post.rb
class Post
attr_reader :id, :title, :body, :author, :created_at
# ...
def self.find(id)
post_hash = connection.execute("SELECT * FROM posts WHERE posts.id = ? LIMIT 1", id).first
Post.new(post_hash)
end
# ...
end
Also because the connection
method needs to be accessed from a class method find
, we'll move it to be a class method as well.
class Post
attr_reader :id, :title, :body, :author, :created_at
# ...
def self.connection
db_connection = SQLite3::Database.new 'db/development.sqlite3'
db_connection.results_as_hash = true
db_connection
end
def connection
self.class.connection
end
# ...
We can now change the show
controller action accordingly:
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# ...
def show_post
post = Post.find(params['id'])
render 'application/show_post', locals: { post: post }
end
end
Here again, we just move our database logic out of our controller and into a method in our model, making sure that this new Post.find
method returns a Post
object.
And after using Post.find
in the show_post
action, we're also able to get rid of our find_post_by_id
method from the controller entirely, since we're now exclusively using our new Post.find
method for this purpose.
We have refactored the code to the model and changed the controller actions, but if we visit /show_post/1
now, we'll find ourselves staring at an error page. The reason for this is that we changed the type of objects that are passed into the view as posts
. Before they were Hash
objects, and now they're Post
objects.
Let's change our show_post
view to use the Post object instead. Since we have defined attr_reader
for the Post class, accessing its attributes is now easier:
<!-- 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 />
<a href="/list_posts">Back to Posts</a>
</body>
</html>
And with that, the show_post
action and view are successfully using our new Post
model.
Next, let's do the same thing for our edit_post
action and view. It will be a very similar process.
After we're done altering the edit_post
action and view, we should be able to visit /edit_post/1
and see a form filled with the current values of that post.
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# ...
def edit_post
post = Post.find(params['id'])
render 'application/edit_post', locals: { post: post }
end
# ...
end
<!-- app/views/application/edit_post.html.erb -->
<html>
<body>
<form method="post" action="/update_post/<%= post.id %>">
<label for="title"> Title</label>
<input id="title" name="title" type="text" value="<%= post.title %>"/>
<br /> <br />
<label for="body"> Body</label>
<textarea id="body" name="body"><%= post.body %></textarea>
<br /> <br />
<label for="author"> Author</label>
<input id="author" name="author" type="text" value="<%= post.author %>"/>
<br /> <br />
<input type="submit" value="Update Post" />
</form>
<br />
<a href="/list_posts">Back to Posts</a>
</body>
</html>
Make sure to try out the code above yourself.
In the next chapter, we'll deal with altering our action for update_post
so that it utilizes the new Post
model we've been working with.