👏

Ransack使わずともActiveRecordのORMでも結構検索できるよね

2021/11/23に公開

概要

Ransack

Ransackのレールにのっかると、高速でプロトタイプが作る事ができる点でとてもすばらしいgemです。

特にviewのフォームとcontrollerを組み合わせるときは、最高。一貫したコードでかくことができるので、コードの省力化に大いに貢献してくれます。僕もとてもお世話になっています

また、viewと連携しない、ビジネスロジックのところでも有用です。とくに文字列のlike検索は *_contなどを使うと事でコードがすっきりさせられます。

ただ、Ransackは関連レコードをleft outer joinで結合するのでこまることがあります。メモリの消費量が大きく、かつ、結合されたテーブルが巨大で制御しづらいときがあります。

Ransackで書かなくてもいいところは、ActiveRecordで書いておけばこの懸念を最小限におさえることができます。

特に、「数値と日付」の範囲や大小検索は、書き味と吐き出されるSQLのすっきりさからActiveRecordで書くほうが幸せになれると個人的には思います。

本記事では、ransackに用意されているDSLのうち、僕がよく使うActiveRecordでかけるものを紹介します。

サンプルコード

下記をcloneし、Set upに従って模擬データを作り、 rails cで試せます。

https://github.com/junara/ar_orm_ransack

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_joinsmergeをつかうとかけます。

  • 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