7
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-06-17

はじめに

本記事はRailsでいいね機能を実装しようという他の方の記事を参考に、そこで実装したいいね機能のN+1問題を修正することが目的です。
いいね機能をまだ実装していない方は上記の記事を参考に実装しておいてください。

やりたいこと

スクショ

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

前提知識

やってみる

下準備

こちらは任意ですが、今回はN+1問題を検出してくれるbulletというGemを利用します。

Gemfile
group :development do
  gem 'bullet'
end
$ bundle install
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

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

修正1

今回は投稿者のemailも表示するため、userもあらかじめキャッシュしておきます。
preloadeager_loadについてはこちらを参考にしてください。
(本来表示させるのはemailではなくuser_nameとかにした方がいいでのですが、今回はサンプルなのでこのままにします。)

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

修正2

いいねの数はcountではなくsizeを使うことが肝です。
countはキャッシュを使わず毎回SQLのCOUNTを実行するため、N+1の原因になります。
sizeはキャッシュがあればそれを使い、なければCOUNTを実行してくれます。(参考

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

posts/index.html.erb
 (<%= post.liked_users.count %>) #削除
 <p>いいねの数: <%= post.likes.size %></p> #追加

修正3

いいねしてるかの判定です。
ここが一番のメインというか、大変でした。
というのもexits?postの数だけ下記のSQLが発行されてしまうからです。

ターミナル
 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が含まれていればいいねしている。

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_idpluckで配列にし、その中にcurrent_userのidが含まれているかチェックしています。
pluckを使うことで、[1,3,8,11,13,20]というようなuser_idの配列が出来上がります。

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

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にも怒られません。

ターミナル
 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]]

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

7
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?