ActiveStorage : N+1問題を回避しよう

Active Storage の N+1

Recipeモデルがあります。 カラムはnameと、ActiveStorageと関連付けられたavatar

ActiveStorageは、モデルの中にimageが保存されるわけではなく、 active_storage_attachmentsテーブルとactive_storage_blobsテーブルとの 関連付けで管理されています。

だから、VIEWで単純にループしちゃうと以下のようなSQLが発行されてしまいます。

def index
  @recipes = Recipe.all
end
<ul>
  <% @recipes.each do |recipe| %>
    <li>
      <p><%= recipe.name %></p>
      <%= image_tag recipe.avatar %>
    </li>
  <% end %>
</ul>
Processing by RecipesController#index as HTML
  Rendering recipes/index.html.erb within layouts/application
  Recipe Load (0.2ms)  SELECT "recipes".* FROM "recipes"
  ↳ app/views/recipes/index.html.erb:5
  ActiveStorage::Attachment Load (0.3ms)  SELECT  "active_storage_attachments".* FROM "active_storage_attachments" WHERE "active_storage_attachments"."record_id" = ? AND "active_storage_attachments"."record_type" = ? AND "active_storage_attachments"."name" = ? LIMIT ?  [["record_id", 1], ["record_type", "Recipe"], ["name", "avatar"], ["LIMIT", 1]]
  ↳ app/views/recipes/index.html.erb:8
  ActiveStorage::Blob Load (0.2ms)  SELECT  "active_storage_blobs".* FROM "active_storage_blobs" WHERE "active_storage_blobs"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
  ↳ app/views/recipes/index.html.erb:8
  ActiveStorage::Attachment Load (0.3ms)  SELECT  "active_storage_attachments".* FROM "active_storage_attachments" WHERE "active_storage_attachments"."record_id" = ? AND "active_storage_attachments"."record_type" = ? AND "active_storage_attachments"."name" = ? LIMIT ?  [["record_id", 2], ["record_type", "Recipe"], ["name", "avatar"], ["LIMIT", 1]]
  ↳ app/views/recipes/index.html.erb:8
  ActiveStorage::Blob Load (0.1ms)  SELECT  "active_storage_blobs".* FROM "active_storage_blobs" WHERE "active_storage_blobs"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
  ↳ app/views/recipes/index.html.erb:8
  ActiveStorage::Attachment Load (0.3ms)  SELECT  "active_storage_attachments".* FROM "active_storage_attachments" WHERE "active_storage_attachments"."record_id" = ? AND "active_storage_attachments"."record_type" = ? AND "active_storage_attachments"."name" = ? LIMIT ?  [["record_id", 3], ["record_type", "Recipe"], ["name", "avatar"], ["LIMIT", 1]]
  ↳ app/views/recipes/index.html.erb:8
  ActiveStorage::Blob Load (0.1ms)  SELECT  "active_storage_blobs".* FROM "active_storage_blobs" WHERE "active_storage_blobs"."id" = ? LIMIT ?  [["id", 3], ["LIMIT", 1]]
  ↳ app/views/recipes/index.html.erb:8
  ActiveStorage::Attachment Load (0.2ms)  SELECT  "active_storage_attachments".* FROM "active_storage_attachments" WHERE "active_storage_attachments"."record_id" = ? AND "active_storage_attachments"."record_type" = ? AND "active_storage_attachments"."name" = ? LIMIT ?  [["record_id", 4], ["record_type", "Recipe"], ["name", "avatar"], ["LIMIT", 1]]
  ↳ app/views/recipes/index.html.erb:8
  ActiveStorage::Blob Load (0.1ms)  SELECT  "active_storage_blobs".* FROM "active_storage_blobs" WHERE "active_storage_blobs"."id" = ? LIMIT ?  [["id", 4], ["LIMIT", 1]]
  ↳ app/views/recipes/index.html.erb:8
  Rendered recipes/index.html.erb within layouts/application (22.8ms)

1: Modelへのアクセス

2: active_storage_attachments

3: active_storage_blobs

2': active_storage_attachments

3': active_storage_blobs

. .

しっかり、N+1問題が発生します。 なので、これを回避するために以下のようにします。

def index
  @recipes = Recipe.with_attached_image
end
Recipe Load (0.2ms)  SELECT "recipes".* FROM "recipes"
  ↳ app/views/recipes/index.html.erb:5
  ActiveStorage::Attachment Load (0.3ms)  SELECT "active_storage_attachments".* FROM "active_storage_attachments" WHERE "active_storage_attachments"."record_type" = ? AND "active_storage_attachments"."name" = ? AND "active_storage_attachments"."record_id" IN (?, ?, ?, ?)  [["record_type", "Recipe"], ["name", "avatar"], ["record_id", 1], ["record_id", 2], ["record_id", 3], ["record_id", 4]]
  ↳ app/views/recipes/index.html.erb:5
  ActiveStorage::Blob Load (0.7ms)  SELECT "active_storage_blobs".* FROM "active_storage_blobs" WHERE "active_storage_blobs"."id" IN (?, ?, ?, ?)  [["id", 1], ["id", 2], ["id", 3], ["id", 4]]

ActiveStorage は、このように自前に用意したスコープで、 includesすることで、N+1問題を回避します。

RailsのN+1問題の回避に関しては以下の記事も参考に。

10 Tips for Eager Loading to Avoid n+1 Queries in Rails

REF

Rails 5.2新機能を先行チェック!Active Storage/ダイレクトアップロード/Early Hintsほか(翻訳)