LoginSignup
2
0

More than 1 year has passed since last update.

Railsでdistinctとpluck/select連用する時ハマった件

Posted at

TL;DR

前後文にdistinctが存在する(可能性がある)場合、pluckまたはselectを使うとき、id(またはその他一意性識別子)を一緒に入れないと、場合によって見つかりにくいBugが発生します。

OK:

user.purchases.distinct.pluck(:id, :amount).map(&:last).sum
# または
user.purchases.distinct.select(:id, :amount).map(&:amount).sum

NG:

user.purchases.distinct.pluck(:amount).sum
# または
user.purchases.distinct.select(:amount).map(&:amount).sum

起因

仕事で開発を行なった通販システムで、ある日から「会員の購入金額の合計データを出力する」という要望がありました。
開発する際に、「購入金額の合計を取るには、わざわざ全件をインスタンス化しなくていいじゃない」と思い、下記のようなコードを書きました。

user.purchases.pluck(:amount).sum

開発環境で何件購入してみて、ちゃんと計算しているように見えたので、そのままPR出しました。
そして後日、営業側から、「今月システムの購入金額の統計と銀行の振込合計が合わない」との問い合わせが来ました。

分析

いろいろ調べた結果、user.purchasesのスコープの中にはdistinctが存在していて、もしその後にpluck/selectを繋いだ場合、amountに対してdistinctを行うこととなっていることがわかりました。

# 見やすいために`distinct`を外に出します
user.purchases.distinct.pluck(:amount)
=> (4.8ms)  SELECT DISTINCT "purchases"."amount" FROM "purchases" WHERE "purchases"."user_id" = $1  [["user_id", 42]]

ログが示したように、"purchases"."amount"に対してDISTINCTを行うよう、SQLが発行されました。
開発した際、たまたま違う値段の商品を購入してテストを行なったため、このBugが露呈しませんでした。

また、selectを使う時も同じです。

# 見やすいために`distinct`を外に出します
user.purchases.distinct.select(:amount)
=> Purchase Load (5.9ms)  SELECT DISTINCT "purchases"."amount" FROM "purchases" WHERE "purchases"."user_id" = $1  [["user_id", 42]]

解決方法

普段distinctを使う時、だいたいはORなどを用いた際の重複排除だと考えられます。
その際DISTINCTの対象は"purchases".*ですが、IDなど一意性の識別子がある場合、pluckselectにその識別子も一緒に入れれば、DISTINCTの誤爆を防げられます。

user.purchases.distinct.pluck(:id, :amount)
=> (4.1ms)  SELECT DISTINCT "purchases"."id", "purchases"."amount" FROM "purchases" WHERE "purchases"."user_id" = $1  [["user_id", 42]]

当然、戻り値は変わりますので、その辺は適宜に処理すれば目的達成となりますでしょう。

2
0
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
2
0