お前らのRansackは間違ってる
概要
煽りタイトル失礼。
地方でリモートで業務をこなし、新卒入社1ヶ月で先輩の尻拭いでPM(もどき)をやらされてる社畜がRansackを語るだけの記事です。
皆さん、Ransackで消耗してますか?
Ransackは業務アプリケーションなどを開発してると、めちゃくちゃ便利ですよね〜
その反面、かゆいとろこに手の届かないのがRansack。
今回は、Ransackで3ヶ月開発を続け、すべてが白紙になり、もう一度システムを1から作り直しになった経験を踏まえて、Ransackのノウハウを共有していきたいと思います。
今からRansackで開発を進めようと思ってる方の糧になってくれれば幸いです。
目次
- search_form_forは使うな
- Ransackのためにカラムを作成するな
- 行いたい検索は大体出来る
- 余談
search_form_forは使うな
単純な話です。search_form_forは使うな…ただそれだけ
Ransackは便利です。search_form_forも便利です。パラメータにmatcherを設定してあげれば、条件に合ったクエリを作成してくれます。
と、Ransack系の記事はよく書いてるし、公式も勧めてるのでそのとおり使ってしまうのも間違いないのですが…僕はおすすめしません。
Ransackが辛くなる時
- 開発途中で検索するテーブルが変わる
- 開発途中でカラム名が変わる
- 同じ検索機能を複数ページに配置する
- 複数条件が絡まるのに、UIは1つしか配置出来ない(痒い所に手が届かない)
- セキュリティ対策がだるい
具体例
-
開発途中で検索するテーブルが変わる
これは現場ではよくあることですが(よくない)、開発途中でテーブル名が変わったりするとき、Ransackに合わせてパラメータをmatcherにしてると、全て変更になるので辛いです。
※ Rails側でModel名指定すればええやんとか言われると辛いのですが、ハードコーディグになるので避けたい所存…
-
開発途中でカラム名が変わる
上記同様。変更が合った場合、変更が合った場合に、そのパラメータを変更しなくてはいけないので辛い。
-
同じ検索機能を複数ページに配置する
複数配置するだけならいいのですが、上記のような変更が合った場合が最悪です。修正点が2倍になるので…
-
複数条件が絡まるのに、UIは1つしか配置出来ない(痒い所に手が届かない)
例えば、用紙サイズを正規化した場合、下記のようなテーブルになりますが、UI上ではセレクトボックスで(A4, B5など)表示したい場合、どうすればいいでしょうか?Ransackを純粋に利用してると解決するのは難しいです。
用紙フォーマット 用紙数値 A 4 B 5 A 1 A 3 B 4 -
セキュリティ対策がだるい
送られてきたパラメータをストロングパラメータに変換するのがめんどいんです。特に業務用アプリケーションなどを開発してる方は解ると思いますが、パラメータ数が異常に多いのでしんどい…
解決策
search_form_forは使わず、form_withを使用しましょう。
Ransack用のパラメータはコントローラなり、Concernなどで置換しましょう。
※ 具体例は後述
メリット
- パラメータは1箇所に集約出来るので、テーブル名やカラム名の変更が合っても、変更箇所は1箇所のみです。加えて、HTML上などで直接修正を行わなくていいので、パラメータ探しで疲弊しなくてすみますし、可読性も向上します。
- 複数条件が絡まっても、コントローラ側で条件分岐出来るのでかゆいとろこに手の届いた検索を作成することが可能になります。
- 飛んでくるパラメータを固定出来るので、許可しすぎたり、許可し忘れたりするということがなくなり、ストロングパラメータで疲弊しなくて済むのと同時に、Ransackのmatcherを直接HTMLに書くことがなくなるので、テーブルの構造やカラム名を隠蔽化出来るのも大きなメリットだと思います。
具体例
先程、例に出した用紙サイズを検索するものを簡単に書いてみます。
テーブル構造はこんな感じ
use
使用用途マスタ
id | use_name |
---|---|
1 | 事務 |
2 | 経理 |
3 | 私用 |
use_paper_log
用紙の使用履歴
paper_format_code
はenumで管理
use_id | paper_format_code | paper_number |
---|---|---|
1 | 01 | 4 |
2 | 02 | 5 |
Model
class Use < ApplicationRecord
has_many :use_paper_log
end
class UsePaperLog < ApplicationRecord
belongs_to :use
enum paper_format_code: {
A: '01',
B: '02',
}, prefix: true
end
# Note: 用紙サイズを表示するためのActiveHash
class Ah::Format < ActiveHash::Base
self.data = [
{id: 1, name: 'A1'},
{id: 2, name: 'A2'},
{id: 3, name: 'A3'},
{id: 4, name: 'A4'},
{id: 5, name: 'A5'},
{id: 6, name: 'B1'},
{id: 7, name: 'B2'},
{id: 8, name: 'B3'},
{id: 9, name: 'B4'},
{id: 10, name: 'B5'},
]
end
controller
class SearchesController < ApplicationController
# 検索ページを表示
def index
@use = Use.all
@format = Ah::Format.all # ActiveHashを使って、選択肢を作成
end
def search
# NOTE: Ransack用のパラメータを取得
# before_actionで対応してもOK
# わかりやすいように関数内で実行
set_params
# NOTE: Ransackを実行
@q = UsePaperLog.ransack(@query)
end
private
# NOTE: パラメータをRansack用に置換する関数
def set_params
@query = {}
# NOTE: ①②③⑤の問題を全部解決
@query['use_id_in'] = params[:use]
# NOTE: 「④複数条件が絡まるのに、UIは1つしか配置出来ない」問題を解消
if params[:format].present?
paper_format_code = nil
paper_number = nil
case params[:format]
when '1'
paper_format_code = 'A'
paper_number = '1'
when '2'
paper_format_code = 'A'
paper_number = '2'
when '3'
paper_format_code= 'A'
paper_number = '3'
# ~ 中略 ~
when '9'
paper_format_code = 'B'
paper_number = '4'
when '10'
paper_format_code = 'B'
paper_number = '5'
end
# NOTE: ①②のように破壊的変更化合っても
# ここを修正すれば終わるので楽
@query['paper_format_code_eq'] = paper_format_code
@query['paper_number_eq'] = paper_number
end
end
end
view
= form_with url: search_path, method: :get, local: true do |f|
.search
.search-head
| 仕様用途
.search-body
= f.collection_check_boxes :use, @use, :id, :name, include_hidden: false
.search
.search-head
| 用紙サイズ
.search-body
= f.collection_select :format, @format, :id, :name, include_blank: '選択して下さい'
Ransackのためにカラムを作成するな
これは、先輩が逃げ出す前の話…
Ransackを技術採用したのがその先輩だったので、前項の「複数条件が絡まるのに、UIは1つしか配置出来ない」という問題にぶち当たったので、どうすればいいのか質問した時。
「Ransackに合わせた方がハッピーになるから、専用のカラムを作成すればええやん」と言われ、Ransack用のカラムを作成してしまったというお話…
単純に上記の様に対応すれば、万事休すだったので現在はそんな馬鹿な対応はしていない。
先輩だからとベストプラクティスを出してくれるわけではなかった…
行いたい検索は大体出来る
Ransackの欠点
各RDBMSごとや、バージョンによって専用の機能を持っていたりすることがある。例えば、空間データ型とか…
Ransack単体ではこういう機能に対しては対応しきれない。
例えば、
- 東京タワー半径500m以内
単純にRansackには辛い… - 星3.5以上
算出してあるカラムがあるのか、それとも毎回算出してるのかで変わってくる
算出してあれば楽勝。毎回算出するのであれば辛い… - 中華料理
これはRansackの得意分野なので楽勝
解決策
Ransackで厳しい部分は自前でWhere文を書いて上げて、その上にRansackを掛けてあげましょう
さっきの例であれば、東京タワー半径500m以内のクエリは自分でwhere文を書いて、その後ろに、ransackを掛けてあげればいい。
具体例
空間データ型の例を書くのは厳しいので、先程の用紙の例を使います。
内容は先ほどと同じで、一部をわざとWhere文で書くだけです。
controller
class SearchesController < ApplicationController
def search
# NOTE: Ransack用のパラメータを取得
# before_actionで対応してもOK
# わかりやすいように関数内で実行
set_params
+ # NOTE: Ransackでは対応しきれない複雑な検索は
+ # ここで対応しておく
+ @q = UsePaperLog.where(user_id: params[:use])
# NOTE: Ransackを実行
+ # その上で、Ransackを掛けてあげる
@q = UsePaperLog.ransack(@query)
end
private
# NOTE: パラメータをRansack用に置換する関数
def set_params
@query = {}
+ # ここをsearch関数へ移動
- # NOTE: ①②③⑤の問題を全部解決
- @query['use_id_in'] = params[:use]
# NOTE: 「④複数条件が絡まるのに、UIは1つしか配置出来ない」問題を解消
if params[:format].present?
paper_format_code = nil
paper_number = nil
case params[:format]
when '1'
paper_format_code = 'A'
paper_number = '1'
when '2'
paper_format_code = 'A'
paper_number = '2'
when '3'
paper_format_code= 'A'
paper_number = '3'
# ~ 中略 ~
when '9'
paper_format_code = 'B'
paper_number = '4'
when '10'
paper_format_code = 'B'
paper_number = '5'
end
# NOTE: ①②のように破壊的変更化合っても
# ここを修正すれば終わるので楽
@query['paper_format_code_eq'] = paper_format_code
@query['paper_number_eq'] = paper_number
end
end
end
余談
最後まで読んでいただきありがとうございます。
初投稿ということもあり、どの程度読まれるかはわかりませんがRansack Lifeで疲弊しない人が増えると嬉しいです。
フリーランスになるか、転職するか迷ってる今日この頃…
Discussion