2014/11/12時点での話で、これからどうなるか分からないのですが、現時点のrails-4.2.0.beta4では、Arelを使ったORクエリの構築を今までと同じ感覚で実行するとNoMethodError
が発生してしまいます。
今まではこういう感じでORクエリが書けた。
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
です。
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
でクエリを追加した時に実行されるメソッドです。
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
もちゃんと取得して引き渡します。
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
に渡す順番に気を付けないと意図しない場所にパラメーターが代入されてクエリが壊れるので注意しましょう。
個人的にこれはかなり面倒になっている気がします。
元々、明示的にドキュメント化されてる機能では無いので何とも言えないし、動作上不自然では無いのですが、ちょっとややこしいクエリ作る時の可読性に結構影響が出そうです。