今回は ActiveAdmin である程度大きなデータを扱う際に気をつけるべきことと、その対策について書いてみたいと思います。
ステージング環境では快適に動いているのに本番環境では動かない・・・!?
ActiveAdmin がステージング環境では快適に動いているのに本番環境では重すぎて使い物にならない・・・なんてことに遭遇したことはありませんか?これは ActiveAdmin の性質上、割りと発生しがちな問題だったりします。
原因は ActiveAdmin が has_many <=> belongs_to の関係を解決しようとするから
ActiveAdmin は自動的に検索条件を付けてくれるのですが、そこでモデル同士のリレーションやカーディナリティを解決しようとします。その結果、たとえば利用者のブックマークを扱うようなシステム(user : boomark = 1 : 多)があった場合、ブックマークの管理画面を開く時に users テーブルが全件検索されています。
それによって、ステージング環境では 100 人前後のユーザーしか登録されていないので特にパフォーマンス的な問題は発生しないが、本番環境では 10 万人とか 100 万人とかの利用者がいて、それに対して全件検索が走ることによって重すぎて途端に使い物にならなくなってしまう、という事象が発生してしまうのです。
たとえば
こんな感じのテーブル構造になっていて
class User < ActiveRecord::Base
has_many :bookmarks
end
class Bookmark < ActiveRecord::Base
belongs_to :user
end
users | bookmarks | |
---|---|---|
id | id | |
: | user_id | |
: | : |
ブックマークの管理画面を開くと検索条件に全ユーザーのセレクトボックスが登場。この裏では SELECT * FROM users
という恐ろしいクエリが走っているのです。。。
解決策は?
前述のような全ユーザーのセレクトボックスが配置されても、10 万人や 100 万人の中から該当のユーザーを探し出すなんてことはできないので、(1) いっそのことそれを検索条件から省いてしまうか、(2) もしくは代替として id を指定して検索できるようにしてしまいましょう。
(1) 検索条件から省く
必要な項目のみ filter
で定義してしまえば終了です。
ActiveAdmin.register Bookmark do
filter :created_at
filter :updated_at
end
(2) id を指定して検索できるようにする
検索条件として user_id
を指定できるようにして、ActiveAdmin がモデルに探しにいく user_id_contains
というメソッドを定義すれば終了です。
ActiveAdmin.register Bookmark do
filter :user_id, as: :string, label: 'user id'
filter :created_at
filter :updated_at
end
class Bookmark < ActiveRecord::Base
use_active_admin_contains_search(:user_id)
end
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
ここでは、その都度モデル側で scope 〜 search_methods
を定義しなくていいように、それをひとまとめにした use_active_admin_contains_search
というメソッドを定義、モデル側ではこれにカラムを指定してもらうだけにしました。また、id はカンマ区切りで複数指定できるように、かつカンマ周辺のスペースは取り除いて検索条件とするようにしました。
まとめ
ActiveAdmin は色んなことを手早く実現させてくれて非常に便利なのですが、ある程度大きなデータを扱う際には注意が必要です。なんだか ActiveAdmin が重たいなと思ったときは filter
を疑ってみましょう。