Background
MySQL 8.0 で utf8mb4
のデフォルトの COLLATION (照合順序) が utf8mb4_general_ci
から utf8mb4_0900_ai_ci
に変更されました。
COLLATE
を指定せず、SET NAMES <CHARSET>
を実行すると、そのキャラクタセットのデフォルトの collation が利用されます。
つまり、SET NAMES utf8mb4
の結果、5.7 まではクライアントのCOLLATIONとして utf8mb4_general_ci
が使われますが、8.0 からは utf8mb4_0900_ai_ci
が使われます。
-- MySQL 5.7 だと utf8mb4_general_ci
mysql> SET NAMES utf8mb4;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT @@SESSION.collation_connection;
+--------------------------------+
| @@SESSION.collation_connection |
+--------------------------------+
| utf8mb4_general_ci |
+--------------------------------+
1 row in set (0.00 sec)
-- MySQL 8.0 だと utf8mb4_0900_ai_ci
mysql> SET NAMES utf8mb4;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT @@SESSION.collation_connection;
+--------------------------------+
| @@SESSION.collation_connection |
+--------------------------------+
| utf8mb4_0900_ai_ci |
+--------------------------------+
1 row in set (0.00 sec)
-- COLLATION を明示的に指定が必要
mysql> SET NAMES utf8mb4 COLLATE utf8mb4_general_ci;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT @@SESSION.collation_connection;
+--------------------------------+
| @@SESSION.collation_connection |
+--------------------------------+
| utf8mb4_general_ci |
+--------------------------------+
1 row in set (0.00 sec)
MySQL 8.0 にバージョンアップした際に、意図せず utf8mb4_0900_ai_ci
が使われてしまうケースがないか調査しました。
ドライバは go-sql-driver/mysql
のみを確認しています。他のドライバは挙動が異なると思います。
前提
default_collation_for_utf8mb4
を変更せず、skip_character_set_client_handshake
も有効化できない環境を前提としています。
これらのパラメータが変更できる場合は、サーバサイドの設定で影響を回避可能なはずです(確かめてませんが)。
まとめ
パターン |
ドライバのオプション |
影響有無 |
指定なし |
|
影響なし |
collation のみ指定 |
collation=utf8mb4_general_ci |
影響なし |
charset のみ指定 |
charset=utf8mb4 |
影響あり |
charset と collation を両方指定 & go-sql-driver/mysql v1.8 以降 |
charset=utf8mb4&collation=utf8mb4_general_ci |
影響なし |
charset と collation を両方指定 & go-sql-driver/mysql v1.8 より前 |
charset=utf8mb4&collation=utf8mb4_general_ci |
影響あり |
詳しい説明
charset も collation も指定していないケース (影響なし)
go-sql-driver/mysql
のデフォルト collation は utf8mb4_general_ci
のため utf8mb4_general_ci
が暗黙的に利用されます。collation のみを指定したケースと同様の動作です。
SET NAMES
は実行されません。
charset のみ指定 (影響あり)
冒頭で、記載した COLLATE 指定なしの SET NAMES utf8mb4
が実行されます。
MySQLサーバの utf8mb4 のデフォルト collation が変更された影響を受けます。
charset と collation を両方指定
go-sql-driver/mysql
が v1.8 より前 (影響あり)
v1.8 より前では、charset と collation の両方指定した場合 (charset=utf8mb4&collation=utf8mb4_general_ci
)、以下のように、作用します。
- ハンドシェイク時に
utf8mb4_general_ci
を指定し接続
- 接続後、COLLATE 無しで、
SET NAMES utf8mb4
を改めて実行(結果として、ハンドシェイク時の collation は無視される)
これにより、MySQLサーバの utf8mb4 のデフォルト collation が変更された影響を受けます。
明示的に両方指定するのが一番確実だと思いがちですが、実際は期待と異なった挙動をするようです。
v1.8 より前では、charset とcollationがちぐはぐな組み合わせでも接続可能です (v1.8 以降ではError 1253 (42000): COLLATION ~ is not valid for CHARACTER SET ~
)。
SET NAMES
する際に、ドライバのオプションに指定した collation が使われないからです。
charset=latin1&collation=utf8mb4_general_ci
だと、SET NAMES latin1
が実行され、最終的に利用される COLLATION は latin1
のデフォルトである latin1_swedish_ci
になります。
このような動作をすることから、v1.7 のドキュメントには charset は指定せず、collation だけを指定するように記載があります。
Usage of the charset parameter is discouraged because it issues additional queries to the server. Unless you need the fallback behavior, please use collation instead.
go-sql-driver/mysql
が v1.8 以降(影響なし)
v1.8.0 (2024/03/09 Release) で、charset と collation を両方指定した場合、COLLATEを含めて、SET NAMES <CHARSET> COLLATE <COLLATION>
が実行されるようになりました。v1.8.0 以降 では問題は発生しなくなっています。
Use SET NAMES charset COLLATE collation. by @methane in #1437
https://github.com/go-sql-driver/mysql/releases
methane.hatenablog.jp
両方指定している場合は、v.1.8.0 へバージョンアップしてから MySQL 8.0 へ移行する必要があります。
参考資料
sejima さんもこのあたりについて詳しく調査されています。
labs.gree.jp
結果一覧
検証用のコード
package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
var (
scope, vname, vvalue *string
masterConfig = "root:Password@(192.168.0.100:3306)/"
parameters = []map[string]string{
{},
{"charset": "utf8mb4"},
{"collation": "utf8mb4_unicode_ci"},
{"collation": "utf8mb4_general_ci"},
{"charset": "utf8mb4", "collation": "utf8mb4_unicode_ci"},
{"charset": "utf8mb4", "collation": "utf8mb4_general_ci"},
{"charset": "latin1"},
{"charset": "latin1", "collation": "latin1_bin"},
/* {"charset": "latin1", "collation": "utf8mb4_unicode_ci"}, */
}
)
func main() {
for i, m := range parameters {
dsn := ""
if charset, ok := m["charset"]; ok {
if collation, ok := m["collation"]; ok {
dsn = masterConfig + "?charset=" + charset + "&collation=" + collation
} else {
dsn = masterConfig + "?charset=" + charset
}
} else {
if collation, ok := m["collation"]; ok {
dsn = masterConfig + "?collation=" + collation
} else { // nothing
dsn = masterConfig
}
}
fmt.Printf("%d. charset = %s, collation = %s\n", i+1, m["charset"], m["collation"])
db, err := sql.Open("mysql", dsn)
if err != nil {
panic(err)
}
defer db.Close()
q := fmt.Sprintf(`SELECT /* %d. charset = %s, collation = %s */ 'session', VARIABLE_NAME, VARIABLE_VALUE FROM performance_schema.session_variables WHERE VARIABLE_NAME = 'collation_connection'
UNION ALL
SELECT 'global ', VARIABLE_NAME, VARIABLE_VALUE FROM performance_schema.global_variables WHERE VARIABLE_NAME IN ('collation_connection', 'collation_server', 'default_collation_for_utf8mb4')`, i+1, m["charset"], m["collation"])
rows, err := db.Query(q)
if err != nil {
panic(err)
}
defer rows.Close()
for rows.Next() {
if err := rows.Scan(&scope, &vname, &vvalue); err != nil {
panic(err)
}
fmt.Printf("%s\t%s\t%s\n", *scope, *vname, *vvalue)
}
}
}