この記事はプログラミング学習者がアプリ開発中に躓いた内容を備忘録として記事におこしたものです。内容に不備などあればご指摘頂けると助かります。
私は普段のプログラミング学習中にRailsのgemでN+1問題や不要なeager loading(includes)を検知してくれるBulletを使用しながらアプリの制作をしています。
自分でも気付き難いN+1問題などもありますので、普段は自動検知して教えてくれて助かっています。
今回はそんなBulletが検知してくれるN+1問題や不要なincludesがどうやらいつも正しくはないようだ、との考えの基に記事を書いています。
まずは検知メッセージと実際のコードをご紹介します。
対象のeager loading(不要なincludes)検知メッセージ
has_many :relations, dependent: :destroy
has_many :followers, through: :relations
has_one_attached :icon
has_one_attached :header
has_many :tweets, dependent: :destroy
has_many :favorites, dependent: :destroy
has_many :comments, dependent: :destroy
has_many :retweets, dependent: :destroy
belongs_to :user
belongs_to :tweet
validates :sentence, presence: true, length: { in: 1..140 }
has_many_attached :images
belongs_to :user
validates :content, presence: true, length: { in: 1..140 }
has_many_attached :images
has_many :favorites, dependent: :destroy
has_many :comments, dependent: :destroy
has_many :retweets, dependent: :destroy
def show
@favorites = @user.favorites.includes(tweet: [{ user: { icon_attachment: :blob } },
{ images_attachments: :blob }]).select(:tweet_id).distinct
@retweets = @user.retweets.includes(tweet: [{ user: { icon_attachment: :blob } },
{ images_attachments: :blob }]).select(:tweet_id).distinct
@comments = @user.comments.includes(tweet: [{ user: { icon_attachment: :blob } },
{ images_attachments: :blob }]).select(:tweet_id).distinct
@tweets = @user.tweets.includes({ user: { icon_attachment: :blob } }, { images_attachments: :blob })
end
private
def set_user
return unless current_user
@user = User.find(current_user.id)
end
念の為、コードの一部を解説しておきます。
-
@comments
の取得内容について
@user(現在ログインしているユーザー)
が作成したコメントcomments
を取得する
その時に同時取得で下記のデータも取得する。
- tweet(ツイート・投稿)
- commentsが属するもの(どのtweetのコメントか?)
- user: { icon_attachment: :blob }
- tweetに属するユーザーとそのユーザーのアイコン画像
- { images_attachments: :blob }
- tweetに付属する画像(一緒に投稿されたもの)
- @comments.each do |comment| #Bulletから指摘されている102行目
= link_to tweet_path(comment.tweet_id) #Bulletから指摘されている103行目
ul
li
object = link_to image_tag(comment.tweet.user.icon, alt: "Icon image", class: "icon_image", size: '50x50'), profile_path(comment.tweet.user.id)
li
object = link_to comment.tweet.user.name, profile_path(comment.tweet.user.id), class: 'link'
li 投稿日: #{comment.tweet.created_at.strftime('%Y/%m/%d %H:%M:%S')}
p = comment.tweet.content
- if comment.tweet.images.attached? #Bulletから指摘されている111行目
- comment.tweet.images.each do |tweet_image|
= image_tag tweet_image, size: '250x200', class: "tweet_image"
end
ここまでで実装しているコードの列挙と簡単な説明を記載しました。
検知されたメッセージの解説と対策
-
メッセージの簡単な説明
Remove from your query: .includes([:user])
とあります。
単純にクエリからincludes([:user])
を外してください、と理解できまず。
当初は@user.comments.includes...
書いているので、user情報は取得しているから要らないんじゃないか?と考えて削除して変更しましたが、エラーが発生してしまいました。
@user
のユーザーと.includes([:user])
のユーザーは別だったのです。
@user
は現在ログインしているユーザー
.includes([:user])
のユーザーは@comments
の場合で言えば、@user
がコメントしたtweet(投稿)
のユーザーとなります。
ということで、.includes([:user])
は削除してはアプリが機能しなくなることが分かりました。 -
誤検知の対策
.includes([:user])
は必要なことが分かりましたので、例外として扱う必要があります。
Bulletには例外を扱う方法としてsafelistというものがありました。
実際にsafelistを設けたコード
if defined?(Bullet)
Bullet.enable = true
Bullet.alert = true
Bullet.bullet_logger = true
Bullet.rails_logger = true
Bullet.add_footer = true
# Tweetモデルのuserに関するN+1問題を無視する様にBulletへ指示する内容
Bullet.add_safelist type: :n_plus_one_query, class_name: 'Tweet', association: :user
# 無駄なEager loading(不要なincludes)に対する警告を無視する内容
Bullet.add_safelist type: :unused_eager_loading, class_name: 'Tweet', association: :user
end
これで検知メッセージも発生せずに動作させることができるようになりました。
今回参考にしたサイト
bullet - github