Ruby
Rails

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

More than 1 year has 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:

参考