【Rails】いいねボタンを作ろう part2/2

## この記事について
- [【Rails】いいねボタンを作ろう part1/2](http://qiita.com/mohira/items/9ae35773e6209adbf0a0)の続きです

## こちらもどうぞ
- [【Rails】ミニアプリを作りながら多対多のアソシエーションとネストされたルーティングを学ぶ](http://qiita.com/mohira/items/2362dc905999b013a746)

## 前回までのあらすじ
- UserとPostあたりを実装した

## 今回の目標
- いよいよ「いいねボタン」をつくる

## 改めて設計の確認
- [これが本丸]**ログインユーザーは各ブログ記事に1回だけいいね！ することができる**
  - いいね！ は1ユーザー1ブログ記事につき1回の制限
  - 自分のブログ記事に対してもいいね！ することはできる
- 既に、いいね！ をしているブログ記事に対して、いいね！ を解除することができる
  - ボタンは1つで実装し、いいね！ の状態によって切り替える
- ブログ記事詳細画面で、いいね！ 件数および、どのユーザーがいいね！ しているかが表示される

## Likeモデルをつくる
- post_id と user_id をもつだけ
  - **どの記事**に**誰**がいいねしたかという情報

```bash
$ rails g model Like post:references user:references
```

MigrationスクリプトにはNull制約だけ追加

```rb:db/migrate/yyyymmddhhiiss_create_likes.rb
class CreateLikes < ActiveRecord::Migration[5.0]
  def change
    create_table :likes do |t|
      t.references :post, null: false, foreign_key: true
      t.references :user, null: false, foreign_key: true

      t.timestamps
    end
  end
end
```

```bash:
$ rails db:migrate
```


## seedファイル追記
- アソシエーションやデータ取得方法が重要なので、データはseedで作る
- ユーザーは3人
- 各ユーザーが各記事にいいね！している状態
- idを振り直したいので、`rails db:reset`

```rb:db/seeds.rb
3.times do |i|
  i += 1
  user = User.create(
    email: "user#{i}@example.com",
    password: 'password'
  )

  3.times do |j|
    j += 1
    Post.create(
      title: "#{user.email}の記事 その#{j}",
      body: "body#{j} by #{user.email}",
      user_id: user.id
    )

    Like.create(post_id: i, user_id: j)
  end
end
```

```bash
$ rails db:reset
```

## Like絡みのアソシエーション
- Likeモデル登場により生まれるモデル間の関係性を考える

### 参考図(手書き)
- 中間テーブルを含むイメージ画像を書きました
- PostはLikeを介してUserにアクセスできる(メソッド名は**liked_users**)
- UserはLikeを介してPostにアクセスできる(メソッド名は**liked_posts**)

![ink-image.png](https://qiita-image-store.s3.amazonaws.com/0/79919/66109bf6-ae63-c0df-7d27-203a94e94713.png)


### Postモデル
- 各モデルとの関係性は2つ
  1. 1件のpostは複数のlikesを持つ(色々なユーザーが、1件のブログ記事にいいね！ をする)
  2. **[こっちがポイント]**1件のpostが持つ複数のlikesを介して、複数のusersとつながっている(この記事にいいねした全てのユーザー)

```rb:app/models/post.rb
class Post < ApplicationRecord
  # ...

  # 1の関係
  has_many :likes

  # 2の関係
  has_many :liked_users, through: :likes, source: :user

end
```

#### `source: :user`について
- `source`は`has_many ... through`を使うときにどのモデルと関連させるかを表現するときに使う
- 例によって、Railsのパターンどおりであれば、この記述は不要
- しかし、今回はpostからlikesを介してuserにアクセスするときのメソッドを`liked_users`にしているのでこの記述は必要

##### sourceのメリット
- メリット1: 自由なメソッド名を付けられるのでメソッド名から取得内容を判断しやすい
  - ブログ記事(post)目線で、いいね(like)を経由したユーザー(user)との関係性を考えると、これらのユーザーはいいね！をした人(liked_users)にあたるので`liked_users`を採用した(別に好きなようにすればよい)
- メリット2: 例えば、参考リンク先のような状況でも対応できる(ユーザー情報はUserモデルで管理しているが、user同士に商品の買い手と売り手といった関係性が存在する場合など)
  - [参考: has_many :through の関連に同一モデルを含む場合【rails4】](http://www.coma-tech.com/archives/223/)
  - **自己結合**とか**自己関連**と呼ぶらしい

### Userモデル
- 考え方はPostのときと同様
- Like絡みの関係性は2つ
  1. [簡単]1人のuserは複数のlikesを持つ(1人が複数の投票できる)
      - dependent: :destroyはつけておく
  2. **[重要]**1人のuserがいいねした複数のlikesから、postsを辿れる(自分がいいねをした全ての記事)
      - その際のメソッドを`liked_posts`にしている

```rb:app/models/user.rb
class User < ApplicationRecord
  has_many :posts, dependent: :destroy
  has_many :likes, dependent: :destroy
  has_many :liked_posts, through: :likes, source: :post

  # ...
end
```

## コンソールで試す
- アソシエーションを設定したのでコンソールで確かめる
- 少々見づらいので`pp`を利用し、なおかつ余計な出力は省略している
  - [参考: ppライブラリ - Ruby](https://docs.ruby-lang.org/ja/latest/library/pp.html)

### Post
```bash
$ rails c
Running via Spring preloader in process 46772
Loading development environment (Rails 5.0.1)
irb > require 'pp'
irb > p1 = Post.first
irb > pp p1.likes
[#<Like:0x007fe688b25e78
  id: 1,
  post_id: 1,
  user_id: 1,
  created_at: Thu, 26 Jan 2017 07:43:16 UTC +00:00,
  updated_at: Thu, 26 Jan 2017 07:43:16 UTC +00:00>,
 #<Like:0x007fe68a085408
  id: 2,
  post_id: 1,
  user_id: 2,
  created_at: Thu, 26 Jan 2017 07:43:16 UTC +00:00,
  updated_at: Thu, 26 Jan 2017 07:43:16 UTC +00:00>,
 #<Like:0x007fe68a0849b8
  id: 3,
  post_id: 1,
  user_id: 3,
  created_at: Thu, 26 Jan 2017 07:43:16 UTC +00:00,
  updated_at: Thu, 26 Jan 2017 07:43:16 UTC +00:00>]
irb > pp p1.liked_users
[#<User id: 1, email: "user1@example.com", created_at: "2017-01-26 07:43:16", updated_at: "2017-01-26 07:43:16">,
 #<User id: 2, email: "user2@example.com", created_at: "2017-01-26 07:43:16", updated_at: "2017-01-26 07:43:16">,
 #<User id: 3, email: "user3@example.com", created_at: "2017-01-26 07:43:16", updated_at: "2017-01-26 07:43:16">]
```

### User
```bash
irb > u1 = User.first
irb > pp u1.likes
[#<Like:0x007fe68c2aae20
  id: 1,
  post_id: 1,
  user_id: 1,
  created_at: Thu, 26 Jan 2017 07:43:16 UTC +00:00,
  updated_at: Thu, 26 Jan 2017 07:43:16 UTC +00:00>,
 #<Like:0x007fe68c2aa880
  id: 4,
  post_id: 2,
  user_id: 1,
  created_at: Thu, 26 Jan 2017 07:43:16 UTC +00:00,
  updated_at: Thu, 26 Jan 2017 07:43:16 UTC +00:00>,
 #<Like:0x007fe68c2aa5b0
  id: 7,
  post_id: 3,
  user_id: 1,
  created_at: Thu, 26 Jan 2017 07:43:16 UTC +00:00,
  updated_at: Thu, 26 Jan 2017 07:43:16 UTC +00:00>]
irb > pp u1.liked_posts
[#<Post:0x007fe68c281ae8
  id: 1,
  title: "user1@example.comの記事 その1",
  body: "body1 by user1@example.com",
  user_id: 1,
  created_at: Thu, 26 Jan 2017 07:43:16 UTC +00:00,
  updated_at: Thu, 26 Jan 2017 07:43:16 UTC +00:00>,
 #<Post:0x007fe68c2819a8
  id: 2,
  title: "user1@example.comの記事 その2",
  body: "body2 by user1@example.com",
  user_id: 1,
  created_at: Thu, 26 Jan 2017 07:43:16 UTC +00:00,
  updated_at: Thu, 26 Jan 2017 07:43:16 UTC +00:00>,
 #<Post:0x007fe68c281868
  id: 3,
  title: "user1@example.comの記事 その3",
  body: "body3 by user1@example.com",
  user_id: 1,
  created_at: Thu, 26 Jan 2017 07:43:16 UTC +00:00,
  updated_at: Thu, 26 Jan 2017 07:43:16 UTC +00:00>]
```



## 1人1記事1いいねValidation
- 次に、いいね回数に制限をかける
- 具体的には「1人1記事1いいね」
  - 今回は自分の投稿にもいいねをつけられることにする(Twitterど同じ)
- 色々な方法がありそうだが、モデルのバリデーションだけ紹介

```
class Like < ApplicationRecord
  belongs_to :user
  belongs_to :post

  validates_uniqueness_of :post_id, scope: :user_id
end
```

## いいねボタン
- いよいよ「いいね！」ボタンをつくる

### ログイン/ログアウトをやりやすくするためのheader作成
- いいねはログインが必要であり、色々なユーザーで試すにはログアウト作業もしなくてはならない
- その作業をスムーズにするために、共通レイアウトにheaderを追加する

```html+erb:app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>LikeBtn</title>
    <%= csrf_meta_tags %>

    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <header>
      <% if current_user %>
        <%= current_user.email %>さんとしてログインしています
        <%= link_to 'ログアウトする', destroy_user_session_path, method: :delete %>
      <% else %>
        <%= link_to 'ログインはこちら', user_session_path %>
      <% end %>
    </header>
    <%= yield %>
  </body>
</html>
```


### Likesコントローラ生成
- 素のコントローラ生成

```bash
$ rails g controller Likes
```

### いいね周りのルーティング
- いいねをする → likes#create
- いいねを取り消す → likes#destroy
  - 論理削除ではなく、そのままデータを削除する

```rb:config/routes.rb
Rails.application.routes.draw do

  # ...

  resources :posts, only: [:index, :show] do
    resources :likes, only: [:create, :destroy]
  end

  # ...
end
```

### Likes#create
- `@post`を記述しておかないとバリデーションに引っかかったときに`@post`(元々はposts#showで宣言していた)の情報を失う
- `post_id`は`params[:post_id]`で取れることを意識する
- Deviseの効果でログインユーザーの情報は`current_user`で取れる


```rb:app/controllers/likes_controller.rb
class LikesController < ApplicationController
  def create
    @post = Post.find(params[:post_id])

    @like = Like.new(
      post_id: params[:post_id],
      user_id: current_user.id
    )

    if @like.save
      redirect_to post_path(@post)
    else
      render template: 'posts/show'
    end
  end

  private
    def like_params
      params.require(:like).permit(:post_id, :user_id)
    end
end
```


### いいねボタン
- posts#show で 空のオブジェクト(`@like`)を生成
- `form_for`の第1引数がポイント
  - `[親モデル, 子モデル]`という記述になる
  - この場合、親:Post 子:Like

```rb:app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def index
    # ...
  end

  def show
    @post = Post.find(params[:id])
    @like = Like.new() # 追記
  end
end
```

```html+erb:app/view/posts/show.html.erb
<h1>タイトル: <%= @post.title %></h1>
<h2>投稿者: <%= @post.user.email %>さん</h2>

<h3>本文</h3>
<p><%= @post.body %></p>

<%= form_for [@post, @like] do |f| %>
  <%= f.submit 'いいね！' %>
<% end %>
```

### 実際に試してみる
- まずはLikeモデルのデータをすべて消しておく(バリデーションなどはあとで)

```bash
$ rails c
irb > Like.destroy_all
```

- ログインした状態で`/posts/1`にアクセスして「いいね」を押すと登録されるはず(DBを見て欲しい)
- ただし、2件以上は登録されない(Likeモデルの`validates_uniqueness_of`の働き)

### それぞれの記事にいいね！しているユーザー一覧の表示
- `has_many ... through`というアソシエーションを設定しているの簡単に取り出せる

```html+erb:app/view/posts/show.html.erb
<h1>タイトル: <%= @post.title %></h1>
<h2>投稿者: <%= @post.user.email %>さん</h2>

<h3>本文</h3>
<p><%= @post.body %></p>

<%= form_for [@post, @like] do |f| %>
  <%= f.submit 'いいね！' %>
<% end %>

<h2>この記事にいいねしたユーザー</h2>
<% @post.liked_users.each do |user| %>
  <li><%= user.email %></li>
<% end %>
```

### いいねに対するバリデーションメッセージを表示する
- せっかくなのでバリデーションメッセージも表示する
- 赤文字で表示するだけ

```html+erb:app/view/posts/show.html.erb
<h1>タイトル: <%= @post.title %></h1>
<h2>投稿者: <%= @post.user.email %>さん</h2>

<h3>本文</h3>
<p><%= @post.body %></p>

<%= form_for [@post, @like] do |f| %>
  <% if @like.errors.any? %>
    <% @like.errors.full_messages.each do |msg| %>
      <li style="color: red;"><%= msg %></li>
    <% end %>
  <% end %>
  <%= f.submit 'いいね！' %>
<% end %>

<h2>この記事にいいねしたユーザー</h2>
<% @post.liked_users.each do |user| %>
  <li><%= user.email %></li>
<% end %>
```

### いいね取り消し機能
- いろいろな書き方があると思う
- 細かいところはさておき、ひとまずの機能を実装
- 既にいいねしているか？ でボタン表記を制御(いいね可能かどうか？ ではない！)
  - `User.already_liked?`を実装
- いいね取り消しは論理削除ではなく物理削除を採択


```rb:app/models/user.rb
class User < ApplicationRecord
  # ...

  def already_liked?(post)
    self.likes.exists?(post_id: post.id)
  end
end
```

```rb:app/controllers/likes_controller.rb
class LikesController < ApplicationController
  def create
    # ...
  end

  def destroy
    @like = Like.find_by(post_id: params[:post_id], user_id: current_user.id)
    @like.destroy
    redirect_to post_path(params[:post_id])
  end

  private
    # ...
end
```

```html+erb:app/view/posts/show.html.erb
<!-- ...  -->

<% if current_user.already_liked?(@post) %>
  <%= button_to 'いいねを取り消す', post_like_path(@post), method: :delete %>
<% else %>
  <%= form_for [@post, @like] do |f| %>
    <% if @like.errors.any? %>
      <% @like.errors.full_messages.each do |msg| %>
        <li style="color: red;"><%= msg %></li>
      <% end %>
    <% end %>
    <%= f.submit 'いいね！' %>
  <% end %>
<% end %>

<!-- ... -->
```

### いいね件数を表示
```html+erb:app/view/posts/show.html.erb
<h1>タイトル: <%= @post.title %></h1>
<h2>投稿者: <%= @post.user.email %>さん</h2>

<h3>本文</h3>
<p><%= @post.body %></p>

<!-- ココ追記 -->
<h3>いいね件数: <%= @post.likes.count %></h3>

<% if current_user.already_liked?(@post) %>
  <%= button_to 'いいねを取り消す', post_like_path(@post), method: :delete %>
<% else %>
  <%= form_for [@post, @like] do |f| %>
    <% if @like.errors.any? %>
      <% @like.errors.full_messages.each do |msg| %>
        <li style="color: red;"><%= msg %></li>
      <% end %>
    <% end %>
    <%= f.submit 'いいね！' %>
  <% end %>
<% end %>

<h2>この記事にいいねしたユーザー</h2>
<% @post.liked_users.each do |user| %>
  <li><%= user.email %></li>
<% end %>
```

## ユーザー権限の制御
- posts#index(ブログ記事一覧あるいは各ユーザーの記事一覧)以外はログインユーザーのみ

```rb:app/controllers/posts_controller.rb
class PostsController < ApplicationController
  before_action :authenticate_user!, except: [:index]

  def index
    # ...
  end

  def show
    # ...
  end
end
```

```rb:app/controllers/likes_controller.rb
class LikesController < ApplicationController
  before_action :authenticate_user!, only: [:create, :destroy]

  def create
    # ...
  end

  def destroy
    # ...
  end

  private
    # ...

end
```

# まとめ
- 無事にいいねボタンを実装した
- やはり重要なのはモデル同士がどう関連しているか、ということ
- Ajax化などもあると思うが、今回はここまで
- ここまでの実装にはGitHub上で`v2`というタグをつけています[mohira/like-btn v2](https://github.com/mohira/like-btn/tree/v2)