Working With Model Associations

Association Between Models

Currently in the ApplicationController, we first find a post from the database, then we need to get a collection of comments that are associated with the post. We're getting that from directly executing SQL in the controller action, which is not the ideal solution.

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

Since the SQL command needs a post id to find the comments, we'll move the command to the Post model.

app/models/post.rb

class Post

  # ...

  def comments
    comment_hashes = connection.execute 'SELECT * FROM comments WHERE comments.post_id = ?', id
    comment_hashes.map do |comment_hash|
      Comment.new(comment_hash)
    end
  end

  # ...

end

Now we can change the code in the controller:

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

And make adjustments on the view:

app/views/application/show_post.html.erb

<html>
  <body>

    <div class="post">

      <!-- display post -->
      <br /> <br />
      <div class="comments">
        <h3>Comments:</h3>
        <% post.comments.each_with_index do |comment, index| %>
          <p class="comment">
            <small class="comment_meta">
              <span class="comment_author">#<%= index %> by <%= comment.author %> -</span>
              <em class="comment_created_at"><%= comment.created_at %></em>
            </small>

            <p class="comment_body"><%= comment.body %></p>
          </p>
          <hr />
        <% end %>
      </div>
    </div>

    <!-- ... -->

  </body>
</html>

Creation Through Association

Moving on, we'll provide users with a way of adding a comment to a post.

And because it's a CRUD action, creating a comment will very much resemble creating a post. Below you'll find a <form> to create a new comment placed in show_post, the route it submits to, and the create_comment action that route points to:

<!-- app/views/application/show_post.html.erb -->

<html>
  <body>

    <div class="post">

      <div class="comments">
        <hr />
        <% comments.each_with_index do |comment| %>
          <!-- display each comment -->
        <% end %>

        <!-- new comment form -->
        <form method="post" action="/create_comment_for_post/<%= post.id %>">

          <label for="body">Comment</label>
          <textarea id="body" name="body"></textarea>
          <br /> <br />

          <label for="author">Name</label>
          <input id="author" name="author" type="text" />
          <br /> <br />

          <input type="submit" value="Add Comment" />
        </form>
        <hr />

      </div>
    </div>

    <br />
    <a href="/list_posts">Back to Posts</a>

  </body>
</html>
### config/routes.rb ###

Rails.application.routes.draw do
  # ...
  post '/create_comment_for_post/:post_id' => 'application#create_comment'
end
### app/controllers/application_controller.rb ###

class ApplicationController < ActionController::Base

  # ...

  def create_comment

    insert_comment_query = <<-SQL
      INSERT INTO comments (body, author, post_id, created_at)
      VALUES (?, ?, ?, ?)
    SQL

    connection.execute insert_comment_query,
      params['body'],
      params['author'],
      params['post_id'],
      Date.current.to_s

      redirect_to "/show_post/#{params['post_id']}"
  end
  # ...

end

The primary difference in creating a comment, as opposed to creating a post, is our concern for post_id. When creating a comment, we'll need to be sure to associate it with the right post. This is essentially done in three steps.

First, we point the <form> action to a path that includes post.id:

<form method="post" action="/create_comment_for_post/<%= post.id %>">

Then we capture this post_id in the route:

post '/create_comment_for_post/:post_id' => 'application#create_comment'

And finally, we make sure to set the post_id for the row in the comments table:

connection.execute insert_comment_query,
  params['body'],
  params['author'],
  params['post_id'],
  Date.current.to_s

This way, when a comment is created through this process, it is always associated with the post identified by the ID in the URL. At the end of create_comment, we just redirect back to show_post, which will end up rendering the post again, now with a new comment.

Similar to how we moved the SQL command to retrieve comments associated to a post to the Post model, we'll do the same to the SQL command to create a comment associated to a post.

### app/controllers/application_controller.rb ###

class ApplicationController < ActionController::Base

  # ...

  def create_comment
    post = Post.find(params['post_id'])
    post.create_comment('body' => params['body'], 'author' => params['author'])
    redirect_to "/show_post/#{params['post_id']}"
  end
  # ...

end

Now the Post model will look like:

class Post

  # ...

  def create_comment(attributes)
    comment = Comment.new(attributes.merge!('post_id' => id))
    comment.save
  end

  # ...

end

Then the Comment model:

app/models/comment.rb

class Comment

  # ...

  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 save
    if new_record?
      insert
    else
      # update # ...not yet defined
    end
  end

  def insert
    insert_comment_query = <<-SQL
      INSERT INTO comments (body, author, post_id, created_at)
      VALUES (?, ?, ?, ?)
    SQL

    connection.execute insert_comment_query,
      @body,
      @author,
      @post_id,
      Date.current.to_s
  end

  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
end

Notice that we have nearly identical database connections in Comment as we do in Post. We'll be extracting these commonalities later on.