1
2

More than 3 years have passed since last update.

ActiveRecordのorをinjectするとSQLのネストが深くなる問題 (Rails 6.0まで)

Last updated at Posted at 2020-12-19

(追記)

Rails6.1からは、下に書いたクエリのネスト問題は解消されたようです。
More concise Arel Or ast and make Or visitor non recursive by kamipo · Pull Request #39051 · rails/rails


教えて&治して下さりありがとうございます!

この記事は、それ以前のRailsでの解決案として参考にしえてもらえるとです。(記事のRailsは6.0.3.4です)


身長と体重の組があるから普通体型の人を抽出して! みたいなクエリを組み立てる話です。

image.png

height_and_weight = [
  [150..159, 50..55],
  [160..169, 56..62],
  [170..179, 63..70],
  [180..189, 71..77]
]

injectorではイマイチ

なんとなく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よりも結合の優先順位が高いので、括弧は無しで大丈夫です。

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