include
to_jsonには便利なオプションが沢山ある.特にinclude.多対多のデータをモデルの要素に配列として返してくれて便利.
例えば,fooとbarそれぞれのモデルが多対多の関係にあるとして
JSON.parse(Foo.queries.to_json(include: :bars))
とすれば,
[{id: 1, bars: [{id: 1}, {id: 2}]}]
ただ,一対多なら+1, 多対多のテーブルなら中間テーブルを足して+2多くクエリーが発行される.まとめて取得したいテーブルが増えると線形に増えていってしまう.
1クエリーで同じ様に返すには?
なんか他にいいメソッドないかな...と思ったがない.ので書いてみる.モデルは上と同じ.DBはMYSQL.
foo.rb
require 'hashie'
require "securerandom"
# 色々
scope hoge, -> () do
separator = SecureRandom.hex(12)
left_outer_joins(:bars)
# .queries
.select("
foo.ids,
foo.names,
GROUP_CONCAT(bars.id separator '#{separator}') AS bar_identifications,
GROUP_CONCAT(bars.name separator '#{separator}') AS bar_names
")
.group(:id)
.map {|f|
Hashie::Mash.new({
**f.attributes,
bars: [
f.bar_identifications&.split(separator) || [],
f.bar_names&.split(separator) || []
].transpose.map {|row| [:id, :name].zip(row).to_h}
})
}
end
ポイント
- 左外部結合する.
- GROUP_BY(
group(:id)
)とGROUP_CONCATで関連モデルのカラムの値を文字列に連結して返す. - separatorは値とかぶらなければよいのでランダムな文字列.
-
.<関連モデル>_ids
はRailsのメソッドして定義済みなのでidentificationsに変えている. - Hashie::Mash.newを使うことでコントローラーやビューでモデルと同じ様にドット記法が使える.
- **f.attributesで要素を展開
-
&.
と|| []
は値がnilだった場合用 -
bars: [{id: 1}, {id: 2}]
のようにするために[[id1, id2], [name1, name2]]
のような二次元配列を転置させて,それぞれの要素にカラム名を加えてhashにする.
デ/メリット
メリット
・どんなデータが返ってくるか一目でわかる
・加工しやすい
デメリット
・コードがやや長い
他に何かよい方法があったら教えて下さい.