(追記)
Rails6.1からは、下に書いたクエリのネスト問題は解消されたようです。
More concise Arel Or ast and make Or visitor non recursive by kamipo · Pull Request #39051 · rails/rails
Rails 6.1ではorを重ねてもネストが深くならなくなりました🆕 https://t.co/JaaedR3ebS
— Ryuta Kamizono (@kamipo) December 20, 2020
教えて&治して下さりありがとうございます!
この記事は、それ以前のRailsでの解決案として参考にしえてもらえるとです。(記事のRailsは6.0.3.4
です)
身長と体重の組があるから普通体型の人を抽出して! みたいなクエリを組み立てる話です。
height_and_weight = [
[150..159, 50..55],
[160..169, 56..62],
[170..179, 63..70],
[180..189, 71..77]
]
inject
とor
ではイマイチ
なんとなくArrayのinject
とActiveRecordのor
メソッドで書けそうですが、
これらをそのまま使うとイマイチなことになってしまいます。
puts height_and_weight.inject(User.none) { |r, (h, w)|
r.or(User.where(heigt: h, weight: w))
}.to_sql
SELECT
`users`.*
FROM
`users`
WHERE
(
(
(
`users`.`heigt` BETWEEN 150 AND 159
AND `users`.`weight` BETWEEN 50 AND 55
OR `users`.`heigt` BETWEEN 160 AND 169
AND `users`.`weight` BETWEEN 56 AND 62
)
OR `users`.`heigt` BETWEEN 170 AND 179
AND `users`.`weight` BETWEEN 63 AND 70
)
OR `users`.`heigt` BETWEEN 180 AND 189
AND `users`.`weight` BETWEEN 71 AND 77
)
目的の絞り込み自体はできるのですが、条件が増えるほどSQLのネストが深くなってしまいます。
文字列で頑張る
解決案ひとつめは、WHERE句内を文字列で作る方法です。
sanitize_sql_array
でSQLインジェクション対策をしつつ条件を組み立てます。
w = height_and_weight.map { |(h, w)|
ActiveRecord::Base.send(:sanitize_sql_array,([
'(height BETWEEN ? AND ? AND weight BETWEEN ? AND ?)',
h.first, h.last, w.first, w.last
]))
}.join(" OR ")
puts User.where(w).to_sql
SELECT
`users`.*
FROM
`users`
WHERE
(
(
height BETWEEN 150 AND 159
AND weight BETWEEN 50 AND 55
)
OR (
height BETWEEN 160 AND 169
AND weight BETWEEN 56 AND 62
)
OR (
height BETWEEN 170 AND 179
AND weight BETWEEN 63 AND 70
)
OR (
height BETWEEN 180 AND 189
AND weight BETWEEN 71 AND 77
)
)
いいかんじになりました。
Arelで頑張る
解決案ふたつめは、Arelで頑張る方法です。
u = User.arel_table
puts User.where(
height_and_weight.map {|(h, w)|
Arel::Nodes::And.new([u[:height].between(h), u[:weight].between(w)])
}.inject { |r, w|
Arel::Nodes::Or.new(r, w)
}
).to_sql
複雑ですね...
SELECT
`users`.*
FROM
`users`
WHERE
`users`.`height` BETWEEN 150 AND 159
AND `users`.`weight` BETWEEN 50 AND 55
OR `users`.`height` BETWEEN 160 AND 169
AND `users`.`weight` BETWEEN 56 AND 62
OR `users`.`height` BETWEEN 170 AND 179
AND `users`.`weight` BETWEEN 63 AND 70
OR `users`.`height` BETWEEN 180 AND 189
AND `users`.`weight` BETWEEN 71 AND 77
出力されるSQLはシンプルになりました。
ANDのほうがORよりも結合の優先順位が高いので、括弧は無しで大丈夫です。