Edited at

Rails 5 の or を色々試してみた

More than 3 years have passed since last update.


はじめに

Rails 5 で ActiveRecord に待望の or メソッドが追加されましたね。これで where メソッドで SQL をベタ書きすることなく OR 演算子が使えます!ただ、where と or を組み合わせた場合、実際にどのような SQL が発行されるのかが気になったので検証してみました。


検証


前準備

以下の Model と seed データを用意します。


app/models/anime.rb

class Anime < ApplicationRecord

has_many :characters
end


app/models/character.rb

class Character < ApplicationRecord

belongs_to :anime

enum sex: { male: 1, female: 2 }
end



db/seeds.rb

Anime.create!(title: 'STEINS;GATE', year: 2011).tap do |anime|

anime.characters.create!(name: '岡部 倫太郎', sex: :male, age: 18)
anime.characters.create!(name: '椎名 まゆり', sex: :female, age: 16)
end

Anime.create!(title: '刀語', year: 2010).tap do |anime|
anime.characters.create!(name: '鑢 七花', sex: :male, age: 24)
anime.characters.create!(name: 'とがめ', sex: :female)
end



1. Model.where(A).or(Model.where(B)) の場合


コード

Character

.where(name: '岡部 倫太郎')
.or(Character.where(sex: :female))


SQL

SELECT "characters".*

FROM "characters"
WHERE ("characters"."name" = '岡部 倫太郎' OR "characters"."sex" = 2)

+----+-------------+--------+-----+
| id | name | sex | age |
+----+-------------+--------+-----+
| 1 | 岡部 倫太郎 | male | 18 |
| 2 | 椎名 まゆり | female | 16 |
| 4 | とがめ | female | |
+----+-------------+--------+-----+
3 rows in set


結果

A OR B となりました。


2. Model.where(A, B).or(Model.where(C)) の場合


コード

Character

.where(name: '岡部 倫太郎', sex: :male)
.or(Character.where(age: nil))


SQL

SELECT "characters".*

FROM "characters"
WHERE ("characters"."name" = '岡部 倫太郎' AND "characters"."sex" = 1 OR "characters"."age" IS NULL)

+----+-------------+--------+-----+
| id | name | sex | age |
+----+-------------+--------+-----+
| 1 | 岡部 倫太郎 | male | 18 |
| 4 | とがめ | female | |
+----+-------------+--------+-----+
2 rows in set


結果

A AND B OR C となりました。OR より AND の方が優先して評価されるため、これは (A AND B) OR C と同義です。

ちなみに

Character

.where(name: '岡部 倫太郎').where(sex: :male)
.or(Character.where(age: nil))

と書いても発行される SQL は同じです。


3. Model.where(A).or(Model.where(B, C)) の場合


コード

Character

.where(name: '岡部 倫太郎')
.or(Character.where(sex: :male, age: nil))


SQL

SELECT "characters".*

FROM "characters"
WHERE ("characters"."name" = '岡部 倫太郎' OR "characters"."sex" = 1 AND "characters"."age" IS NULL)

+----+-------------+------+-----+
| id | name | sex | age |
+----+-------------+------+-----+
| 1 | 岡部 倫太郎 | male | 18 |
+----+-------------+------+-----+
1 row in set


結果

A OR B AND C となりました。これは A OR (B AND C) と同義ですね。


4. Model.where(A).or(Model.where(B)).or(Model.where(C)) の場合


コード

Character

.where(name: '岡部 倫太郎')
.or(Character.where(sex: :male))
.or(Character.where(age: nil))


SQL

SELECT "characters".*

FROM "characters"
WHERE (("characters"."name" = '岡部 倫太郎' OR "characters"."sex" = 1) OR "characters"."age" IS NULL)

+----+-------------+--------+-----+
| id | name | sex | age |
+----+-------------+--------+-----+
| 1 | 岡部 倫太郎 | male | 18 |
| 3 | 鑢 七花 | male | 24 |
| 4 | とがめ | female | |
+----+-------------+--------+-----+
3 rows in set


結果

(A OR B) OR C となりました。これは A OR B OR C と同義ですね。


5. joins を含めた場合

最後に joins (INNER JOIN) を含めたコードで検証してみます。

まず以下のコードを実行します。

Character

.joins(:anime)
.merge(Anime.where(title: '刀語'))
.or(Character.where(sex: :male))

するとエラーとなってしまいます。

ArgumentError: Relation passed to #or must be structurally compatible. Incompatible values: [:joins]

「or に渡される Relation は構造的に互換性がなくてはいけない」ということらしいですね。そこで、この指摘に従って書き換えてみます。


コード

relation = Character.joins(:anime)

relation
.merge(Anime.where(title: '刀語'))
.or(relation.where(sex: :male))


SQL

SELECT "characters".*

FROM "characters"
INNER JOIN "animes" ON "animes"."id" = "characters"."anime_id"
WHERE ("animes"."title" = '刀語' OR "characters"."sex" = 1)

+----+-------------+--------+-----+
| id | name | sex | age |
+----+-------------+--------+-----+
| 1 | 岡部 倫太郎 | male | 18 |
| 3 | 鑢 七花 | male | 24 |
| 4 | とがめ | female | |
+----+-------------+--------+-----+
3 rows in set


結果

joins と merge を使ったコードでも、or を使って想定通りの SQL を発行することができました。 :blush:


参考