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

# <i class="fa fa-fw fa-smile-o"></i> はじめに

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

# <i class="fa fa-fw fa-search"></i> 検証

## 前準備

以下の 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)) の場合

#### コード
```rb
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)) の場合

#### コード

```rb
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** と同義です。

ちなみに

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

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

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

#### コード

```rb
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)) の場合

#### コード

```rb
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) を含めたコードで検証してみます。

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

```rb
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 は構造的に互換性がなくてはいけない」ということらしいですね。そこで、この指摘に従って書き換えてみます。

#### コード

```rb
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: 

# <i class="fa fa-fw fa-book"></i> 参考

* [ActiveRecord::QueryMethods#or](http://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-or)
* [Rails 5: ActiveRecord OR query](http://stackoverflow.com/questions/32753168/rails-5-activerecord-or-query)