Help us understand the problem. What is going on with this article?

rails twitter風アプリで「いいね」機能を追加

目的

railsにて、オリジナルのSNSアプリ(「Kuishare」 URL:https://kuishare.herokuapp.com)
に「いいね」機能を追加します。

ここでいう「いいね」機能はtwitterの「いいね」と同じ意味合いです。

環境

rails 5.1.6

「いいね」機能に関与するモデルの現在の構造(今回関与しない属性は省略します)
User(ユーザー情報用モデル)
→name
Post(投稿用モデル)
→content

1.データモデルの構造

データモデルの構造は、ユーザをフォローする機能と少し似ています。簡単に言うと、「いいね」をする”誰か”
”どのマイクロポスト”「いいね」したかを管理するモデルを追加すればいいんです。
このデータモデルを実装するために、マイグレーションを生成します。

$ rails generate model Like user_id:integer post_id:integer

また、追加したカラムでの検索が頻繁に行われるため、インデックスも追加します。

db/migrate/[timestamp]_create_likes.rb
class CreateLikes < ActiveRecord::Migration[5.1]
  def change
    create_table :likes do |t|
      t.integer :user_id, null: false
      t.integer :post_id, null: false

      t.timestamps

      t.index :user_id
      t.index :post_id
      t.index [:user_id, :post_id], unique: true
    end
  end
end

複合キーインデックスがあります。これにより、user_idとpost_idの
組み合わせが必ずユニークであることを保証し、1ユーザが同じマイクロポストに複数回いいねすることを防ぎます。

データベースのマイグレーションを行います。

$ rails db:migrate

2. User/Like、Post/Likeの関連付け

1つのポストには1対多(has_many)のいいねがあり、1人のユーザには1対多(has_many)のいいねがあります。
いいねはその両方に属します(belongs_to)。

UserとLike、PostとLikeの関係をコードにまとめると、以下のようになります。

app/models/user.rb
class User < ApplicationRecord
  .
  .
  .
  has_many :likes, dependent: :destroy
  .
  .
  .
end
app/models/post.rb
class Post < ApplicationRecord
  .
  .
  .
  has_many :likes, dependent: :destroy
  .
  .
  .
end
app/models/like.rb
class Like < ApplicationRecord
  belongs_to :user
  belongs_to :post
  validates :user_id, presence: true
  validates :post_id, presence: true
end

User、Postには、それぞれが削除されたらLikeも削除する、
Likeにはuser_id、post_idが必須である、という記載もしています。

3.ルーティング

Likesコントローラには、
createとdestoryがあれば十分です。コントローラを作成し、ルーティングを修正しましょう。
その際、コントローラ作成時に自動的に追加されるルーティングの削除を忘れないようにしてください。

config/routes.rb
Rails.application.routes.draw do
  .
  .
  .
  resources :likes, only: [:create, :destroy]
  .
  .
  .
end

4.いいね機能(仮)を実装する

ルーティングができたので、機能について考えましょう。
考え方のヒントは「”誰”が”どのポスト”にいいねしたか」です。

”誰”がいいねをするかは、ログインしているユーザです。

”どのポスト”にいいねするかは、いいねが配置さているポスト。

いいねは各ポストに対して配置していますので、マイクロポストのいいね機能をPostモデルに記載します。

これで、likesテーブルのレコードを作る情報が全て出揃いましたので、いいね機能(仮)を実装できるはずです。

app/models/post.rb
class Post < ApplicationRecord
  .
  .
  .
  # マイクロポストをいいねする
  def iine(user)
    likes.create(user_id: user.id)
  end

  # マイクロポストのいいねを解除する(ネーミングセンスに対するクレームは受け付けません)
  def uniine(user)
    likes.find_by(user_id: user.id).destroy
  end
  .
  .
  .
end

これらのメソッドをコントローラから呼び出せば(仮)の完成です。

app/controllers/likes_controller.rb
class LikesController < ApplicationController
  before_action :logged_in_user

  def create
    @post = Post.find(params[:post_id])
    @post.iine(current_user)
  end

  def destroy
    @post = Like.find(params[:id]).post
    @post.uniine(current_user)
  end
end

5.いいねボタンを実装する

いいねボタンはポストの下に配置するので、PostビューにLikeビューを配置することが分かります。

app/views/microposts/_post.html.erb
<li id="post-<%= post.id %>" data-post-id="<%= post.id %>">
.
.
.
  <span class="timestamp">
    .
    .
    .
  </span>
  <%= render "likes/like", post: post %>
</li>

render時に見慣れない記述があります。

<%= render "likes/like", post: post %>

Likesコントローラではポストのidが必要になるため、Likeビューをパーシャル化する際に、
Postをビューに渡してあげる必要があります。

次に、上記で呼んでいるLikeのパーシャルは以下のようになります。

app/views/likes/_like.html.erb
<% if !current_user?(post.user) %>
    <span class="like">
    <% if post.iine?(current_user) %>
      <%= form_for(post.likes.find_by(user_id: current_user.id), method: :delete, remote: true) do |f| %>
        <%= button_tag(class: "btn btn-xs") do %>
          <%= content_tag :span, "#", class: "glyphicon glyphicon-heart" %>
        <% end %>
      <% end %>
    <% else %>
      <%= form_for(post.likes.build, remote: true) do |f| %>
        <div><%= hidden_field_tag :post_id, post.id %></div>
        <%= button_tag(class: "btn btn-xs") do %>
          <%= content_tag :span, "#", class: "glyphicon glyphicon-heart-empty" %>
        <% end %>
      <% end %>
    <% end %>
  </span>
<% end %>

ざっくり説明すると以下の通りになります。

・いいねボタンは自分のマイクロポスト以外の場合に表示
・既にいいねしている場合は、いいねボタンを塗りつぶされたハートとし、押下時にdestroyを呼ぶ
・いいねしていない場合は、いいねボタンを塗りつぶされていないハートとし、押下時にcreateを呼ぶ

button_tag配下にcontent_tagを指定し、classにBootstrapのiconのクラスを指定すると、
ボタンにクラスに応じた可愛いiconが表示されます。content_tagにある"#"の箇所に文字列を入れれば、
そのiconの後ろに該当の文字列が表示されます。ここには後ほど、いいね数を表示するようにします。

ここで、既にいいねされているかどうかを判断する必要が出てきました。

<% if post.iine?(current_user) %>

このメソッドをPostモデルに作成します。その際に、モデルの関連も追加したいと思います。
どのような関連かというと、「ポストにいいねをしたユーザーの一覧」という関連です。
これを追加すれば、あとはそのユーザ一覧にcurrent_userがいれば、既にいいねしている、
いなければ、まだいいねしていないということが分かります。

app/models/post.rb
class Post < ApplicationRecord
  .
  .
  .
  has_many :iine_users, through: :likes, source: :user
  .
  .
  .
  # 現在のユーザーがいいねしてたらtrueを返す
  def iine?(user)
    iine_users.include?(user)
  end
  .
  .
  .
end

6.いいねボタンAjax化

上記までの実装で、基幹部分の実装ができていますが、いいねボタン押下時の描画の実装をしていません。
その描画をAjaxを使用して実装していきます。

まずはコントローラをAjaxリクエストに対応させます。

app/controllers/likes_controller.rb
class LikesController < ApplicationController
  def create
    @post = Post.find(params[:post_id])
    unless @post.iine?(current_user)
      @post.iine(current_user)
      @post.reload
      respond_to do |format|
        format.html { redirect_to request.referrer || root_url }
        format.js
      end
    end
  end

  def destroy
    @post = Like.find(params[:id]).post
    if @post.iine?(current_user)
      @post.uniine(current_user)
      @post.reload
      respond_to do |format|
        format.html { redirect_to request.referrer || root_url }
        format.js
      end
    end
  end
end

ここで、既にいいねされているかどうかの確認を追加していることにも注意してください。likeテーブルは
user_id、post_idがPKとなっているため、2つのブラウザで同時にいいねされてもエラーと
ならないようにしています。

次に、js用のerbを作成します。

app/views/likes/create.js.erb
$("#post-<%= @post.id %> .like").html("<%= escape_javascript(render "likes/like", post: @post) %>");
app/views/likes/destroy.js.erb
$("#post-<%= @post.id %> .like").html("<%= escape_javascript(render "likes/like", post: @post) %>");

これで、いいね機能が正しく動作するようになりました。しかし、まだ終わりではありません。
いいねの数をいいねボタンの横に表示する必要があります。

7.いいね数のカウントとcounter_culture

いいね数のカウント方法ですが、railsには counter_cultureというgemがあります。
これを利用したいと思います。

Gemfile
source 'https://rubygems.org'
.
.
.
gem 'counter_culture', '~> 1.8'

group :development, :test do
.
.
.

bundle installを実行して、counter_cultureをインストールします。

$ bundle install

次に、必要なテーブルにカウント数を格納するカラムを追加します。今回は、Postテーブルに
いいね数を格納したいので、Postテーブルに追加します。

$ rails generate migration add_likes_count_to_microposts likes_count:integer

生成されたマイグレーションファイルを編集します。

db/migrate/[timestamp]_add_likes_count_to_posts.rb
class AddLikesCountToPosts < ActiveRecord::Migration[5.1]
  class MigrationUser < ApplicationRecord
    self.table_name = :posts
  end

  def up
    _up
  rescue => e
    _down
    raise e
  end

  def down
    _down
  end

  private

  def _up
    MigrationUser.reset_column_information

    add_column :posts, :likes_count, :integer, null: false, default: 0 unless column_exists? :posts, :likes_count
  end

  def _down
    MigrationUser.reset_column_information

    remove_column :posts, :likes_count if column_exists? :posts, :likes_count
  end
end

ここでは私の練習のため、changeメソッドは使用せず、up、downメソッドを使用しています。
なぜ分割しているかというと、migrationファイルは冪等性(べきとうせい)を担保する必要があり、
upとdownで処理を分ける場合があるからみたいです。(むずかしい。。)
冪等性が担保されなかった場合、rollbackが正しく行われず、中途半端なDB状態となる恐れがあります。
(私たちのプロジェクトでも過去のmigrationファイルの冪等性が担保されていなかったために、
開発環境のDBが壊れ、バックアップから復元する羽目になりました。)
では、いつものようにデータベースのマイグレーションを行います。

$ rails db:migrate

そして関連付けを行います。

app/models/like.rb
class Like < ApplicationRecord
  belongs_to :user
  belongs_to :post
  counter_culture :post
  validates :user_id, presence: true
  validates :post_id, presence: true
end

これでいいねのカウントを自動的に集計し、Postテーブルに持つことが出来るようになりました。
後はそれを表示するだけとなります。Bootstrapのiconを使用する際に、#としていた箇所を書き換えます。

app/views/likes/_like.html.erb
<% if !current_user?(post.user) %>
    <span class="like">
    <% if post.iine?(current_user) %>
      <%= form_for(post.likes.find_by(user_id: current_user.id), method: :delete, remote: true) do |f| %>
        <%= button_tag(class: "btn btn-xs") do %>
          <%= content_tag :span, "#{post.likes_count}", class: "glyphicon glyphicon-heart" %>
        <% end %>
      <% end %>
    <% else %>
      <%= form_for(post.likes.build, remote: true) do |f| %>
        <div><%= hidden_field_tag :post_id, post.id %></div>
        <%= button_tag(class: "btn btn-xs") do %>
          <%= content_tag :span, "#{post.likes_count}", class: "glyphicon glyphicon-heart-empty" %>
        <% end %>
      <% end %>
    <% end %>
  </span>
<% end %>

これで、いいね機能完成です。

感想

bootstrap-icon便利だと思いました。
ajaxを利用した箇所の理解が少し深まってよかったです!

baskenshiro
web開発を独学で学習し、web開発業務に携わることのできる自社開発企業に入社できました!! 現在は、awsを利用したインフラ構築やruby on rails でのwebシステム開発業務に携わっています。 独学での学習方法や、業務中に得た知識、つまづいた事など さまざまな技術ブログを投稿していきます!!
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away