Ruby on Rails いいね機能の実装と解説（Rails Tutorial 14章 演習の機能拡張）

# 目的

Rails Tutorial 14章の最難関、【サンプルアプリケーションの機能を拡張する】の注釈にある、
いいね機能を実装したので、__Rails Tutorial風__ に解説してみたいと思います。

# 環境

 - Rails Tutorialと同じ環境

# 第14α章 マイクロポストをいいねする

この章では、サンプルアプリケーションの中にあればいいかな？という部分を完成させます。
具体的には、他のユーザのマイクロポストをいいね（およびいいね解除）できる機能を追加します。
また、１人のユーザが同じマイクロポストに複数回いいねすることもできないようにします。
本書の中で学んだ内容を思い出せば、この最終章（仮）もそれほど苦労はしないでしょう。

では、実装に入る前の事前準備として、以下にモック（完成版）を示します。
そして、まずはモデルから考え、その後コントローラ、ビュー、その他アクションの順で実装して
いきたいと思います。

<img width="1047" alt="スクリーンショット 2018-01-12 13.31.20.png" src="https://qiita-image-store.s3.amazonaws.com/0/207081/aaf911d6-b813-5de1-b96f-9aa0e574cefb.png">
図 14α-1: いいねボタン追加後のプロフィールページ


## 14α.1 Likeモデル

マイクロポストをいいねする機能を実装する第一歩は、データモデルを構成することです。
前章を読み込んでいる読者であれば、__has_many__ を使用すれば、実装できることが分かるでしょう。

では、まずはこれまで同様新しいトピックブランチを作成してください。

```console
$ git checkout -b iine
```


### 14α.1.1 データモデルの構造

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

```console
$ rails generate model Like user_id:integer micropost_id:integer
```

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

```ruby: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 :micropost_id, null: false

      t.timestamps

      t.index :user_id
      t.index :micropost_id
      t.index [:user_id, :micropost_id], unique: true
    end
  end
end
```

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

では、likesテーブルを作成するために、いつものようにデータベースのマイグレーションを行います。

```console
$ rails db:migrate
```


### 14α.1.2 User/Like、Micropost/Likeの関連付け

１つのマイクロポストには１対多（__has_many__）のいいねがあり、１人のユーザには１対多（__has_many__）の
いいねがあります。いいねはその両方に属します（__belongs_to__）。
前章では、関連名をactive_relationshipsとしていたため、class_nameの記載が必要でしたが、
本章では余計なことをしないため、不要となります。

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

```ruby:app/models/user.rb
class User < ApplicationRecord
  .
  .
  .
  has_many :likes, dependent: :destroy
  .
  .
  .
end
```

```ruby:app/models/micropost.rb
class Micropost < ApplicationRecord
  .
  .
  .
  has_many :likes, dependent: :destroy
  .
  .
  .
end
```

```ruby:app/models/like.rb
class Like < ApplicationRecord
  belongs_to :user
  belongs_to :micropost
  validates :user_id, presence: true
  validates :micropost_id, presence: true
end
```

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


## 14α.2 いいね機能を実装する

モデルの作成が完了したので、いよいよいいね機能に取りかかります。
ここでは、コントローラ、ビューについて作成していきましょう。

### 14α.2.1 ルーティング

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

```console
$ rails generate controller Likes create destroy
```

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

| HTTPリクエスト | URL | アクション | 名前付きルート |
| :-- | :-- | :-- | :-- |
| POST | /likes | create | likes_path |
| DELETE | /likes/1 | destroy | like_path(like) |
表 14α-1: Likesリソースが提供するRESTfulルート


### 14α.2.2 いいね機能（仮）を実装する

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

”誰”がいいねをするかというと、ログインしているユーザとなるので、current_userです。

”どのマイクロポスト”にいいねするかというと、いいねボタンは、__図 14α-1__ を見て分かるとおり、
各マイクロポストに対して配置していますので、マイクロポストのいいね機能をMicropostモデルに
記載することが分かります。ユーザーのフォローの際は、フォローボタンをユーザーページに配置し、
フォロー機能をUserモデルに記載していましたが、それと同様です。

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

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

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

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

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

  def create
    @micropost = Micropost.find(params[:micropost_id])
    @micropost.iine(current_user)
  end

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

## 14α.3 いいねボタンを実装する

コントローラでの処理ができましたので、次はいいねボタンを実装していきます。

Bootstrapには便利なiconがありますので、それを利用していきたいと思います。
また、フォローボタンと同じように、いいねボタンを押した時にページの再ロードが起こらないように、
Ajaxを使用して非同期でリクエストを飛ばすようにします。


### 14α.3-1 いいねボタンの配置とデザイン

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

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

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

```erb
<%= render "likes/like", micropost: micropost %>
```

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

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

