Ask LSBot

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.

This conversation with LSBot is temporary. Sign up for free to save your conversations with LSBot.

Hi! I'm LSBot. I'm here to help you understand this chapter content with fast, focused answers.

Ask me about concepts, examples, or anything you'd like clarified from this chapter. I can explain complex topics, provide examples, or help connect ideas.

Want to know more? Refer to the LSBot User Guide.

This conversation with LSBot is temporary. Sign up for free to save your conversations with LSBot.

Hi! I'm LSBot. I'm here to help you think through this exercise by providing hints and guidance, without giving away the solution.

You can ask me about your approach, request clarification on the problem, or seek help when you feel stuck.

Want to know more? Refer to the LSBot User Guide.