はじめに
本記事はRailsでいいね機能を実装しようという他の方の記事を参考に、そこで実装したいいね機能のN+1問題を修正することが目的です。
いいね機能をまだ実装していない方は上記の記事を参考に実装しておいてください。
やりたいこと
少しデザインが変わっていますが、単に見やすくしただけなので気にしないでください。
変わった部分は一覧ページでもいいねができるようにしたくらいです。
画像のように投稿者のemailと本文、いいね数、いいねボタンを一覧表示させ、N+1にも配慮していきましょう。
前提知識
- N+1って何?という方向け
- 参考になるもの
やってみる
下準備
こちらは任意ですが、今回はN+1問題を検出してくれるbulletというGemを利用します。
group :development do
gem 'bullet'
end
$ bundle install
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もあらかじめキャッシュしておきます。
preload
やeager_load
についてはこちらを参考にしてください。
(本来表示させるのはemailではなくuser_name
とかにした方がいいでのですが、今回はサンプルなのでこのままにします。)
def index
@posts = Post.all #削除
@posts = Post.preload(:likes, :user) #追加
end
修正2
いいねの数はcount
ではなくsize
を使うことが肝です。
count
はキャッシュを使わず毎回SQLのCOUNTを実行するため、N+1の原因になります。
size
はキャッシュがあればそれを使い、なければCOUNTを実行してくれます。(参考)
先ほどposts_controller
でpreload
を用いてlikes
をキャッシュしたため、今回はSQLのCOUNTは発行されません。
(<%= 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が含まれていればいいねしている。
#削除
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
の配列が出来上がります。
## 修正4
こちらはviewのいいねしているか判定です。
いいねボタンも追加しました。
# 削除
<% 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]]
お疲れ様でした。
私自身まだ理解が浅く誤った点があるかもしれませんが、記事は随時アップデートしていきます。