やりたいこと
- あるテーブルのある VARCHAR 型のカラムで LIKE 検索したい。
- このとき、検索文字列の「カタカナの半角・全角」「ひらがな・カタカナ」を区別せずに検索したい。
- せっかくなので色々なテーブルやカラムで使用できるように、特定のモデルのメソッドではなく汎用的なクラスとして実装したい。
バージョン情報
$ ruby -v
ruby 3.2.2 (2023-03-30 revision e51014f9c0) [arm64-darwin22]
$ bin/rails -v
Rails 7.0.4.3
$ mysql --version
mysql Ver 14.14 Distrib 5.7.42, for osx10.18 (x86_64) using EditLine wrapper
方法
- カタカナの半角・全角を区別せずに検索するために COLLATE 句で
utf8mb4_unicode_ci
を指定する。- COLLATE 句は照合順序を指定するための句。
- 照合順序とはデータベース内で文字を比較したりソートしたりする際のルールのこと。
-
utf8mb4_unicode_ci
は Unicode のコードポイントに基づいて文字列を比較する照合順序のひとつ。- この照合順序ではカタカナの半角・全角を区別しないし、ひらがな・カタカナも区別しない。
- quote_table_name メソッドでテーブル名を quote_column_name メソッドでカラム名をバックスラッシュでクォートする。
-
sanitize_sql_like メソッドで LIKE 演算子の値をサニタイズする。
- 具体的には
%
と_
をエスケープすることで、ユーザの入力値を安全に LIKE 演算子に渡すことができる。
- 具体的には
class PerformCaseInsensitiveSearch
COLLATION = 'collate utf8mb4_unicode_ci'
delegate :model, to: :@relation, private: true
delegate :connection, :sanitize_sql_like, to: :model, private: true
delegate :quote_column_name, :quote_table_name, to: :connection, private: true
def self.call(...)
new(...).call
end
def initialize(relation, column_name:, value:)
@relation = relation
@column_name = column_name
@value = value
end
def call
@relation.where(
"#{quoted_table_name}.#{quoted_column_name} #{COLLATION} LIKE ?",
"%#{sanitized_value}%"
)
end
private
def quoted_table_name
quote_table_name(model.table_name)
end
def quoted_column_name
quote_column_name(@column_name)
end
def sanitized_value
sanitize_sql_like(@value)
end
end
Monster.pluck(:name)
#=> ["リオレウス", "リオレイア", "銀レウス", "るかりお"]
searched_monsters = PerformCaseInsensitiveSearch.call(Monster.all, column_name: :name, value: 'リオ')
searched_monsters.to_sql
#=> "SELECT `monsters`.* FROM `monsters` WHERE (`monsters`.`name` collate utf8mb4_unicode_ci LIKE '%リオ%')"
searched_monsters.pluck(:name)
#=> ["リオレウス", "リオレイア", "るかりお"]