はじめに
Rails 5 で ActiveRecord に待望の or メソッドが追加されましたね。これで where メソッドで SQL をベタ書きすることなく OR 演算子が使えます!ただ、where と or を組み合わせた場合、実際にどのような SQL が発行されるのかが気になったので検証してみました。
検証
前準備
以下の Model と seed データを用意します。
class Anime < ApplicationRecord
has_many :characters
end
class Character < ApplicationRecord
belongs_to :anime
enum sex: { male: 1, female: 2 }
end
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 を発行することができました。