問題
ransackでは、ActiveRecordのscopeを用いて検索することができます。しかし、以下のように検索キーワードとして 'T'
は使えません。エラーメッセージにある通り、scopeに渡されているはずのキーワードが渡されていません。
環境
- Rails 7.1.7
- Ruby 3.2.2
- Ransack 4.0.0
原因
このような問題が起きるのは、'T'
などといった真理値っぽいものは、ActiveRecordのscopeに渡される前にサニタイズされるからです。
検索キーワードとして、真っぽいものが入っていたら true
に、偽っぽいものが入っていたらfalse
に変換しています(sanitized_scope_args
メソッドの5-8行目)。
def sanitized_scope_args(args)
if args.is_a?(Array)
args = args.map(&method(:sanitized_scope_args))
end
if Constants::TRUE_VALUES.include? args
true
elsif Constants::FALSE_VALUES.include? args
false
else
args
end
end
TRUE_VALUES = [true, 1, '1', 't', 'T', 'true', 'TRUE'].to_set
FALSE_VALUES = [false, 0, '0', 'f', 'F', 'false', 'FALSE'].to_set
ここで変換されたものがchain_scope
メソッドに渡されます(add_scope
メソッドの12行目)。検索キーワードとして 'T'
を渡していれば、sanitized_args
としてtrue
が渡されます。したがって、chain_scope
メソッド内の3行目が実行されます。つまり、ActiveRecord scopeの引数として何も渡さないということです。そのため、上記gifにあるようなエラーメッセージが表示されます。
def add_scope(key, args)
sanitized_args = if Ransack.options[:sanitize_scope_args] && !@context.ransackable_scope_skip_sanitize_args?(key, @context.object)
sanitized_scope_args(args)
else
args
end
if @context.scope_arity(key) == 1
@scope_args[key] = args.is_a?(Array) ? args[0] : args
else
@scope_args[key] = args.is_a?(Array) ? sanitized_args : args
end
@context.chain_scope(key, sanitized_args)
end
def chain_scope(scope, args)
return unless @klass.method(scope) && args != false
@object = if scope_arity(scope) < 1 && args == true
@object.public_send(scope)
else
@object.public_send(scope, *args)
end
end
解決策
真理値っぽいものがtrue
やfalse
にサニタイズされることが原因です。このサニタイズのスキップは、ransack でサポートされています。ransackable_scopes_skip_sanitize_args
メソッドを以下のようにオーバーライドします。そうすれば、partial_match
scopeの引数はサニタイズされません。詳しくはドキュメントを参照ください。
class Stock < ApplicationRecord
scope :partial_match, ->(keyword) do
return all if keyword.blank?
pattern = "%#{sanitize_sql(keyword)}%"
where('name LIKE :pattern', pattern:)
end
private
class << self
def ransackable_scopes_skip_sanitize_args
%i[partial_match]
end
end
end
まとめ
「ransack scopeを用いた検索では、キーワードとして 'T' が使えない」という問題の原因は、検索キーワード中の真理値っぽいものがサニタイズされていることでした。この問題は、そのサニタイズをスキップすることで解決しました。
なぜ真理値っぽいものをサニタイズしているのか?
boolean型のフィールドに対してransack scopeをいい感じに使えるようにするために、真理値っぽいものをサニタイズしているのかもしれません(@aki77 さんに教えてもらいました)。
例えば、チェックボックスのvalue
は'1'
や'T'
などいろんな値が使われています。'1'
や'T'
を一意な値に変換して、scope内で使いやすいようにしているのだとしたら、サニタイズするのも少しは納得します。ただ、サニタイズという言い方は微妙ですが。。。