```erb:app/views/likes/_like.html.erb
<% if !current_user?(micropost.user) %>
    <span class="like">
    <% if micropost.iine?(current_user) %>
      <%= form_for(micropost.likes.find_by(user_id: current_user.id), method: :delete, remote: true) do |f| %>
        <%= button_tag(class: "btn btn-default btn-xs") do %>
          <%= content_tag :span, "#", class: "glyphicon glyphicon-heart" %>
        <% end %>
      <% end %>
    <% else %>
      <%= form_for(micropost.likes.build, remote: true) do |f| %>
        <div><%= hidden_field_tag :micropost_id, micropost.id %></div>
        <%= button_tag(class: "btn btn-default 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の後ろに該当の文字列が表示されます。ここには後ほど、いいね数を表示するようにします。

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

```erb
<% if micropost.iine?(current_user) %>
```

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

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


### 14α.3-2 いいねボタン(Ajax化)

上記までの実装で、基幹部分の実装ができていますが、いいねボタン押下時の描画の実装をしていません。
その描画をAjaxを使用して実装していきます。これはフォローボタンのAjax化とほとんど同じです。

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

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

  def create
    @micropost = Micropost.find(params[:micropost_id])
    unless @micropost.iine?(current_user)
      @micropost.iine(current_user)
      respond_to do |format|
        format.html { redirect_to request.referrer || root_url }
        format.js
      end
    end
  end

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

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

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

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

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

...どちらも実施していることは一緒ですね。
フォローの場合は、フォロー用パーシャルとアンフォロー用パーシャルが分かれていたかと思いますが、
いいねの場合は、パーシャルを分けていません。その理由は忘れましたが、分けて実装しようとした際に
出来なかった気がします。もし出来るようでしたらご教授いただきますよう、よろしくお願いいたします。

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


### 14α.3-3 いいね数のカウントとcounter_culture

いいね数のカウント方法ですが、railsには __counter_cache__ という機能があります。
しかし、どうやらより高機能である、 __counter_culture__ というgemがあるみたいですので、
そちらを利用したいと思います。

counter_cultureのREADMEを読みながら実装していきます。

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

group :development, :test do
.
.
.
```

いつものようにbundle installを実行して、counter_cultureをインストールします。

```console
$ bundle install
```

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

```console
$ rails generate migration add_likes_count_to_microposts likes_count:integer
```

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

```ruby:db/migrate/[timestamp]_add_likes_count_to_microposts.rb
class AddLikesCountToMicroposts < ActiveRecord::Migration[5.1]
  class MigrationUser < ApplicationRecord
    self.table_name = :microposts
  end

  def up
    _up
  rescue => e
    _down
    raise e
  end

  def down
    _down
  end

  private

  def _up
    MigrationUser.reset_column_information

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

  def _down
    MigrationUser.reset_column_information

    remove_column :microposts, :likes_count if column_exists? :microposts, :likes_count
  end
end
```

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

```console
$ rails db:migrate
```

ここで、冪等性が担保されているか確認するため、再度マイグレーションを行います。

```console
$ rails db:migrate:redo
```

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

```ruby:app/models/like.rb
class Like < ApplicationRecord
  belongs_to :user
  belongs_to :micropost
  counter_culture :micropost
  validates :user_id, presence: true
  validates :micropost_id, presence: true
end
```

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

```erb:app/views/likes/_like.html.erb
<% if !current_user?(micropost.user) %>
    <span class="like">
    <% if micropost.iine?(current_user) %>
      <%= form_for(micropost.likes.find_by(user_id: current_user.id), method: :delete, remote: true) do |f| %>
        <%= button_tag(class: "btn btn-default btn-xs") do %>
          <%= content_tag :span, "#{micropost.likes_count}", class: "glyphicon glyphicon-heart" %>
        <% end %>
      <% end %>
    <% else %>
      <%= form_for(micropost.likes.build, remote: true) do |f| %>
        <div><%= hidden_field_tag :micropost_id, micropost.id %></div>
        <%= button_tag(class: "btn btn-default btn-xs") do %>
          <%= content_tag :span, "#{micropost.likes_count}", class: "glyphicon glyphicon-heart-empty" %>
        <% end %>
      <% end %>
    <% end %>
  </span>
<% end %>
```
これで、完成となります。実際に動かしてみましょう。









.
.
.






動かしてみましたか？

筆者は完成したと思って動かしてみたら、カウント数が正しくないことに気づきました。
「うぉぉおおおきたぁぁああああ」って喜んて恥ずかしい思いをしました。
原因はビューに渡しているMicropostが、いいねする前の状態となっているからです。
これは、コントローラでiine後にreloadをすれば解決します。

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

  def create
    @micropost = Micropost.find(params[:micropost_id])
    unless @micropost.iine?(current_user)
      @micropost.iine(current_user)
      @micropost.reload
      respond_to do |format|
        format.html { redirect_to request.referrer || root_url }
        format.js
      end
    end
  end

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

これで本当に完成となります。実際に動かして確認してみましょう。
※testについては、本項では記載いたしません。


## 14α.4 本番環境へデプロイする

testが完了したら、masterブランチに取り込みましょう。

```console
$ rails test
$ git add -A
$ git commit -m "Add micropost iine"
$ git checkout master
$ git merge iine
```

コードをリポジトリにpushして、本番環境にデプロイしてみましょう。

```console
$ git push
$ git push heroku
$ heroku pg:reset DATABASE
$ heroku run rails db:migrate
$ heroku run rails db:seed
```

# 感想

railsを勉強してから２ヶ月で、ほぼ自分一人で初めて１つの機能を作りましたが、
先人の知恵(gem)の探し方がとても難しいと感じました。
これを期に、どんな先人の知恵があるのか、トレンドなどを見るようにしたいと思います。