ループ内でアソシエーション以外の方法で参照クエリを実行しなければいけない場合に、
クエリ回数を最小限に抑える方法について記す。
状況
以下のような関係性の時、ユーザーが所有する全てのアイテムの名前を取得したい。
# アソシエーション
User
has_many: user_items
UserItem
# ### Columns
# Name |
# ---------------------------------- |
# **`user_id`** |
# **`item_id`** |
belongs_to: item
Item
# ### Columns
# Name |
# ---------------------------------- |
# **`name`** |
問題
以下のようなコードでは、ループ内でfind_byを行なっているためループ回数分クエリが実行される。取得するItemの件数が多い場合や、Item.allの数が多い場合には大量のクエリが発行されるので、処理が重くなる、という問題が起こる。
user = User.find(?)
user_items = Item.all.map do |item|
user.user_items.find_by(item: item)
end
# 以降はuser_itemsを使ってアイテム名を返す
解決
これを回避するにはループ外でまとめてUserItemを取得するようにする。
user = User.find(?)
# ループ外でUserItemを取得しておく
user_items = user.user_items.preload(:item)
Item.all.map do |item|
# ループ内ではRubyのメソッドで検索する
user_item = user_items.detect {|user_item| item.id == user_item.item_id }
user_item.name
end
これで発行クエリ数は少なくできたが、Item.all.mapループの中でuser_items.detectループを回しているのが問題。
user_items.detectループはItemの数が多くなるとループの数も増えるので、検索にかかる時間が長くなる。
以下のように、あらかじめ検索対象であるuser_itemsをhashにしておけば、user_items.detectループは不要になる。
user = User.find(?)
# { item_id => user_item }形式のHashを用意する
user_item_hash = user.user_items.includes(:item).index_by(&:item_id)
# hashから目当てのItemを探す
Item.all.map do |item|
user_item_hash[item.id].name
end
まとめ
1、ループ内でアソシエーションを参照する場合はActiveRecordのeager loadingメソッドを利用する
2、ループ内でアソシエーション以外の参照を行う場合はループ外で自前で必要なデータをまとめて取得する
3、2で取得したデータをループ内で検索に使用する場合は、探索効率を良くするためにHashにする
コメント