22
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ActiveRecordのorクエリをscope化する

Posted at

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文が出来上がってしまったこともありました。

22
9
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?