Bulletがいつも正しいわけではない？！

この記事はプログラミング学習者がアプリ開発中に躓いた内容を備忘録として記事におこしたものです。内容に不備などあればご指摘頂けると助かります。

私は普段のプログラミング学習中にRailsのgemでN+1問題や不要なeager loading(includes)を検知してくれるBulletを使用しながらアプリの制作をしています。
自分でも気付き難いN+1問題などもありますので、普段は自動検知して教えてくれて助かっています。

今回はそんなBulletが検知してくれるN+1問題や不要なincludesがどうやらいつも正しくはないようだ、との考えの基に記事を書いています。

まずは検知メッセージと実際のコードをご紹介します。

対象のeager loading(不要なincludes)検知メッセージ
![スクリーンショット 2024-09-13 13.19.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3508918/66e71273-d0cb-d454-8a51-e376edbda527.png)

```ruby:user.rb
  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
```
```ruby:comment.rb
  belongs_to :user
  belongs_to :tweet
  validates :sentence, presence: true, length: { in: 1..140 }
  has_many_attached :images
```
```ruby:tweet.rb
  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
```

```ruby:users_controller.rb
  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`を取得する
その時に同時取得で下記のデータも取得する。
<dl>
  <dt>tweet(ツイート・投稿)</dt>
  <dd>commentsが属するもの(どのtweetのコメントか？)</dd>
  <dt>user: { icon_attachment: :blob }</dt>
  <dd>tweetに属するユーザーとそのユーザーのアイコン画像</dd>
  <dt>{ images_attachments: :blob }</dt>
  <dd>tweetに付属する画像(一緒に投稿されたもの)</dd>
</dl>
このコードを実装することで現ユーザーのコメントとそれに関連するデータをまとめて取得でき、N+1問題や不要なincludesを回避することができます。
users_controllerの他の取得文も似たような内容です。

```ruby:show.html.slim
- @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を設けたコード
```ruby:bullet.rb
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](https://github.com/flyerhzm/bullet)