MySQL 5.5以降のutf8mb4とPerlのDBD::mysqlのmysql_enable_utf8のワナ

結論から言うと、ひろせが望ましいと思う順にこうすればいいんじゃないの?ってのを列挙します。

  1. my.cnfに[libmysqlclient]グループを追加しそこにdefault-character-set = utf8mb4と書き、DBD::mysqlでは mysql_read_default_file=/etc/my.cnf;mysql_read_default_group=libmysqlclient と指定する
    • mysql_enable_utf8を使いたい場合はSET NAMES指定も必要
  2. my.cnfの[client]グループにdefault-character-set = utf8mb4と書き、DBD::mysqlでは mysql_read_default_file=/etc/my.cnf と指定する。mysqlbinlogなど--default-character-setオプションを解せずエラーになるものは--no-defaultsをつけて実行する
    • mysql_enable_utf8を使いたい場合はSET NAMES指定も必要
  3. DBD::mysqlで接続後に SET NAMES utf8mb4 を実行する

オハマリポイントは

  • DBD::mysql (libmysqlclientなクライアント全般?) は loose-default-character-set の設定値を認識しない
  • select結果をPerlの内部表現文字列(UTF8フラグ)にしようと DBD::mysqlmysql_enable_utf8 を使うと、ついでにクライアント側も文字コード設定を(utf8mb4ではなく)utf8にしてしまう

です。

DBD::mysql (libmysqlclientなクライアント全般?) は loose-default-character-set の設定値を認識しない

my.cnfの[client]グループに書いた設定はクライアント全般に適用されるので、ここにdefault-character-set = utf8mb4 と書ければいいのですが、[client]グループに書くとmysqlbinlogなどこのオプションに対応していないクライアントがエラー終了してしまいます。

my.cnfの設定項目には loose- というprefixをつけることができて、対応していないクライアントはそれを無視することができます。

なので、[client]グループに loose-default-character-set = utf8mb4 と書いておけば、シアワセになれると思っていたのですが、loose- だとDBD::mysql (libmysqlcientを使うクライアント全般かもしれません)が認識しくれず、libmysqlclientのデフォルト文字コード(特に変えてなければ latin1)になってしまいます(ガーン)。

仕方ないので、[libmysqlclient]といったグループを新規で追加してそこにdefault-character-setを書き、DBD::mysqlにはそのグループを読むように指示するか、mysqlbinlogがエラーになるのは --no-defaults オプションをつけて回避するか、の2択になります。

SET NAMESよりdefault-character-setを推す理由は、エスケープ処理に関連するからです。詳しくはこのへんを参照してください。

select結果をPerlの内部表現文字列(UTF8フラグ)にしようと DBD::mysqlmysql_enable_utf8 を使うと、ついでにクライアント側も文字コード設定を(utf8mb4ではなく)utf8にしてしまう

の通りですが、DBD::mysqlmysql_enable_utf8 はPerlの文字列の内部表現化(sv_utf8_decode)するだけでなく、mysql_options(MYSQL_SET_CHARSET_NAME) も実行します。

  • mysql_enable_utf8 が 1
  • mysql_enable_utf8 が 0
  • mysql_enable_utf8を指定しない
    • mysql_options(MYSQL_SET_CHARSET_NAME) は呼ばない

なにが問題になるかというと、mysql_enable_utf8 を 1 にしてネコ (Unicode: U+1F639, UTF-8: \xF0\x9F\x98\xB9) をutf8mb4なテーブルにinsertしても、utf8mb4でなく MYSQL_SET_CHARSET_NAME=utf8 になっているので DBD::mysql::st execute failed: Incorrect string value というエラーでinsertできません。

また、mysql_enable_utf8 を 0 にした場合(MYSQL_SET_CHARSET_NAME=latin1 を実行している)やmysql_enable_utf8を指定しなかった場合(libmysqlclientのデフォルトであるlatin1が使われる)、my.cnfの適切なグループにdefault-character-set = utf8mb4と書いてあれば、character_set_clientや_connectionはutf8mb4になりますが、libmysqlclientのエスケープ処理は(多分)latin1で行われるのでもしかしたら問題があるかもしれません。

エスケープ処理が本当にlatin1で行われるのかと、その場合具体的にどういう問題があるのか、はちょっと調べきれてないので情報あったら教えてください><)

エスケープの問題を別にすれば、mysql_enable_utf8を有効にしてもしなくてもいずれにせよ、MySQLに接続した直後にSET NAMES utf8mb4を実行すれば、ネコもちゃんとハンドリングできます。

生のDBD::mysqlなら、Callbacksのconnectedでやるといいと思います。

my $dbh = DBI->connect($dsn, $user, $password, {
    ...
    Callbacks => {
        connected => sub {
            $_[0]->do('SET NAMES utf8mb4');
            return;
        },
    },
});