FBCのActive Storageを使ったユーザーアイコンの表示機能を実装しています。 Active Storageを使用した画像取得時にN+1問題が発生していると指摘を受けたので解決法を調べました。
結論
Active StorageのN+1問題はwith_attached_attachment_name
スコープを使うことで解決する。
N+1問題とは?
N+1問題は関連モデルで他のモデルの情報を参照する場合に発生するケースが殆どです。
【Ruby on Rails】N+1問題ってなんだ? - Qiita
ORMとは?
N+1問題を調べていると頻出するORM
について調べました。
RDB(Relational Database: 関係データベース)に対するデータの操作をオブジェクト指向型言語のやり方で扱えるようにするための手法
Object Relational Mapper
の略O/Rマッパー
と書かれている場合もある- SQLを直接書く必要が無くなる(Railsの場合はActive Recordがこの働きを担っている)
- RailsもORMの一つ
- N+1問題が起こりやすいので注意が必要
関連モデルを使った場合のN+1の対策
has_many
やbelongs_to
等を使用したアソシエーションを組まれている場合に、一覧画面で関連先の情報を取得する際にN+1問題が発生することが多いようです。
関連モデルを使った場合のN+1問題については下記のメソッドを使って対策するようです。
include
preload
eager_load
ActiveRecordのincludesは使わずにpreloadとeager_loadを使い分ける理由 - Qiita
Rails で includes して N+1 問題対策 - Qiita
Active StorageのN+1の対策
今記事の本題です。 今回の場合は他のモデルとのアソシエーションは組んでおらず、次のように1つのモデルでActive Storageを使用した画像表示機能でN+1問題が発生しています。
# app/models/user.rb class User < ApplicationRecord devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable has_one_attached :icon # ←Active Storageで画像表示用の属性を追加 end
# app/controllers/users_controller.rb class UsersController < ApplicationController def index @users = User.order(:id).page(params[:page]) end end
.page(params[:page])
はkaminari gemでのページネーション用メソッドorder(:id)
でid
順でレコードを並び替え
# app/views/users/index.html.erb <% @users.each do |user| %> <tr> <% if user.icon.attached? %> <td><%= image_tag user.icon.variant(resize_to_fit: [500, 500]).processed, class: 'user-icon' %> # ←ここでN+1が発生 </td> <% else %> <td></td> <% end %> <td><%= user.email %></td> <td><%= user.name %></td> <td><%= user.postal_code %></td> <td><%= user.address %></td> <td><%= link_to t('views.common.show'), user %></td> </tr> <% end %>
上記の場合でusers#index
にアクセスするとuserの数だけSELECT文が発行されN+1が発生しています。
Rendering layout layouts/application.html.erb Rendering users/index.html.erb within layouts/application User Load (0.1ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? OFFSET ? [["LIMIT", 25], ["OFFSET", 0]] ↳ app/views/users/index.html.erb:16 ActiveStorage::Attachment Load (0.1ms) 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", "User"], ["name", "icon"], ["LIMIT", 1]] ↳ app/views/users/index.html.erb:18 ActiveStorage::Attachment Load (0.0ms) 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", "User"], ["name", "icon"], ["LIMIT", 1]] ↳ app/views/users/index.html.erb:18 ActiveStorage::Attachment Load (0.1ms) 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", "User"], ["name", "icon"], ["LIMIT", 1]] ↳ app/views/users/index.html.erb:18
Active Storageは画像表示用のテーブルを用意し、そのテーブルに画像を保存する仕組みです。
Active Storageを使った画像アップロード機能のセットアップ - karlley's tech blog
この画像保存用のテーブルから画像取得する際の重複したSQLを排除することでN+1問題を解決できます。
Railsに用意されているwith_attached_attachment_name
スコープを使うと良いようです。
Active StorageのN+1問題を解決する - Qiita
users#index
メソッドをActive Storageに用意されているwith_attached_attachement_name
スコープを使って次のように修正します。
with_attached_attachment_name
のattachement_name
はActive Storageをセットアップする際にapp/models/user.rb
に追加したhas_one_attached :icon
の属性を指します(今回の場合だと:icon
)。
# app/controllers/users_controller.rb class UsersController < ApplicationController def index @users = User.order(:id).page(params[:page]) # これを↓のように修正 @users = User.with_attached_icon.order(:id).page(params[:page]) # with_attached_attachment_name を使う end end
修正後のSQLは次のように表示する画像取得をIN句を使ってまとめて取得するようになります。
Rendering layout layouts/application.html.erb Rendering users/index.html.erb within layouts/application User Load (0.3ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? OFFSET ? [["LIMIT", 25], ["OFFSET", 0]] ↳ app/views/users/index.html.erb:16 ActiveStorage::Attachment Load (0.6ms) 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", "User"], ["name", "icon"], ["record_id", 1], ["record_id", 2], ["record_id", 3], ["record_id", 4], ["record_id", 5], ["record_id", 6], ["record_id", 7], ["record_id", 8], ["record_id", 9], ["record_id", 10], ["record_id", 11], ["record_id", 12], ["record_id", 13], ["record_id", 14], ["record_id", 15], ["record_id", 16], ["record_id", 17], ["record_id", 18], ["record_id", 19], ["record_id", 20], ["record_id", 21], ["record_id", 22], ["record_id", 23], ["record_id", 24], ["record_id", 25]]
with_attached_attachment_name
は内部的にinclude
を使用しているようです。
Active StorageのN+1問題に対処する - シュッと開発日記
ActiveStorage::Attached::Model | RailsDoc(β)
この辺の仕組みはまだ理解があいまいなので、再度深堀りしてみようと思います。
まとめ
前々から気になっていたN+1問題への理解が深まったので良かった。 スコープ等の理解が全然できていないので、今後もRailsの深い部分を学んでいきたい。
参照
【Ruby on Rails】N+1問題ってなんだ? - Qiita
ActiveRecordのincludesは使わずにpreloadとeager_loadを使い分ける理由 - Qiita
Rails で includes して N+1 問題対策 - Qiita
Active Storageを使った画像アップロード機能のセットアップ - karlley's tech blog
Active StorageのN+1問題を解決する - Qiita