LoginSignup
0
1

More than 5 years have passed since last update.

RoR|sumの計算がおかしくなるケース#1

Posted at

はじめに

何でもかんでも先読みすればすればいいってもんじゃないです。
Bulletを導入してダイアログが出るたびに、
 「はいはい、includesかjoinsすればいいんでしょという」
といった対応だとつらくなるケースがあったのでメモ書きです:smile:

前準備

モデル

モデルはCountryモデル、Prefectureモデル、Cityモデルの3つあります。
リレーションは下記のプログラムを参照してください:smile:

app/models/country.rb
class Country < ApplicationRecord
  has_many :prefectures
end
app/models/prefecture.rb
class Prefecture < ApplicationRecord
  has_many :cities
  belongs_to :country
end
app/models/city.rb
class City < ApplicationRecord
  belongs_to :prefecture
  has_one :country, through: :prefecture
end

検証用データ

db/seeds.rb
# 実データに基づくものではありませんのでご注意
# あくまでイメージなのでご了承ください
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モデルを参照したときですね。
「なんでそんなことするん?」というツッコミはなしだぞ:wink:

このときの結果は{1=>132, 2=>44, 3=>96}になり先程の結果とかなり違います:sweat_smile:

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"

これは計算対象のレコードがテーブル結合したときに重複してるせいですね:sweat_smile:
SQLを分解して実行してみましょう:smile:

Cityモデル - Prefectureモデルまで結合

Cityモデル - Prefectureモデルまで結合してもまだレコードは重複していませんね。よく見る外部結合の結果です:eyes:

SELECT *
FROM "cities"
LEFT OUTER JOIN "prefectures" "prefectures_cities_join" ON "prefectures_cities_join"."id" = "cities"."prefecture_id"

テーブルイメージ
スクリーンショット 2019-03-03 22.48.53.png

Prefectureモデル - Countryモデルまで結合

先程の結果にさらに国(Countryモデル)のレコードを結合していますね。
ここでもまだレコードが重複していませんね:eyes:

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"

テーブルイメージ
スクリーンショット 2019-03-03 22.49.34.png

Countryモデル - Prefectureモデルまで結合

ここからが問題です:sweat:

先の手順で結合した国(Countryモデル)のレコードから県(Prefectureモデル)を結合しています。
県(Prefectureモデル)は、いま4つありますので、いままでの結果が4つずつ増えることになります。
先程の結果をよく見ると、正しい結果に4をかけたものとおんなじですね:smile:

  • 正しい結果
    {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"

テーブルイメージ(長いので途中は略)
スクリーンショット 2019-03-03 22.51.24.png

まとめ

今回は和を求める場合でしたが、他の場合でもこういった結合をすると問題が出ると思います。
いちどになんでも先読みして済ませようとするとトラブルの元ですし、テーブルも大きくなるのであまりメリットはないかなと思います:wink:

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1