はじめに
何でもかんでも先読みすればすればいいってもんじゃないです。
Bulletを導入してダイアログが出るたびに、
「はいはい、includesかjoinsすればいいんでしょという」
といった対応だとつらくなるケースがあったのでメモ書きです
前準備
モデル
モデルはCountryモデル、Prefectureモデル、Cityモデルの3つあります。
リレーションは下記のプログラムを参照してください
class Country < ApplicationRecord
has_many :prefectures
end
class Prefecture < ApplicationRecord
has_many :cities
belongs_to :country
end
class City < ApplicationRecord
belongs_to :prefecture
has_one :country, through: :prefecture
end
検証用データ
# 実データに基づくものではありませんのでご注意
# あくまでイメージなのでご了承ください
jpn = Country.create(name: 'Japan')
hyogo = jpn.prefectures.create!(name: 'Hyogo')
osaka = jpn.prefectures.create!(name: 'Osaka')
shimane = jpn.prefectures.create!(name: 'Shimane')
nagasaki = jpn.prefectures.create!(name: 'Nagasaki')
hyogo.cities.create!(name: 'Kobe', population: 22)
hyogo.cities.create!(name: 'Akashi', population: 11)
osaka.cities.create!(name: 'Ikeda', population: 3)
osaka.cities.create!(name: 'Takatsuki', population: 1)
osaka.cities.create!(name: 'Kadoma', population: 7)
shimane.cities.create!(name: 'Matsue', population: 9)
shimane.cities.create!(name: 'Izumo', population: 15)
__END__
各県の合計値は
- Hyogo: 33
- Osaka: 11
- Shimane: 24
- Nagasaki: 未登録
フェッチ
つらくない場合
Cityモデルから順に結合している場合は問題ありません。
結果として、{1=>33, 2=>11, 3=>24}
を得ることが出来ています。
*Prefectureモデルは1から順にHyogo、Osaka、Shimaneが登録されています。
City.includes(prefecture: :country).
group(:prefecture_id).
sum(:population)
SELECT SUM("cities"."population") AS sum_population,
"cities"."prefecture_id" AS cities_prefecture_id
FROM "cities"
LEFT OUTER JOIN "prefectures" ON "prefectures"."id" = "cities"."prefecture_id"
LEFT OUTER JOIN "countries" ON "countries"."id" = "prefectures"."country_id"
GROUP BY "cities"."prefecture_id"
つらい場合
つらくなる場合はCityモデルからCountryモデルを参照して、そこからPrefectureモデルを参照したときですね。
「なんでそんなことするん?」というツッコミはなしだぞ
このときの結果は{1=>132, 2=>44, 3=>96}
になり先程の結果とかなり違います
City.includes({prefecture: :country}, {country: :prefectures}).
group(:prefecture_id).
sum(:population)
SELECT SUM("cities"."population") AS sum_population,
"cities"."prefecture_id" AS cities_prefecture_id
FROM "cities"
LEFT OUTER JOIN "prefectures" ON "prefectures"."id" = "cities"."prefecture_id"
LEFT OUTER JOIN "countries" ON "countries"."id" = "prefectures"."country_id"
LEFT OUTER JOIN "prefectures" "prefectures_cities_join" ON "prefectures_cities_join"."id" = "cities"."prefecture_id"
LEFT OUTER JOIN "countries" "countries_cities" ON "countries_cities"."id" = "prefectures_cities_join"."country_id"
LEFT OUTER JOIN "prefectures" "prefectures_countries" ON "prefectures_countries"."country_id" = "countries_cities"."id"
GROUP BY "cities"."prefecture_id"
これは計算対象のレコードがテーブル結合したときに重複してるせいですね
SQLを分解して実行してみましょう
Cityモデル - Prefectureモデルまで結合
Cityモデル - Prefectureモデルまで結合してもまだレコードは重複していませんね。よく見る外部結合の結果です
SELECT *
FROM "cities"
LEFT OUTER JOIN "prefectures" "prefectures_cities_join" ON "prefectures_cities_join"."id" = "cities"."prefecture_id"
Prefectureモデル - Countryモデルまで結合
先程の結果にさらに国(Countryモデル)のレコードを結合していますね。
ここでもまだレコードが重複していませんね
SELECT *
FROM "cities"
LEFT OUTER JOIN "prefectures" "prefectures_cities_join" ON "prefectures_cities_join"."id" = "cities"."prefecture_id"
LEFT OUTER JOIN "countries" "countries_cities" ON "countries_cities"."id" = "prefectures_cities_join"."country_id"
Countryモデル - Prefectureモデルまで結合
ここからが問題です
先の手順で結合した国(Countryモデル)のレコードから県(Prefectureモデル)を結合しています。
県(Prefectureモデル)は、いま4つありますので、いままでの結果が4つずつ増えることになります。
先程の結果をよく見ると、正しい結果に4をかけたものとおんなじですね
- 正しい結果
{1=>33, 2=>11, 3=>24}
- 誤った結果
{1=>(33 * 4), 2=>(11 * 4), 3=>(24 * 4)}
SELECT *
FROM "cities"
LEFT OUTER JOIN "prefectures" "prefectures_cities_join" ON "prefectures_cities_join"."id" = "cities"."prefecture_id"
LEFT OUTER JOIN "countries" "countries_cities" ON "countries_cities"."id" = "prefectures_cities_join"."country_id"
LEFT OUTER JOIN "prefectures" "prefectures_countries" ON "prefectures_countries"."country_id" = "countries_cities"."id"
まとめ
今回は和を求める場合でしたが、他の場合でもこういった結合をすると問題が出ると思います。
いちどになんでも先読みして済ませようとするとトラブルの元ですし、テーブルも大きくなるのであまりメリットはないかなと思います