Rails4.2でArelを元にORクエリを作ろうとすると死ぬ問題の解決策

2014/11/12時点での話で、これからどうなるか分からないのですが、現時点のrails-4.2.0.beta4では、Arelを使ったORクエリの構築を今までと同じ感覚で実行すると`NoMethodError`が発生してしまいます。

今まではこういう感じでORクエリが書けた。

```rb
a = User.where(name: "joker1007").where_values.reduce(:and)
b = User.where(name: "Tomohiro").where_values.reduce(:and)
User.where(a.or b)
```

しかし4.2だと例外が出て死にます。
発生箇所はactiverecordの`lib/active_record/relation/query_methods.rb`です。

```rb
    def build_arel
      arel = Arel::SelectManager.new(table.engine, table)

      build_joins(arel, joins_values.flatten) unless joins_values.empty?

      collapse_wheres(arel, (where_values - [''])) #TODO: Add uniq with real value comparison / ignore uniqs that have binds

      arel.having(*having_values.uniq.reject(&:blank?)) unless having_values.empty?

      arel.take(connection.sanitize_limit(limit_value)) if limit_value
      arel.skip(offset_value.to_i) if offset_value

      arel.group(*group_values.uniq.reject(&:blank?)) unless group_values.empty?

      build_order(arel)

      build_select(arel, select_values.uniq)

      byebug

      arel.distinct(distinct_value)
      arel.from(build_from) if from_value
      arel.lock(lock_value) if lock_value

      # Reorder bind indexes if joins produced bind values
      bvs = arel.bind_values + bind_values
      reorder_bind_params(arel.ast, bvs)
      arel
    end

    def reorder_bind_params(ast, bvs)
      ast.grep(Arel::Nodes::BindParam).each_with_index do |bp, i|
        column = bvs[i].first
        bp.replace connection.substitute_at(column, i)
      end
    end

```

この`reorder_bind_params`内で`bvs[i]`がnilになって死にます。

これは何故起こるかというとrails-4.2からARのクエリを高速化のための変更が入っており、`where`にHash形式でパラメーターを渡してクエリを構築すると、内部でバインドパラメーターに変換されるようになったためです。
クエリのパラメーター部分を`?`(バインドパラメーター)に置き換えてクエリをノーマライズし、ハッシュで渡したパラメーターは別途`bind_values`として保持します。
同じ`query_methods.rb`に`build_where`というメソッドがあり、ここで`bind_values`を設定しています。
この`build_where`は`where`でクエリを追加した時に実行されるメソッドです。

```rb
    def build_where(opts, other = [])
      case opts
      when String, Array
        [@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))]
      when Hash
        opts = PredicateBuilder.resolve_column_aliases(klass, opts)

        tmp_opts, bind_values = create_binds(opts)
        self.bind_values += bind_values

        attributes = @klass.send(:expand_hash_conditions_for_aggregates, tmp_opts)
        add_relations_to_bind_values(attributes)

        PredicateBuilder.build_from_hash(klass, attributes, table)
      else
        [opts]
      end
    end

```

このため、今までの`where_values`でArelのノードを取得してwhereに渡すだけでは`bind_values`の取得ができず、クエリ構築に必要な情報が揃わないため、クエリ構築に失敗します。

じゃあどうすればいいかというと`bind_values`もちゃんと取得して引き渡します。

```rb
a_scope = User.where(name: "joker1007")
a_where = a_scope.where_values.reduce(:and)
a_bind = a_scope.bind_values

b_scope = User.where(name: "Tomohiro")
b_where = b_scope.where_values.reduce(:and)
b_bind = b_scope.bind_values

User.where(a_where.or b_where).tap {|sc| sc.bind_values = a_bind + b_bind }

# もしくは

(a_bind + b_bind).inject(User.where(a.where.or b_where)) do |sc, b|
  sc.bind(b)
end
```

こんな感じで渡せばちゃんとクエリが構築できます。
この時、`bind_values`や`bind`に渡す順番に気を付けないと意図しない場所にパラメーターが代入されてクエリが壊れるので注意しましょう。

個人的にこれはかなり面倒になっている気がします。
元々、明示的にドキュメント化されてる機能では無いので何とも言えないし、動作上不自然では無いのですが、ちょっとややこしいクエリ作る時の可読性に結構影響が出そうです。