Data Encapsulation With Models

Creating a New Post

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.

app/models/post.rb

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.

Find and Show a Post

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.

Edit a Post

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.