Rails 4.2 で Arel を使って OR クエリを構築する

# <i class="fa fa-star fa-fw"></i> 概要

Rails 4.1 を 4.2 にバージョンアップしている時のことでした。

```rb
# Rails 4.1.10
scope1 = User.where(sex: 'male', age: 18)
scope2 = User.where(sex: 'female', age: 16)
# 条件ごとに括弧をつけるために Arel::FactoryMethods#grouping を使う。
condition1 = User.arel_table.grouping(scope1.where_values.reduce(:and))
condition2 = User.arel_table.grouping(scope2.where_values.reduce(:and))

User.where(condition1.or(condition2)).to_sql
#=> "SELECT `users`.* FROM `users` WHERE ((`users`.`sex` = 'male' AND `users`.`age` = 18) OR (`users`.`sex` = 'female' AND `users`.`age` = 16))"
```

上記のように [Arel::Nodes::Node#or](http://www.rubydoc.info/github/rails/arel/Arel/Nodes/Node:or) を使って OR クエリを構築している箇所があるのですが、
それが Rails のバージョンアップに伴って不正なクエリを発行するようになり、動かなくなってしまいました。

原因を調査していると @joker1007 さんの [Rails4.2でArelを元にORクエリを作ろうとすると死ぬ問題の解決策](http://qiita.com/joker1007/items/5c851526e73b3bc0273a) という記事に原因とその解決方法を発見しました。

```rb
# Rails 4.2.1
scope1 = User.where(sex: 'male', age: 18)
scope2 = User.where(sex: 'female', age: 16)
condition1 = User.arel_table.grouping(scope1.where_values.reduce(:and))
condition2 = User.arel_table.grouping(scope2.where_values.reduce(:and))

# そのままでは WHEERE 句の条件値が欠落した SQL が発行されてしまう。
User.where(condition1.or(condition2))
#=> "SELECT `users`.* FROM `users` WHERE ((`users`.`sex` =  AND `users`.`age` = ) OR (`users`.`sex` =  AND `users`.`age` = ))"

# 上記の記事を参考に bind_values を設定すると正しい SQL が発行される。
User.where(condition1.or(condition2))
  .tap { |scope| scope.bind_values = scope1.bind_values + scope2.bind_values }
  .to_sql
#=> "SELECT `users`.* FROM `users` WHERE ((`users`.`sex` = 'male' AND `users`.`age` = 18) OR (`users`.`sex` = 'female' AND `users`.`age` = 16))"
```

ただ OR クエリを構築している箇所で毎回このように記述するのは辛いので、
ActiveRecord を拡張して、例えば `where_or(*scopes)` のようなメソッドを追加できないかと思い、試してみました。

# <i class="fa fa-lightbulb-o fa-fw"></i> OR クエリ構築するマン

結果は以下のようになりました。

```rb
ActiveRecord::QueryMethods::WhereChain.include(Module.new do
  def or(*scopes)
    bind_values  = []
    where_values = []

    scopes.each do |scope|
      temp_scope = scope.is_a?(Hash) ? @scope.model.where(scope) : scope
      where_values << temp_scope.arel_table.grouping(temp_scope.arel.constraints.inject(:and))
      bind_values  += temp_scope.bind_values
    end

    @scope.where_values += [where_values.inject(:or)]
    @scope.bind_values  += bind_values
    @scope
  end
end)

scope1 = User.where(sex: 'male', age: 18)
scope2 = User.where(sex: 'female', age: 16)
User.where.or(scope1, scope2).to_sql
#=> "SELECT `users`.* FROM `users` WHERE ((`users`.`sex` = 'male' AND `users`.`age` = 18) OR (`users`.`sex` = 'female' AND `users`.`age` = 16))"

# ハッシュでもいいのよ。
User.where.or({ sex: 'male', age: 18 }, { sex: 'female', age: 16 }).to_sql
#=> "SELECT `users`.* FROM `users` WHERE ((`users`.`sex` = 'male' AND `users`.`age` = 18) OR (`users`.`sex` = 'female' AND `users`.`age` = 16))"
```

[ActiveRecord::QueryMethods::WhereChain](http://api.rubyonrails.org/classes/ActiveRecord/QueryMethods/WhereChain.html) を拡張して `where.not` のように `where.or` と書けるようにしました。

これは StackOverflow の [OR operator in WHERE clause with Arel in Rails 4.2](
http://stackoverflow.com/questions/27627390/or-operator-in-where-clause-with-arel-in-rails-4-2) という記事にあった投稿を参考にし、
さらに各条件ごとに括弧を付けて

```sql
WHERE ((A1 AND A2) OR (B1 AND B2) OR (C1 AND C2)) AND the other conditions
```

のような WHERE 句の SQL を発行できるように手を加えました。

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

* :kobito: Qiita
    * [Rails4.2でArelを元にORクエリを作ろうとすると死ぬ問題の解決策](http://qiita.com/joker1007/items/5c851526e73b3bc0273a)
* <i class="fa fa-stack-overflow fa-fw"></i> StackOverflow
    * [OR operator in WHERE clause with Arel in Rails 4.2](
http://stackoverflow.com/questions/27627390/or-operator-in-where-clause-with-arel-in-rails-4-2)
    * [Grouping ands and ors in AREL](http://stackoverflow.com/questions/8083292/grouping-ands-and-ors-in-arel)
* <i class="fa fa-github fa-fw"></i> GitHub
    * [ActiveRecord::QueryMethods::WhereChain のソース](https://github.com/rails/rails/blob/v4.2.1/activerecord/lib/active_record/relation/query_methods.rb)