Rails 5では、ActiveRecordにor
クエリを立てる機能が加わったのはいいのですが、うまく使わないと変なハマり方をします。
#or
の機能
すでに詳しい紹介があるのでそちらに譲りますが、ざっと使い方を書くと、リレーション.or(別なリレーション)
とすることで、両者のORを取る、という流れです。
スコープにしてのハマリゴト
サンプルテーブル
今回は、以下のようなテーブルでSQLを組み立ててみます。
create_table :users do |t|
t.string :name
t.string :nickname
t.string :telephone
t.string :mobile_telephone
end
シンプルに立ててみると
では、「name
もしくはnickname
が引数に一致する」ようなスコープを作ってみましょう。とりあえずは、これで動きます。
scope :simple_name, ->(name) { where(name: name).or(where(nickname: name)) }
ただ、これには1つ問題があって、where
がそれまでに付けた条件すべてを拾う形になるので、User.別なscope.simple_name('hoge')
のようにすると、User.別なscope.where(name: name).or(User.別なscope.where(nickname: name))
のように、条件が増えてしまいます。このスコープを複数回使えば、倍々ゲームでSQLが伸びていくという、悲惨な事態となります1。これでは汎用のスコープとしては適当ではありません。
回避策
では、どうすればいいのでしょうか。いくつか方法を考えてみました。
ActiveRecordの#or
を使わない
少し負けたような感じもありますが、文字列やArel、baby_squeelなど別な手段でORクエリを組み立てれば、この問題の影響は出ません。
merge
を使う
unscoped
などを使って、正確に「追加する分だけ」の条件を組み立てて、それを元のリレーションにmerge
することで、#or
だけでも今までの条件と完全分離して作ることができます。
scope :merge_name, lambda { |name|
rel = unscoped.where(name: name).or(unscoped.where(nickname: name))
merge(rel)
}
脚注
-
10個orしたものを5つ連ねた結果、数MBのSQL文が出来上がってしまったこともありました。 ↩