【Rails】N+1問題に配慮していいね機能を実装


# はじめに
本記事は[Railsでいいね機能を実装しよう](https://qiita.com/nojinoji/items/2c66499848d882c31ffa)という他の方の記事を参考に、そこで実装したいいね機能のN＋1問題を修正することが目的です。
いいね機能をまだ実装していない方は上記の記事を参考に実装しておいてください。


## やりたいこと
<img width="300px" alt="スクショ" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631045/35ce07af-6d93-642d-af17-aae7751fb506.png">

少しデザインが変わっていますが、単に見やすくしただけなので気にしないでください。
変わった部分は一覧ページでもいいねができるようにしたくらいです。
画像のように投稿者のemailと本文、いいね数、いいねボタンを一覧表示させ、N＋1にも配慮していきましょう。

## 前提知識

* N+1って何？という方向け
    * [【Ruby on Rails】N+1問題ってなんだ？](https://qiita.com/massaaaaan/items/4eb770f20e636f7a1361)
* 参考になるもの
    * [Railsアプリケーションを遅くするActiveRecordの3つの間違い：Count,Where,Present](https://scoutapm.com/blog/three-activerecord-mistakes-ja)

## やってみる

## 下準備
こちらは任意ですが、今回はN+1問題を検出してくれる[bullet](https://github.com/flyerhzm/bullet)というGemを利用します。

```ruby:Gemfile	
group :development do
  gem 'bullet'
end
```
```shell
$ bundle install
```
```ruby:config/environments/development.rb 
config.after_initialize do
  Bullet.enable = true
  Bullet.alert = true
  Bullet.bullet_logger = true
  Bullet.console = true
  Bullet.rails_logger = true
end
```

現段階だとブラウザ再読み込みすればアラートが出てくるかと思います。
それでは直していきましょう。

## 修正１
今回は投稿者のemailも表示するため、userもあらかじめキャッシュしておきます。
`preload`や`eager_load`については[こちら](https://qiita.com/k0kubun/items/80c5a5494f53bb88dc58)を参考にしてください。
（本来表示させるのはemailではなく`user_name`とかにした方がいいでのですが、今回はサンプルなのでこのままにします。）

```ruby:posts_controller
def index
  @posts = Post.all #削除
  @posts = Post.preload(:likes, :user) #追加
end
```
## 修正2

**いいねの数は`count`ではなく`size`を使うことが肝です。**
`count`はキャッシュを使わず毎回SQLのCOUNTを実行するため、N+1の原因になります。
`size`はキャッシュがあればそれを使い、なければCOUNTを実行してくれます。（[参考](https://www.lanches.co.jp/blog/3199)）

先ほど`posts_controller`で`preload`を用いて`likes`をキャッシュしたため、今回はSQLのCOUNTは発行されません。

```erb:posts/index.html.erb
 (<%= post.liked_users.count %>) #削除
 <p>いいねの数: <%= post.likes.size %></p> #追加
```
## 修正3
いいねしてるかの判定です。
ここが一番のメインというか、大変でした。
というのも`exits?`で`post`の数だけ下記のSQLが発行されてしまうからです。

```shell:ターミナル
 Like Exists? (0.1ms)  SELECT 1 AS one FROM "likes" WHERE "likes"."user_id" = ? AND "likes"."post_id" = ? LIMIT ?  [["user_id", 5], ["post_id", 3], ["LIMIT", 1]]
```

キャッシュを使ってSQLの発行を抑えられないか試みましたが、うまくできませんでした。
なので判定方法自体を変えました。
この辺はもっといい方法があると思います...

**変更前：** `exists?`を用い`current_user`のidが格納された`user_id`と投稿の`post_id`がセットになっているレコードが存在していればいいねしている。
**変更後：** likeの`user_id`を配列にし、その中に`current_user`のidが含まれていればいいねしている。


```ruby:user.rb
 #削除
 def already_liked?(post)
   self.likes.exists?(post_id: post.id)
 end

 #追加
 def already_liked?(like, current_user_id)
   like.pluck(:user_id).include?(current_user_id)
 end
```
`current_user`はモデルで使うことができないため、viewの呼び出し元で引数にして渡してあげます。
受け取ったlikeのうち`user_id`を`pluck`で配列にし、その中に`current_user`のidが含まれているかチェックしています。
`pluck`を使うことで、`[1,3,8,11,13,20]`というような`user_id`の配列が出来上がります。

##　修正４
こちらはviewのいいねしているか判定です。
いいねボタンも追加しました。

```erb:posts/index.html.erb
# 削除
 <% if current_user.already_liked?(@post) %>

# 追加
 <% if current_user.already_liked?(post.likes, current_user.id) %>
   <%= button_to 'いいねを取り消す', post_like_path(post, post_id: post.id), method: :delete %>
 <% else %>
   <%= button_to 'いいね', post_likes_path(post.id) %>
 <% end %>
```
## 動作確認
もしうまくいかない場合は適宜修正してください。
最終的にSQLは4回の発効に抑えられました。
これで`bullet`にも怒られません。

```shell:ターミナル
 User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? ORDER BY "users"."id" ASC LIMIT ?  [["id", 5], ["LIMIT", 1]]
  ↳ app/views/posts/index.html.erb:2
  Post Load (0.3ms)  SELECT "posts".* FROM "posts"
  ↳ app/views/posts/index.html.erb:13
  Like Load (0.2ms)  SELECT "likes".* FROM "likes" WHERE "likes"."post_id" IN (?, ?, ?, ?)  [[nil, 1], [nil, 2], [nil, 3], [nil, 4]]
  ↳ app/views/posts/index.html.erb:13
  User Load (0.2ms)  SELECT "users".* FROM "users" WHERE "users"."id" IN (?, ?, ?)  [[nil, 1], [nil, 4], [nil, 5]]
```

お疲れ様でした。
私自身まだ理解が浅く誤った点があるかもしれませんが、記事は随時アップデートしていきます。