#はじめに
普段、RailsはActiveRecord使っているんですが、条件を複数書いたときに、
where.not()の挙動が直感と反しており、なかなかバグに気づかなかったので、共有します。
#環境
Ruby : 2.7.1
Rails : 6.0.2
Posgresql : 13.1
#where.notの挙動
まず、where.not()の単数条件で名字が山田以外のuserを取得したい時を考えてみます。
発行されるSQLは以下のようになります。
User.where.not(last_name: '山田')
=> SELECT "users".* FROM "users" WHERE "users"."last_name" != $1
[["last_name", "山田"]]
これは期待通りにlast name
が山田でないuserを取得してくれます。
では次に**「名字が山田」かつ「名前が太郎」(山田 太郎)以外**のuserを取得するとしましょう。
僕の直感では以下のような記述で取得できると思ってましたが...
User.where.not(last_name: '山田', first_name: '太郎')
=> SELECT "users".* FROM "users"
WHERE "users"."last_name" != $1 AND "users"."first_name" != $2
[["last_name", "山田"], ["first_name", "太郎"]]
上記、SQLを見てみると、「山田ではない」かつ「太郎ではない」というSQLが発行されています。でも欲しかったのは「名字が山田」かつ「名前が太郎」
ではない userです。集合記号を使うと以下のようにかけます。
\overline{ 山田 \cap 太郎} \neq \overline {山田} \cap \overline {太郎}
取得したいuserは赤と青の共通部分(山田太郎)以外の箇所です。
しかし、発行されるSQLで取得しているのは、赤と青以外、すなわち白の部分のuserです。
悲しいかな、「山田孝之」や「麻生太郎」は取得してくれません。
#解決
では、山田孝之や麻生太郎を取得するにはどうすれば良いかというと、以下のようになります。
User.where.not(last_name: '山田').or(User.where.not(first_name:'太郎'))
=> SELECT "users".* FROM "users"
WHERE ("users"."last_name" != $1 OR "users"."first_name" != $2)
[["name", "山田"], ["public_id", "太郎"]]
上記は、書き方的には**「山田ではない」または「太郎ではない」**になります。これはドモルガンの法則を用いて
\overline{A \cap B} = \overline A \cup \overline B
となることを使っています。A={山田},B={太郎}です。
よって、山田孝之は「山田ではある」ものの「太郎ではない」ので取得することができます。
もっとスマートに書きたいんじゃ!と思っておりますので、良い方法あったら教えてくだされー!