レガシープロジェクトでは、DBにShift_JISやEUC-JPなど、UTF-8文字コードを使っていることがあります。
でも、我らがActiveRecordはDBにRead/Writeする時に文字コードを自動変換してくれるので、文字化けが起こることはありません!
しかし、もちろん、Shift_JISに含まれない文字を保存することはできないので、
フォームに「⛄️」を入力してしまったときには、バリデーションエラーにしなければなりません。
カスタムバリデータ
そこで、文字コードをチェックするためのバリデータを作りました。
文字コードチェックは、全ての文字列型カラムで行わなければなりませんが、
いちいちカラムを指定するのは面倒なので、自動で検出するようにしています。
class DatabaseEncodingValidator < ActiveModel::Validator
def initialize(options, &block)
super(options, &block)
@encoding = options[:encoding]
raise "'encoding' parameter is required" if @encoding.blank?
end
def validate(record)
string_attributes(record).each do |attribute|
value = record.read_attribute_for_validation(attribute)
next if value.blank?
begin
value.encode(@encoding)
rescue Encoding::UndefinedConversionError => e
record.errors.add(
attribute, :encoding_incompatible_char,
encoding: @encoding.to_s.upcase,
error_char: e.error_char
)
end
end
end
private
def string_attributes(record)
record.class.columns.select { |c| c.type == :string }.map(&:name)
end
end
使い方
ApplicationRecordに仕込めば、全モデルで文字コードチェックが行われるようになります。
# app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
...
# 全てのモデルクラスで、全ての文字列型カラムの、バリデーションを行う
validates_with DatabaseEncodingValidator, encoding: Encoding::Shift_JIS
end
メッセージを日本語化するために、ja.ymlにも追加します。
# config/locale/ja.yml
---
ja:
errors:
messages:
encoding_incompatible_char: に%{encoding}では使用できない文字「%{error_char}」が含まれています
補足と課題
DBの文字コードは ActiveRecord::Base.connection_config[:encoding]
でも取得できるのですが、encoding: Encoding::Shift_JIS
と明示するようにしています。例えば Oracle DB では文字コードが "sjis" (JA16SJISTILDE) でも、実際にはCP932の文字を保存することができたりするためです。
また、カラムが文字列型かどうかを record.class.columns
で判定していますが、全てのケースでうまく動くかは自信がありません。もっと一般的な判定方法があるかもしれません。