Ransack使わずともActiveRecordのORMでも結構検索できるよね
概要
Ransackのレールにのっかると、高速でプロトタイプが作る事ができる点でとてもすばらしいgemです。
特にviewのフォームとcontrollerを組み合わせるときは、最高。一貫したコードでかくことができるので、コードの省力化に大いに貢献してくれます。僕もとてもお世話になっています
また、viewと連携しない、ビジネスロジックのところでも有用です。とくに文字列のlike検索は *_cont
などを使うと事でコードがすっきりさせられます。
ただ、Ransackは関連レコードをleft outer joinで結合するのでこまることがあります。メモリの消費量が大きく、かつ、結合されたテーブルが巨大で制御しづらいときがあります。
Ransackで書かなくてもいいところは、ActiveRecordで書いておけばこの懸念を最小限におさえることができます。
特に、「数値と日付」の範囲や大小検索は、書き味と吐き出されるSQLのすっきりさからActiveRecordで書くほうが幸せになれると個人的には思います。
本記事では、ransackに用意されているDSLのうち、僕がよく使うActiveRecordでかけるものを紹介します。
サンプルコード
下記をcloneし、Set upに従って模擬データを作り、 rails c
で試せます。
Database
dbdoc/README.md
をご覧ください。
tblsをつかっています。今回の主題ではないけど、これとてもよいです。テーブルのスキーマを共有するのがとても便利。強くおすすめです。
設定ファイルは.tbls.yml
です。railsの規約どおりであれば関連も適当に推測してくれます。
Set up
模擬データ作成できます。
rails db:seed
比較
RansackのDSLをActiveRecordで書きます。 Ransack→ActiveRecordの順です。
紹介するDSL
*_eq
*_not_eq
*_in
*_lt
*_lteq
*_gt
*_gteq
*_null
- おまけ1
*_gteq
and*_lteq
(BETWEEN
) - おまけ2 enumフィールドの検索
*_eq
等しいレコードを検索します。一番オーソドックスなものです。
- Ransack
User.ransack(name_eq: 'Yajirobe').result
#=> User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."name" = 'Yajirobe'
- ActiveRecord ORM
User.where(name: 'Yajirobe')
#=> User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."name" = 'Yajirobe'
INNER JOINをつかうならActiveRecord ORM一択。
User.joins(blogs: :comments).merge(Comment.where(document: 'abc'))
#=> User Load (0.3ms) SELECT "users".* FROM "users" INNER JOIN "blogs" ON "blogs"."user_id" = "users"."id" INNER JOIN "comments" ON "comments"."blog_id" = "blogs"."id" WHERE "comments"."document" = ? [["document", "abc"]]
Relationあり
Ransackを使ううま味はrelationがあるときですね。 Ransackほど簡単にかけないけど、ActiveRecordでleft_joins
と merge
をつかうとかけます。
- Ransack
User.ransack(blogs_comments_document_eq: 'abc').result
#=> User Load (0.2ms) SELECT "users".* FROM "users" LEFT OUTER JOIN "blogs" ON "blogs"."user_id" = "users"."id" LEFT OUTER JOIN "comments" ON "comments"."blog_id" = "blogs"."id" WHERE "comments"."document" = 'abc'
- ActiveRecord
User.left_joins(blogs: :comments).merge(Comment.where(document: 'abc'))
#=> User Load (0.3ms) SELECT "users".* FROM "users" LEFT OUTER JOIN "blogs" ON "blogs"."user_id" = "users"."id" LEFT OUTER JOIN "comments" ON "comments"."blog_id" = "blogs"."id" WHERE "comments"."document" = ? [["document", "abc"]]
*_not_eq
where.not
をつかうとかけます。
- ransack
User.ransack(name_not_eq: 'Yajirobe').result
#=> User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."name" != 'Yajirobe'
- ActiveRecord ORM
User.where.not(name: 'Yajirobe')
#=> User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."name" != ? [["name", "Yajirobe"]]
*_in
配列を直接渡すと自動的に IN句になります。
User.ransack(name_in: ['Yajirobe', 'Raditz']).result
#=> User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."name" IN ('Yajirobe', 'Raditz')
User.where(name: ['Yajirobe', 'Raditz'])
#=>User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."name" IN (?, ?) [["name", "Yajirobe"], ["name", "Raditz"]]
*_lt
Range オブジェクトを渡します。
Integer
User.ransack(age_lt: 20).result
#=> User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."age" < 20
User.where(age: ...20)
#=> User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."age" < ? [["age", 20]]
Date
- ransack
Blog.ransack(published_dt_lt: Date.new(2021, 1, 1)).result
#=> Blog Load (0.2ms) SELECT "blogs".* FROM "blogs" WHERE "blogs"."published_dt" < '2021-01-01'
- ActiveRecord
Blog.where(published_dt: ...Date.new(2021, 1, 1))
#=> Blog Load (0.1ms) SELECT "blogs".* FROM "blogs" WHERE "blogs"."published_dt" < ? [["published_dt", "2021-01-01"]]
*_lteq
Range オブジェクトを渡します。
Integer
User.ransack(age_lteq: 20).result
#=> User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."age" <= 20
User.where(age: ..20)
#=> User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."age" <= ? [["age", 20]]
Date
- ransack
Blog.ransack(published_dt_lteq: Date.new(2021, 1, 1)).result
#=> Blog Load (0.2ms) SELECT "blogs".* FROM "blogs" WHERE "blogs"."published_dt" <= '2021-01-01'
- ActiveRecord
Blog.where(published_dt: ..Date.new(2021, 1, 1))
#=> Blog Load (0.2ms) SELECT "blogs".* FROM "blogs" WHERE "blogs"."published_dt" <= ? [["published_dt", "2021-01-01"]]
*_gteq
*_gt
*_lteq
*_lt
と同様なので省略
*_null
- ransack
User.ransack(name_null: true).result
#=> User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."name" IS NULL
- ActiveRecord
User.where(name: nil)
#=> User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."name" IS NULL
*_gteq
and *_lteq
(BETWEEN
)
Range objectを渡すだけです。 これについては、ActiveRecordのほうがかなりすっきりかけます。
- ransack
Blog.ransack(published_dt_gteq: Date.new(2020, 12, 1), published_dt_lteq: Date.new(2021, 1, 1)).result
#=> Blog Load (0.2ms) SELECT "blogs".* FROM "blogs" WHERE ("blogs"."published_dt" >= '2020-12-01' AND "blogs"."published_dt" <= '2021-01-01')
- ActiveRecord
range..
を渡すとBETWEENを発行してくれます。ransackよりもきれい。
Blog.where(published_dt: Date.new(2020, 12, 1)..Date.new(2021, 1, 1))
#=> Blog Load (0.1ms) SELECT "blogs".* FROM "blogs" WHERE "blogs"."published_dt" BETWEEN ? AND ? [["published_dt", "2020-12-01"], ["published_dt", "2021-01-01"]]
おまけ2 enumフィールドの検索
Ransackはenum使えないので、数値に変換する必要があります。 しかし、ActiveRecordは当然ながら数値変換不要で検索できます。
下記では、ransack!
https://github.com/activerecord-hackery/ransack/blob/master/CHANGELOG.md#241---2020-12-21 をつかってみました。これは、ransackのDSL構文が古いとエラーを出してくれるとてもいいやつです。
ransackのversionが古いと対応していません。その場合は ransack
出読み替えて下さい。
- ransack
User.ransack!(blogs_status_eq: Blog.statuses[:published]).result
#=> User Load (0.2ms) SELECT "users".* FROM "users" LEFT OUTER JOIN "blogs" ON "blogs"."user_id" = "users"."id" WHERE "blogs"."status" = 2
- ActiveRecord
User.left_joins(:blogs).merge(Blog.where(status: :published))
#=> User Load (0.3ms) SELECT "users".* FROM "users" LEFT OUTER JOIN "blogs" ON "blogs"."user_id" = "users"."id" WHERE "blogs"."status" = ? [["status", 2]]
Discussion