MySQLの文字セットがutf8の場合、utf-8で符号化すると4バイトになる文字(😁のような絵文字など)をセットすると、SQLモード(sql_mode)が厳密モード(STRICT_ALL_TABLES または STRICT_TRANS_TABLES のいずれかが有効)でない場合、その文字以降が切り捨てられてしまう。(警告は発生する)
4バイトUTF-8文字に対応するためには、CHARACTER SET に utf8mb4(COLLATE に utf8mb4_unicode_520_ci など utf8mb4_xxx) を指定したカラムを使用し、接続文字セットも utf8mb4 を使用する必要がある。
ちなみにRailsでは、MySQLのカラムの文字セットがutf8の場合に4バイトUTF-8文字をセットしようとすると、以下のようなエラーが発生するので、気付かないうちに文字列が切り捨てられてしまうことはない。
An ActiveRecord::StatementInvalid occurred in news#update:
Mysql2::Error: Incorrect string value: '\xF0\x9F\x98\x80\x0D\x0A' for column 'description' at row 1: UPDATE `news` SET `description` = '😀\r\n' WHERE `news`.`id` = 2
app/controllers/news_controller.rb:98:in `update'
これは特に指定していない場合、AbstractMysqlAdapter#configure_connection で、STRICT_ALL_TABLES がセッションのSQL_MODEに追加されているから。(NO_AUTO_VALUE_ON_ZERO も追加される。)
これは以下のようにして確認できる。
- mysqlクライアントで確認
mysql> show variables like 'sql_mode'; +---------------+------------------------+ | Variable_name | Value | +---------------+------------------------+ | sql_mode | NO_ENGINE_SUBSTITUTION | +---------------+------------------------+ 1 row in set (0.00 sec)
- 同じデータベースに対して、rails consoleで確認
> con = ActiveRecord::Base.connection > con.select_all("SHOW VARIABLES LIKE 'sql_mode'") (0.8ms) SHOW VARIABLES LIKE 'sql_mode' => #<ActiveRecord::Result:0x00007fc6ca533728 @columns=["Variable_name", "Value"], @rows=[["sql_mode", "NO_AUTO_VALUE_ON_ZERO,STRICT_ALL_TABLES,NO_ENGINE_SUBSTITUTION"]], @hash_rows=nil, @column_types={}>
対処方法
カラムの CHARACTER SET を utf8mb4 、COLLATE を utf8mb4_xxx に変換して、接続文字セットに utf8mb4 を使用すればよいが、何らかの事情でカラムを utf8mb4 に変換できない場合は、黙って4バイトUTF-8文字以降が切り捨てられるといろいろとまずいので、4バイトのUTF-8文字をバリデーションではじくことになるだろう。
UTF-8にエンコードすると4バイトになるUnicode文字の範囲は、U+10000からU+10FFFFである。
- https://ja.wikipedia.org/wiki/UTF-8#%E3%82%A8%E3%83%B3%E3%82%B3%E3%83%BC%E3%83%89%E4%BD%93%E7%B3%BB
- https://ja.wikipedia.org/wiki/Unicode#%E9%9D%A2
PHPでの例
if (preg_match('/[\x{10000}-\x{10FFFF}]/u', $s) { /* ... */ }
if (preg_match('/[\xF0-\xF7][\x80-\xBF][\x80-\xBF][\x80-\xBF]/', $s)) { /* ... */ }
preg_match_all('/[\x{10000}-\x{10FFFF}]/u', $s, $matches); // $matches[0]に4バイトutf-8の文字の配列が格納される。
Rubyでの例
if /[\u{10000}-\u{10FFFF}]/ =~ s # ... end
chars = s.scan(/[\u{10000}-\u{10FFFF}]/) # charsに4バイトutf-8の文字の配列が格納される。