既存アプリのN+1問題の解消

#はじめに
既存アプリにおいてActiveStorageで画像を取得する際発生していたN+1問題を改善したため、これを機にN+1問題についてまとめてみました。

##実行環境
・macOS
・Rails 6.1.4
・Ruby 2.6.7

#N+1問題とは
モデルに対してeach文などで繰り返しを行う時、そのモデルに紐付く要素に対してモデルの数分SQLクエリが発行されてしまう問題のこと。レコード数が多くなってしまうとレスポンスが遅くなるなどの影響が出てしまう。

自分のアプリで実際にN+1問題が起きていた箇所を例に見ていきます。

```ruby:shop.rb
class Shop < ApplicationRecord
  belongs_to :user
end
```

userがshopを投稿するため上の関連付けになっています

```ruby:shops_controller.rb
class ShopsController < ApplicationController
  def index
    @shops = Shop.all
  end
end
```

shopレコードは上のコードで取得します。

```ruby:viewコード
<% if current_user?(shop.user) %>
  <div class="manipulate_shop">
    <%= link_to icon("far", "edit"), edit_shop_path(shop), class: "edit_icon" %>
    | <%= link_to icon("far", "trash-alt"), shop, method: :delete, data: { confirm: "本当に削除しますか？" }, class: "trash_icon" %>
  </div>
<% end %>
```

ログインユーザーが店舗の投稿主かどうかでレイアウトを変えるコードです。eachがありませんがshopを繰り返し読み込んでいます。
このviewコードを読み込む際N+1問題が以下のように発生します。

```zsh:ターミナル 
SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]
SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 2], ["LIMIT", 1]]
SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 3], ["LIMIT", 1]]
```

shopに紐付くuserの数だけSQLを発行しています。
このままではレコードが多くなるほどSQLも多く発行されるので、将来的にレスポンスが遅くなってしまいます。
そのため、コントローラでshopを取得するときにincludesを用いて一緒にそのshopを投稿したuserも取得します。

```ruby:shops_controller.rb
class ShopsController < ApplicationController
  def index
    @shops = Shop.all.includes(:user)
  end
end
```

以下がincludesを使った後のSQLクエリです。一回の問い合わせでレコードを取得してくれるようになりました。
IN (1, 2, 3)は WHERE "users"."id" = 1 OR "users"."id" = 2 OR "users"."id" = 3と同じでusersテーブルからidが1か２か３がtrueになるものでテーブルを作成しています。

```zsh:ターミナル 
SELECT "users".* FROM "users" WHERE "users"."id" IN ($1, $2, $3)  [["id", 1], ["id", 2], ["id", 3]]
```

これでN+1問題が解消されました。

#ActiveStorageでのN+1問題
shopモデルに画像を紐づけるためにActiveStorageを使いimagesとしてアタッチしました。

```ruby:shop.rb
class Shop < ApplicationRecord
  belongs_to :user
  has_many_attached :images
end
```

先ほどのshops_contorollerと同じindexアクションから取得した@shopsを使って以下のコードを書きました。
画像が紐づいているかどうかで画像表示を変えています。この`shop.images.first`の部分でN+1問題が発生しました。

```ruby:viewコード
<%= link_to shop do %>
  <% if shop.images.attached? %>
    <%= image_tag shop.images.first, class: "card-img" %>
  <% else %>
    <%= image_tag "noimage.png", class: "card-img" %>
  <% end %>
<% end %>
```
以下が発行されたSQLクエリです。N+1問題が発生しております。

```zsh:ターミナル 
SELECT "active_storage_attachments".* FROM "active_storage_attachments" WHERE "active_storage_attachments"."record_id" = $1 AND "active_storage_attachments"."record_type" = $2 AND "active_storage_attachments"."name" = $3 ORDER BY "active_storage_attachments"."id" ASC LIMIT $4  [["record_id", 3], ["record_type", "Shop"], ["name", "images"], ["LIMIT", 1]]
SELECT "active_storage_blobs".* FROM "active_storage_blobs" WHERE "active_storage_blobs"."id" = $1 LIMIT $2  [["id", 5], ["LIMIT", 1]]
SELECT "active_storage_attachments".* FROM "active_storage_attachments" WHERE "active_storage_attachments"."record_id" = $1 AND "active_storage_attachments"."record_type" = $2 AND "active_storage_attachments"."name" = $3 ORDER BY "active_storage_attachments"."id" ASC LIMIT $4  [["record_id", 2], ["record_type", "Shop"], ["name", "images"], ["LIMIT", 1]]
SELECT "active_storage_blobs".* FROM "active_storage_blobs" WHERE "active_storage_blobs"."id" = $1 LIMIT $2  [["id", 2], ["LIMIT", 1]]
SELECT "active_storage_attachments".* FROM "active_storage_attachments" WHERE "active_storage_attachments"."record_id" = $1 AND "active_storage_attachments"."record_type" = $2 AND "active_storage_attachments"."name" = $3 ORDER BY "active_storage_attachments"."id" ASC LIMIT $4  [["record_id", 1], ["record_type", "Shop"], ["name", "images"], ["LIMIT", 1]]
SELECT "active_storage_blobs".* FROM "active_storage_blobs" WHERE "active_storage_blobs"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]
```

ActiveStorageの場合は`with_attached_つけた名前`でN+1問題を解消できます。shopレコードに関連するactive_storage_attachmentsとactive_storage_blobsをまとめて取得してくれます。

```ruby:shops_controller
class ShopsController < ApplicationController
  def index
    @shops = Shop.all.includes(:user).with_attached_images
  end
end
```

以下が改善後のSQLクエリです。今回の場合、改善後により多くのデータを取得してしまっていますがSQLクエリの回数が抑えられています。

```zsh:ターミナル 
SELECT "active_storage_attachments".* FROM "active_storage_attachments" WHERE "active_storage_attachments"."record_type" = $1 AND "active_storage_attachments"."name" = $2 AND "active_storage_attachments"."record_id" IN ($3, $4, $5)  [["record_type", "Shop"], ["name", "images"], ["record_id", 3], ["record_id", 2], ["record_id", 1]]
SELECT "active_storage_blobs".* FROM "active_storage_blobs" WHERE "active_storage_blobs"."id" IN ($1, $2, $3, $4, $5, $6, $7, $8)  [["id", 1], ["id", 2], ["id", 3], ["id", 4], ["id", 5], ["id", 6], ["id", 7], ["id", 8]]
```

以上です。