お久しぶりです。寺岡です。
自作のgemライブラリactiverecord-blockwhereを作ってみたのでご紹介します。
ライブラリの紹介
activerecord-blockwhereは、ActiveRecordに検索条件の組立を行うDSLを提供します。
DSLはwhereに与えたブロック内で記述でき、Arelの条件を短いコードで記述できます。
ActiveRecord3から導入されたArelはよく出来たライブラリで、非常に強力なクエリ構築機能があります。
しかし、個々のモデルクラスから利用する場合、後述するarel_tableや論理演算の関係でインターフェースが煩雑になるため敬遠しがちでした。
もっと簡潔に使えればいいのに、もったいないな~。と常々思っていたのです。
そんな時、Ruby2.0のRefinementsを利用した activerecord-refinements に出会いました。
(Refinementsの仕様変更により、惜しくも正式版では使えなくなってしまいましたが…)
このライブラリのおかげで、「whereのブロック引数は空いてるんだ!」という事実に気付き、勢いに任せて作ったのがactiverecord-blockwhereです。
発想的にありがちなので、既出のライブラリと被ってないかちょっと不安です。
ActiveRecordで複雑な条件を指定したい!
Hashによる検索条件の指定
ActiveRecordのwhereは、引数にHashを与えて条件を指定することができます。
1 2 |
> Person.where(id: 1).to_sql => SELECT "people".* FROM "people" WHERE "people"."id" = 1 |
値に配列を指定することにより、INで検索することもできます。
1 2 |
> Person.where(id: [1,2,3]).to_sql => SELECT "people".* FROM "people" WHERE "people"."id" IN (1, 2, 3) |
しかし、NOTやOR、LIKEなどの検索を行おうとした場合、Hashで与える条件では実現出来ません。
そんな場合、whereにSQL構文とbindパラメータを与えることで解決できます。
ORを含む条件 SQL版
1 2 |
> Person.where('id NOT IN (?) OR name LIKE ?', [1,2,3], '%taro%').to_sql => SELECT "people".* FROM "people" WHERE (id NOT IN (1,2,3) OR name LIKE '%taro%') |
解決はしましたが、この記述方法はSQLの一部がロジックに現れてしまうのであまり綺麗なやり方とは言えません。
検索方法によってインターフェースを変える(HashとSQLを使い分ける)という方法も感心しません。
今までHashで与えていたパラメータを、NOT条件を一つ追加するためにSQLとbindパラメータに書き直す…なんて作業はやりたくないですよね。
複雑なクエリはArelに任せろ?
ORを含む条件 Arel版
複雑な条件を指定したい。でもSQLとbindパラメータは使いたくない。
そんな時にはActiveRecord(3以降)においてSQL構築を担うライブラリ、Arelを利用します。
先ほどの条件もArelを使えばこんなに風にシンプル――
1 2 3 4 |
> Person.where(Person.arel_table[:id].not_in([1,2,3]). or(Person.arel_table[:name].matches('%taro%'))).to_sql => SELECT "people".* FROM "people" WHERE (("people"."id" NOT IN (1, 2, 3) OR "people"."name" LIKE '%taro%')) |
…に書くことはできませんでした。
ArelのAttributeオブジェクトを取得するにはarel_tableを経由する必要があるため、どうしても冗長なコードになってしまいます。
また、orやandなどの論理演算を使うとメソッド呼び出しのネストが深くなってしまうのも可読性を下げる要因ですね。
activerecord-blockwhereを使う
ORを含む条件 blockwhere版
activerecord-blockwhereを導入すると、先ほどの条件を以下のように記述出来ます。
1 2 3 |
> Person.where{ id.not_in([1,2,3]) | name.matches('%taro%') }.to_sql => SELECT "people".* FROM "people" WHERE (("people"."id" NOT IN (1, 2, 3) OR "people"."name" LIKE '%taro%')) |
かなり直感的になった気がしませんか?
ブロック内では Person.arel_table[:id] の代わりに id でArelのAttributeを取得できます。
述語(条件演算子)として利用できるメソッドはArelのものなので、以下コードで確認することが出来ます。
Arel::Predicationsで定義されているメソッド一覧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
> puts Arel::Predications.instance_methods(false) not_eq not_eq_any not_eq_all eq eq_any eq_all in in_any in_all not_in not_in_any not_in_all matches matches_any matches_all does_not_match does_not_match_any does_not_match_all gteq gteq_any gteq_all gt gt_any gt_all lt lt_any lt_all lteq lteq_any lteq_all |
※参考: Arel::Predications
述語サンプル
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# eq > Person.where{ name.eq('taro') }.to_sql => SELECT "people".* FROM "people" WHERE "people"."name" = 'taro' # not_eq > Person.where{ id.not_eq(1) }.to_sql => SELECT "people".* FROM "people" WHERE ("people"."id" != 1) # gt > Person.where{ id.gt(100) }.to_sql => SELECT "people".* FROM "people" WHERE ("people"."id" > 100) # matches_any > Person.where{ name.matches_any(['%taro%', '%hanako%']) }.to_sql => SELECT "people".* FROM "people" WHERE (("people"."name" LIKE '%taro%' OR "people"."name" LIKE '%hanako%')) |
また、条件の結合などを簡潔に記述するため、Arelを拡張して「and or not」に対応する演算子「& | !」を定義しています。
論理演算子サンプル
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# and > Person.where{ id.gt(50) & id.lt(100) }.to_sql => SELECT "people".* FROM "people" WHERE ("people"."id" > 50 AND "people"."id" < 100) # or > Person.where{ name.eq('suzuki taro') | name.matches('%ichiro%') }.to_sql => SELECT "people".* FROM "people" WHERE (("people"."name" = 'suzuki taro' OR "people"."name" LIKE '%ichiro%')) # not > Person.where{ !(id.eq(10) & name.matches('jiro')) }.to_sql => SELECT "people".* FROM "people" WHERE (NOT ("people"."id" = 10 AND "people"."name" LIKE 'jiro')) |
コントローラから直接使うこともできますが、複雑な条件をscopeやクラスメソッドにまとめる際などにも有用です。
よかったら一度お試しください!