ActiveRecord の where.not とド・モルガンの法則

where.not を使っていて遭遇した問題についてrailsdmの懇親会で話していたら、@kamipo さんから「5.2では少し直っている」という情報を頂いた。

この話をしているときに思ったけど、今の where.not の問題点について知らない人も多そうなので、せっかくなのでブログにまとめてみた。

where.notの問題

where.not のドキュメントにいくつか例がある。

問題は複数の条件を引数で指定した場合。

User.where.not(name: "Jon", role: "admin")
# SELECT * FROM users WHERE name != 'Jon' AND role != 'admin'

これはド・モルガンの法則でORになりそうなのに、なっていない。

# ド・モルガンの法則
# NOT(A AND B) = NOT(A) OR NOT(B)

# つまり、これは
!(name == "Jon" && role == "admin")

# こうなる
name != "Jon" || role != "admin"

実際に遭遇したケース

ポリモーフィック関連を使っていて、 where.not と組み合わせた時に遭遇した。

# $ rails g model Picture imageable:references{polymorphic}
# $ rails g model Employee
# $ rails db:migrate:reset
# $ rails c
# Loading development environment (Rails 5.1.5)

e = Employee.create
#=> #<Employee id: 1, created_at: "2018-03-26 09:50:28", updated_at: "2018-03-26 09:50:28">
Picture.where.not(imageable: e).to_sql
#=> SELECT "pictures".* FROM "pictures" WHERE ("pictures"."imageable_type" != 'Employee') AND ("pictures"."imageable_id" != 1)

5.2.0.rc2 の挙動

5.2.0.rc2 ではポリモーフィック関連の挙動が直っている。

# $ rails g model Picture imageable:references{polymorphic}
# $ rails g model Employee
# $ rails db:migrate:reset
# $ rails c
# Loading development environment (Rails 5.2.0.rc2)

e = Employee.create
#=> #<Employee id: 1, created_at: "2018-03-26 09:56:18", updated_at: "2018-03-26 09:56:18">
Picture.where.not(imageable: e).to_sql
#=> SELECT "pictures".* FROM "pictures" WHERE NOT ("pictures"."imageable_type" = 'Employee' AND "pictures"."imageable_id" = 1)

そして、これが @kamipo さんの最高のコミット。

github.com

213796f のコミットでポリモーフィック関連の挙動が直った後、すかさずテストを追加している。*1

ポリモーフィック関連以外のケース

Issue は作られていて、議論されている。

github.com

個人的には 6.0 で複数条件のケースも直っていると嬉しい。

まとめ

where.not で ポリモーフィック関連複数の条件 を渡すコードがあったら注意しましょう。

*1:テストのある機能は基本的に維持される