ActiveRecordでOR文を作るときのエラー対処
少し複雑なSQLのOR文をActiveRecordで作るときにはすこし億劫になりますね。
ArgumentError: Relation passed to #or must be structurally compatible. Incompatible values: [:joins, :references]
とエラーが出てしまったのでいろいろ調べてみました。
今回のエラーはRails5.2の以下のコードで ArgumentError が発生しています。
rails/query_methods.rb at c81a7fcf76663e6d189792d6eed57b1162199635 · rails/rails · GitHub
structurally_incompatible_values_for_or が何なのかと見てみると
rails/query_methods.rb at c81a7fcf76663e6d189792d6eed57b1162199635 · rails/rails · GitHub
STRUCTURAL_OR_METHODS = Relation::VALUE_METHODS - [:extending, :where, :having, :unscope, :references] def structurally_incompatible_values_for_or(other) STRUCTURAL_OR_METHODS.reject do |method| get_value(method) == other.get_value(method) end end
Relation::VALUE_METHODS が 以下で登録されているのですが、
rails/relation.rb at ac1efe7a947ba04b276a7109f1a86e559a6ab683 · rails/rails · GitHub
[:includes, :eager_load, :preload, :select, :group, :order, :joins, :left_joins, :left_outer_joins, :references, :extending, :unscope, :limit, :offset, :lock, :readonly, :reordering, :reverse_order, :distinct, :create_with, :where, :having, :from]
ここから [:extending, :where, :having, :unscope, :references] を省いた項目が一致してることをチェックしてるので
[:includes, :eager_load, :preload, :select, :group, :order, :joins, :left_joins, :left_outer_joins, :limit, :offset, :lock, :readonly, :reordering, :reverse_order, :distinct, :create_with, :from]
の項目が一致してないと ArgumentError になるということですね。
これを個別に対応していくのは大変なのですね。。
これらの項目を設定するメソッドはいろいろ用意されてはいますが、
ActiveRecord::QueryMethods#methods: extending_values group_values includes_values= left_joins_values left_outer_joins_values= lock_value offset_value= preload_values readonly_value= reordering_value reverse_order_value= set_value eager_load_values extending_values= group_values= joins_values left_joins_values= limit_value lock_value= order_values preload_values= references_values reordering_value= select_values unscope_values eager_load_values= get_value includes_values joins_values= left_outer_joins_values limit_value= offset_value order_values= readonly_value references_values= reverse_order_value select_values= unscope_values=
a の オブジェクトから b のオブジェクトに移すみたいなことは項目数から考えても面倒ですね。
b.joins(a.joins_values)
項目を並べてget_value と set_value で移すこともできますが、、、
[:includes, :eager_load, :preload, :select, :group, :order, :joins, :left_joins, :left_outer_joins, :limit, :offset, :lock, :readonly, :reordering, :reverse_order, :distinct, :create_with, :from].each do |method| b.set_value(a.get_value(method)) end
流石に面倒そうです。
User. joins(:accounts). group(:id). having('count(accounts.id) > 2'). whrere(created_at: Time.zone.today.all_day)
このSQLにORを付けたいのですが、
当日と5日前に追加されたユーザを調べたいとします。
today = Time.zone.today relation = User. joins(:accounts). group(:id). having('count(accounts.id) > 2') relation.where(created_at: today.all_day).or(scope.whrere(created_at: today.days_ago(5).all_day))
このようにローカル変数にor前のRelationを渡してしまってwhereをつくればいいということですね。 少し面倒ですが、これで大丈夫そうです。
スコープを使ったケースを見てみます。
scope :hogehoge, -> { where(created_at: Time.zone.today.all_day) }
このようなscopeがあるとします。まだ条件は当日のみです。
User. joins(:accounts). references(:accounts). having('count(accounts.id) > 2'). group(:id). extending(Pagination). unscoped. hogehoge
先程の処理にhogehogeを追加してみました。 hogehogeのなかでOR文を作ってみます。
scope :hogehoge, -> { today = Time.zone.today where(created_at: today.all_day).or(where(created_at: today.days_ago(5).all_day)) }
ここではローカル変数なしに設定できました。
スコープではない場合は一旦変数に入れないといけないというのは不便ではあります。
これ自体が面倒な場合は String で直接SQLを記載するのがいいかもしれないです。
今回のケースはDateTime型で日付に対する条件になるので BETWEENが使われます。
day1 = today.all_day day2 = today.days_ago(5).all_day where('(users.created_at BETWEEN ? AND ?) OR (users.created_at BETWEEN ? AND ?)', day1.first, day1.last, day2.first, day2.last)
このような感じなります。
? に 設定する値がnilだった場合はこれだと面倒になったりするので
ケースによって使い方を選択してもいい気がします。
以下、参考にしたサイトです。