概要
簡単にタグ機能を実装できるacts-as-taggable-onというgemがあり、とても便利だが、as_jsonなどのシリアライズするメソッドを使用した際ににN+1問題を引き起こす原因となることがあるのでメモ
詳細
このissueにも書いてあるが
-
acts_as_taggable_on
メソッドは、引数に指定した値に対応する*_list
という属性と、メソッドを自動的に定義する(例えば:tagsと指定すると、tag_list
というインスタンスの属性とメソッドが追加される) - このtag_listメソッドはincludesなどを用いてクエリ回数を減らすことができない(ここの詳細はそこまで深く追ってない)
- as_jsonは、SomeModel.attributes.keysで返ってくる値それぞれに対して、sendメソッドでメソッド化して実行した結果をハッシュにする
要は、複数のインスタンスに対して、関連するタグを取得するために毎回クエリを発行してしまうのだが、それをうまく回避するための策がない
解決策として、as_json(except: [:tag_list])
したり、同様の結果を返すメソッドを定義すればいいと思ったが、インターフェースを変えたくなかった(tag_list: [タグ1, タグ2...]
という結果をもうすでに他の場所で利用していた)ので、影響を与えないように、内部の処理だけを変える方法を考えてみた
回避策
いくつか考えたが、自分の環境では、モデルでオーバーライドするのが簡単かつ現実的であった。(あまりしたくないのはもちろんだが)
このtag_list
メソッドが実際に定義されているのは、モデルにinclude
されている無名モジュールなので、モデルで同名メソッドを定義してオーバーライドできる
acts_as_taggable_on :tags
def tag_list
tags.map(&:name)
end
実行してみる
SomeModel.includes(:tags).as_json
# @controller
render json: {
some_models: SomeModel.includes(:tags)
}
これでN+1問題は発生しないようにできた
気持ち
いっぱいソースコード読んだのでせっかくなのでメモっとこう