波ダッシュをいい感じにする Wavedash という gem を作ってます
これは何か?
波ダッシュのような文字を変換するための ruby 用ライブラリです。
takatoshiono/wavedash · GitHub
対象ユーザー
- アプリケーションの文字コードは utf-8 だが、MySQL の文字コードが ujis, eucjp-ms, cp932, sjis である
- アプリケーションの外部と通信するために、ujis, eucjp-ms, cp932, sjis など異なる文字コードへの変換を必要としている
いるのかな…(もしいたら教えてください)
問題
たとえば文字コードが ujis の MySQL データベースを使用する Rails アプリケーションにおいて、〜
(U+301C WAVE DASH) をデータベースに保存しようとすると Mysql2::Error: Incorrect string value
というエラーになる。
何が起きているか
MySQL
Incorrect string value というエラーを出しているのは MySQL。MySQL は sql_mode が strict モードのときに不正な文字を保存しようとするとエラーになる。
Ruby
Ruby から MySQL に接続するのに mysql2 を使ってる。mysql2 は Ruby 側の文字コードを MySQL 側の文字コードに変換する仕事をしている。この仕事は C レベルで行われていて変換に失敗しても例外が発生しない。今回のケースでは U+301C を eucjp-ms に変換できないが、エラーが発生しないのでそのまま MySQL まで届いてしまって Incorrect string value になる。
再現テスト
charset=ujis のテーブルを作る。
CREATE TABLE `ujis_test` ( `id` int(11) NOT NULL AUTO_INCREMENT, `col1` varchar(255) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=ujis
テストスクリプトを作る。
#!/usr/bin/env ruby require 'mysql2' class Mysql2Client def initialize @client = Mysql2::Client.new(username: 'root', password: nil, host: 'localhost', database: 'test', encoding: 'ujis') end def sql_mode(sql_mode = nil) @client.query("set session sql_mode='#{sql_mode}'") if sql_mode @client.query('select @@sql_mode').first['@@sql_mode'] end def insert(value) sql = "insert into ujis_test(col1) values('#{value}')" puts sql @client.query(sql) end end client = Mysql2Client.new puts client.sql_mode(ARGV[1]) client.insert(ARGV[0])
実行する。
sql_mode = STRICT_ALL_TABLES のとき。
$ ./mysql2-client.rb "〜" STRICT_ALL_TABLES STRICT_ALL_TABLES insert into ujis_test(col1) values('〜') ./mysql2-client.rb:18:in `query': Incorrect string value: '\xE3\x80\x9C' for column 'col1' at row 1 (Mysql2::Error) from ./mysql2-client.rb:18:in `insert' from ./mysql2-client.rb:24:in `<main>'
エラーメッセージの '\xE3\x80\x9C’
は utf-8 バイト列。以下のように確認できる。
irb(main):016:0> "〜".bytes.map { |b| b.to_s(16) } => ["e3", "80", "9c"]
また sql_mode = NO_ENGINE_SUBSTITUTION ( MySQL 5.6.6 以降でのデフォルト ) のときはエラーにならない。
$ ./mysql2-client.rb "〜" NO_ENGINE_SUBSTITUTION NO_ENGINE_SUBSTITUTION insert into ujis_test(col1) values('〜')
どうすればいいのか
文字コードを変換する限り必ず変換できない文字はある。文字コードを変換しなくて済むならそれがベストだが、仕事だとそうもいかない。変換できない文字が含まれていたときに以下のようにすればいいと考えてる。
- アプリケーションでバリデーションする
- MySQL でエラーが発生して 500 エラーになるのはユーザーフレンドリーじゃない
- バリデーションしてエラーメッセージを表示したい(「使用できない文字が含まれています。」)
- 変換可能な文字に置換する
- U+301C WAVE DASH は eucjp-ms に変換できないが、U+FF5E FULLWIDTH TILDE は 0xA1C1 に変換できる
- エラーメッセージにどの文字が使用できないのか記載する
- 「使用できない文字(〜)が含まれています」
これらを全部を実装できたらいいけどなかなか大変なので、上から順番にやっていけば、それぞれの対策がそれなりの効果を発揮すると思う。
Wavedash
というわけで「変換可能な文字に置換する」というのを行うためのライブラリとして takatoshiono/wavedash · GitHub というのを作っています。
また、「アプリケーションでバリデーションする」を行うために takatoshiono/character_encoding_validator · GitHub というのも作りました。
蛇足
上でこう書いた。
mysql2 は Ruby 側の文字コードを MySQL 側の文字コードに変換する仕事をしている。この仕事は C レベルで行われていて変換に失敗しても例外が発生しない。今回のケースでは U+301C を eucjp-ms に変換できないが、エラーが発生しないのでそのまま MySQL まで届いてしまって Incorrect string value になる。
ujis のデータベースに対して、mysql2 は eucjp-ms をマッピングしている。それは mysql2/mysql_enc_to_ruby.rb at master · brianmario/mysql2 · GitHubを見るとわかると思う。個人的には euc-jp にするのが正しいのでは?と思っているけど、どうなんだろう?
とはいえ、どちらにマッピングしようと、今回の問題は依然として残るので放置している。
まとめ
ある問題に対する自分の考えと、それを実現するために作っているものについて書きました。
もっといい解決方法を持っていたり、ここに書いたことに間違いがあったら教えていただけるとありがたいです。