やりたいことは簡単なのに。。。
MySqlのvarcharが扱える文字コード(character set)は、デフォルトでutf8であり、これはutf8mb3(3バイト文字)のエイリアスでもあるようです(バージョンがあがるごとにだんだんutf8mb4に移行しつつあるようですが)。
で、業務で使うmysql5.7には、utf8で接続することになっているため、4バイト文字が入りません。
というわけで、サーバー側(java)で文字を切ってあげればよいことになりました。
調べてみたものの、なんかスマートな方法がぱっと出てこなかったので、なんとなく調べたことをまとめておきます。
サロゲートペアと4バイト文字の違い
まずこれがわからなかった。
上司に言われた指示は「絵文字を使えないようにしたい」だった。
自分の会社の先達が作ったコードでは、バリデーションでサロゲートペアをひっかけており、これを参考にしろと提示された。
で、サロゲートペアって?
わからんので調べてみる
いったんUnicodeとutf8、サロゲートペアの話をある程度理解してから進みます。
- ある文字があったとき、Unicodeとutf8では、それぞれ違う符号であらわす
- 通信上はUnicodeで行い、実際プログラムが利用するときにutf8の符号にする
- utf8は1~3バイトで表す
- データ通信等ではUnicodeを使用し、実際のプログラムでは符号化(1~3バイト)して使う
- 文字「あ」は、Unicodeでは「3042」、utf8では「E3 81 82」
- データ通信等ではUnicodeを使用し、実際のプログラムでは符号化(1~3バイト)して使う
- Unicodeは当初の設計上、2バイト一文字で表現しようとしていたが、表現する文字が増えてきたため、2バイト*2で
一文字で表現する方法が追加された- これがサロゲートペアで、前半がU+D800~U+DBFF、後半がU+DC00~U+DFFFの範囲と定義されている
- UTF-16ではサロゲートペアで表されるような、基本多言語面外の符号位置をUTF-8で表す時は(中略)U+10000~U+10FFFFの符号位置にデコードしてから変換する(Wiki参照)
- BPMの表現 = UTF8では3バイト文字で表現可能
- 1~3バイト内で表現しきれなかったutf8の文字を表現するために4バイト使って表現する
- utf8mb3ではこの4バイト文字が入らない
こちらの話から、utf8においては、Unicodeのサロゲートペアはutf8で4バイトで符号化されるのと同義であることがわかりました(でいいのかな?たぶん)。
理解力が乏しいせいか、なかなか把握できず困りました。。。。
でこれをいざJavaでかいてみるとするとどうなるか。
いったん、上記の話を確かめるためJshellで確認してみます。
import java.util.stream.IntStream;
String word = "𠮷";
System.out.println("this word has length of " + word.length());
if (word.length() != word.codePointCount(0, word.length())) {
System.out.println("code point count " + word.codePointCount(0, word.length()));
}
IntStream.range(0, word.length()).forEach(i -> {
System.out.println("code point is " + Integer.toHexString(word.codePointAt(i)));
var target = word.charAt(i);
if (Character.isSurrogate(target)){
System.out.println("this is surrogate pair");
}
System.out.println("check "+ target);
});
-------------------
this word has length of 2
code point count 1
code point is 20bb7
this is surrogate pair
check ?
code point is dfb7
this is surrogate pair
check ?
ちなみに𠮷は「つちよし」で変換できます。
ためしにMysql5.7のvarcharにいれるとエラーになりました。
Incorrect string value: '\xF0\xA0\xAE\xB7' for column 'name' at row 1
名前にいれられないとか。。。。
まあ、今回の要件はそこではないのでよいのです。
javaのcharには2バイト文字が格納されるので、string.length()は文字数でなく、左記ユニット数がカウントされます。
コードポイントが実際のUnicodeで表現する文字1つにあたるため、サロゲートペアを確認する時はcodePointCountと比較することも可能です。
結論?
ということで、結局サロゲートペア = UTF8の4バイト文字の認識でよい、のかな?
どちらにしろ、Character.isSurrogate()
が最も便利そうなので、これでサロゲートペアを確認し、4バイト文字を制限することとします。
参考
- 【図解】【3分解説】UnicodeとUTF-8の違い!【今さら聞けない】
- Unicodeがどんな風にUTF-8に割当てられているか
- UTF-8の符号化方法について
- Unicode 【 ユニコード 】
- 符号化文字集合と文字符号化方式 - 「プログラマのための文字コード技術入門」を読んだ
- MySQL で utf8 と utf8mb4 の混在で起きること