Rails
activeadmin

ActiveAdmin で大きなデータを扱うときは has_many <=> belongs_to に注意しよう

More than 3 years have passed since last update.

今回は ActiveAdmin である程度大きなデータを扱う際に気をつけるべきことと、その対策について書いてみたいと思います。

ステージング環境では快適に動いているのに本番環境では動かない・・・!?

ActiveAdmin がステージング環境では快適に動いているのに本番環境では重すぎて使い物にならない・・・なんてことに遭遇したことはありませんか?これは ActiveAdmin の性質上、割りと発生しがちな問題だったりします。

原因は ActiveAdmin が has_many <=> belongs_to の関係を解決しようとするから

ActiveAdmin は自動的に検索条件を付けてくれるのですが、そこでモデル同士のリレーションやカーディナリティを解決しようとします。その結果、たとえば利用者のブックマークを扱うようなシステム(user : boomark = 1 : 多)があった場合、ブックマークの管理画面を開く時に users テーブルが全件検索されています。

それによって、ステージング環境では 100 人前後のユーザーしか登録されていないので特にパフォーマンス的な問題は発生しないが、本番環境では 10 万人とか 100 万人とかの利用者がいて、それに対して全件検索が走ることによって重すぎて途端に使い物にならなくなってしまう、という事象が発生してしまうのです。

たとえば

こんな感じのテーブル構造になっていて

app/models/user.rb
class User < ActiveRecord::Base
  has_many :bookmarks
end
app/models/bookmark.rb
class Bookmark < ActiveRecord::Base
  belongs_to :user
end
users bookmarks
id id
: user_id
: :

ブックマークの管理画面を開くと検索条件に全ユーザーのセレクトボックスが登場。この裏では SELECT * FROM users という恐ろしいクエリが走っているのです。。。

activeadmin_対処前.png

解決策は?

前述のような全ユーザーのセレクトボックスが配置されても、10 万人や 100 万人の中から該当のユーザーを探し出すなんてことはできないので、(1) いっそのことそれを検索条件から省いてしまうか、(2) もしくは代替として id を指定して検索できるようにしてしまいましょう。

(1) 検索条件から省く

必要な項目のみ filter で定義してしまえば終了です。

app/admin/bookmarks.rb
ActiveAdmin.register Bookmark do
  filter :created_at
  filter :updated_at
end

activeadmin_対処(1).png

(2) id を指定して検索できるようにする

検索条件として user_id を指定できるようにして、ActiveAdmin がモデルに探しにいく user_id_contains というメソッドを定義すれば終了です。

app/admin/bookmarks.rb
ActiveAdmin.register Bookmark do
  filter :user_id, as: :string, label: 'user id'
  filter :created_at
  filter :updated_at
end
app/models/bookmark.rb
class Bookmark < ActiveRecord::Base
  use_active_admin_contains_search(:user_id)
end
config/initializers/active_admin.rb
class ActiveRecord::Base
  def self.use_active_admin_contains_search(attr_name)
    scope "#{attr_name}_contains", ->(id_list) { where("#{attr_name} IN (?)", id_list.split(',').map(&:strip)) }
    search_methods "#{attr_name}_contains"
  end
end

activeadmin_対処(2).png

ここでは、その都度モデル側で scope 〜 search_methods を定義しなくていいように、それをひとまとめにした use_active_admin_contains_search というメソッドを定義、モデル側ではこれにカラムを指定してもらうだけにしました。また、id はカンマ区切りで複数指定できるように、かつカンマ周辺のスペースは取り除いて検索条件とするようにしました。

まとめ

ActiveAdmin は色んなことを手早く実現させてくれて非常に便利なのですが、ある程度大きなデータを扱う際には注意が必要です。なんだか ActiveAdmin が重たいなと思ったときは filter を疑ってみましょう。