🛤️

ActiveRecord での AND OR 条件と merge との違い

2024/05/06に公開

要点

  • and() or() の優先度は SQL の世界とは異なり組み立て順序で決まる
  • or() の連続は括弧で囲まれ、and と混じる際にも先に囲まれる
  • and() と merge() は異なる

下準備

開く
require "active_record"
ActiveRecord::VERSION::STRING  # => "7.1.3.2"
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Migration.verbose = false
ActiveRecord::Schema.define do
  create_table :users
end
class User < ActiveRecord::Base; end
def _(s) = s.to_sql.remove(/"/, "users.", /SELECT.*?WHERE\s+/, / = 1/).swapcase
A = User.where(a: 1)
B = User.where(b: 1)
C = User.where(c: 1)
D = User.where(d: 1)
E = User.where(e: 1)
X = User.where(id: [5, 6])
Y = User.where(id: [6, 7])

まとめ

_ A.and(B).and(C)        # => "A and B and C"
_ A.or(B).or(C)          # => "(A or B or C)"
_ A.and(B).or(C)         # => "(A and B or C)"
_ C.or(A).and(B)         # => "(C or A) and B"
_ C.or(A.and(B))         # => "(C or A and B)"
_ A.or(B).and(C.or(D))   # => "(A or B) and (C or D)"
_ A.and(B).or(C.and(D))  # => "(A and B or C and D)"
_ X.and(Y)               # => "ID in (5, 6) and ID in (6, 7)"
_ X.merge(Y)             # => "ID in (6, 7)"

検証

and で繋げる

_ A.and(B).and(C)  # => "A and B and C"

括弧はつかない。

or で繋げる

_ A.or(B).or(C)  # => "(A or B or C)"

外側に括弧がつく。

(A かつ B) または C

_ A.and(B).or(C)  # => "(A and B or C)"

and の優先度が高いため or と混っても頑なに括弧が省略されている。

[注意] C または (A かつ B)

_ C.or(A.and(B))  # => "(C or A and B)"

と書くのが正しい。これを前のように、

_ C.or(A).and(C)  # => "(C or A) and C"

と書いてしまうと条件が変わってしまう。つまり組み立て順序が重要になる。

(A または B) かつ (C または D)

_ A.or(B).and(C.or(D))  # => "(A or B) and (C or D)"

先に出てくる A.or(B) を括弧で囲む方法で悩みそうだが or の連続は自動で囲まれる。

(A かつ B) または (C かつ D)

_ A.and(B).or(C.and(D))  # => "(A and B or C and D)"

and の優先度が高いため括弧が省略されている。

and と merge を使い分ける

まず and と merge は異なる。and は and だが merge は Hash#merge のような挙動になる。この違いが影響するのが、たとえば同じカラムに対する二つのフィルタースコープを

_ X  # => "ID in (5, 6)"
_ Y  # => "ID in (6, 7)"

両方適用したいときで、

_ X.and(Y)  # => "ID in (5, 6) and ID in (6, 7)"

とすれば意図した通りだが、

_ X.merge(Y)  # => "ID in (6, 7)"

としてしまうと前のスコープが消されて条件が変わってしまう。and の意味で merge を紹介している記事も見かけるが、それはたまたまどちらを使っても結果が同じだったというだけであって正しく使い分けないと事故る。

だからといって and を積極的に使った方がよいというわけではなく、多くの場合は merge の方が適している。

無条件に and するわけでもない

上の続きで、それなら X.and(X)ID in (5, 6) and ID in (5, 6) になりそうだが、

_ X.and(X)  # => "ID in (5, 6)"

そこはさすがに同じ条件を繋いでも意味がないので省いてくれている。

Discussion