LoginSignup
8
3

More than 1 year has passed since last update.

ActiveRecordの複数条件でのwhere notの挙動

Last updated at Posted at 2021-01-23

はじめに

普段、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 {太郎}

スクリーンショット 2021-01-24 1.37.55.png
取得したい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={太郎}です。
よって、山田孝之は「山田ではある」ものの「太郎ではない」ので取得することができます。

もっとスマートに書きたいんじゃ!と思っておりますので、良い方法あったら教えてくだされー!

8
3
1

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
8
3