Help us understand the problem. What is going on with this article?

ActiveRecordのorクエリをscope化する

More than 1 year has passed since last update.

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)
}

脚注


  1. 10個orしたものを5つ連ねた結果、数MBのSQL文が出来上がってしまったこともありました。 

jkr_2255
qiitadon
Qiitadon(β)から生まれた Qiita ユーザー・コミュニティです。
https://qiitadon.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away