CLOVER🍀

That was when it all began.

MySQL Connector/JとCharacter Set/Character Set Results/Connection Collationとの設定、関係がよくがわからなかったので調べてみる

これは、なにをしたくて書いたもの?

MySQLJDBCドライバー、Connector/JのcharacterEncodingcharacterSetResultsconnectionCollationあたりの説明を見ていて、
不思議な感じがしたので調べてみることにしました。

MySQL :: MySQL Connector/J 8.0 Developer Guide :: 6.3.3 Session

どう指定したらいいか、よくわからなくなるんですよね。

Connector/Jの説明を読む

characterEncodingcharacterSetResultsconnectionCollationの説明を、それぞれ見てみます。

MySQL :: MySQL Connector/J 8.0 Developer Guide :: 6.3.3 Session

なお、このドキュメントを見ている時のMySQL Connector/Jのバージョンは、8.0.28です。

characterEncodingは、character_set_clientおよびcharacter_set_connectionを「指定されたJavaエンコーディングのデフォルトの
Character Setに設定し、collation_connectionをCharacter SetのデフォルトのCollationに設定する」と書かれています。

Instructs the server to set session system variables 'character_set_client' and 'character_set_connection' to the default character set for the specified Java encoding and set 'collation_connection' to the default collation for this character set.

characterEncodingconnectionCollationも指定されていない場合は、characterEncodingとしては8.0.26以降はutf8mb4が指定されると
書かれています。

If neither this property nor the property 'connectionCollation' is set:
For Connector/J 8.0.25 and earlier, the driver will try to use the server default character set; For Connector/J 8.0.26 and later, the driver will use "utf8mb4".

utf8mb4Javaエンコーディングではありませんが…。

ちなみに、現在のMySQLはCharacter Encodingはutf8mb4がデフォルトであり、utf8mb4のデフォルトのCollationはutf8mb4_0900_ai_ciです。

MySQL Server にはサーバー文字セットとサーバー照合順序があります。 デフォルトでは、これらは utf8mb4 および utf8mb4_0900_ai_ci ですが、サーバーの起動時にコマンドラインまたはオプションファイルで明示的に設定し、実行時に変更できます。

MySQL :: MySQL 8.0 リファレンスマニュアル :: 10.3.2 サーバー文字セットおよび照合順序

characterSetResultsについても、「指定されたJavaエンコーディングに対応するCharacter Setでエンコードされたデータを返すように
サーバーに指示します」と書かれています。

Instructs the server to return the data encoded with the default character set for the specified Java encoding.

指定しない、またはnullの場合、サーバーは元のCharacter Setでデータを送信し、ドライバーは結果のメタデータに従ってデータを
デコードします。

If not set or set to "null", the server will send data in its original character set and the driver will decode it according to the result metadata.

connectionCollationは、セッションシステム変数collation_connectionを指定されたCollationに設定し、character_set_client
character_set_connectionを対応するCharacter Setに設定するようにサーバーに指示します。

Instructs the server to set session system variable 'collation_connection' to the specified collation name and set 'character_set_client' and 'character_set_connection' to the corresponding character set.

この結果、connectionCollationcharacterEncodingで指定した値を上書きする挙動になるようです。

This property overrides the value of 'characterEncoding' with the character set this collation belongs to.

そしてconnectionCollationcharacterEncodingも指定されていない場合は、connectionCollationのデフォルトのCollationになると
書かれています。

If neither this property nor the property 'characterEncoding' is set:
For Connector/J 8.0.25 and earlier, the driver will try to use the server default character set;
For Connector/J 8.0.26 and later, the driver will use "utf8mb4" default collation.

これは、どう指定するのが適切でしょうか?こうなると、各変数がアプリケーションの動作に与える影響を確認しておく必要が
ありそうですね。

現在はCharacter Encodingはutf8mb4を指定するのが無難かと思いますので、主にCollationまわりに関する話がポイントかなとは
思いますが。

MySQLのCollationのドキュメントを読んでみる

次のドキュメントを見てみます。

MySQL :: MySQL 8.0 リファレンスマニュアル :: 10.4 接続文字セットおよび照合順序

まずは、サーバーおよびデータベースレベルのCharacter SetとCollationについて。

・character_set_server および collation_server システム変数は、サーバーの文字セットと照合順序を示します。
・character_set_database および collation_database システム変数は、デフォルトデータベースの文字セットおよび照合順序を示します。

character_set_clientは、クライアントが送信するデータのエンコーディングに関わる話になります。

クライアントから離れるときのステートメントの文字セットは何ですか。
サーバーは、character_set_client システム変数値を、クライアントが送信するステートメントの文字セットにします。

character_set_connectionは、クライアントが送信したデータを変換する先のエンコーディングを指定するようです。

サーバーがステートメントを受信したあと、どの文字セットに変換するべきですか。
これを確認するために、サーバーは character_set_connection および collation_connection システム変数を使用します:
サーバーは、クライアントによって送信されたステートメントを character_set_client から character_set_connection に変換します。

一方で、collation_connectionリテラル文字列の比較に使われるだけのようですね。

collation_connection は、リテラル文字列の比較に重要です。 カラム値と文字列を比較する場合、collation_connection は関係ありません。

ということは、collation_connectionを気にすることはほとんどなさそうですね。

character_set_resultsは、サーバーから返すデータのエンコーディングに使用されるようです。

クエリー結果をクライアントに返送する前に、サーバーはどの文字セットに変換する必要がありますか。
character_set_results システム変数値は、サーバーがクライアントにクエリー結果を返信するときに使用する文字セットを示します。 これには、カラム値、結果メタデータ (カラム名など)、エラーメッセージなどの結果データが含まれます。

特に変換を行い場合は、設定しないかbinaryに指定する、と。

結果セットまたはエラーメッセージの変換を実行しないようにサーバーに指示するには、character_set_results を NULL または binary に設定します:

character_set_servercharacter_set_databasecharacter_set_clientcharacter_set_connectionutf8mb4で統一していれば
問題なさそうですし、そうするとcharacter_set_resultsは明示的に指定しなくてもいいのでは、という感じでしょうか。

character_set_resultsを指定したとしても、utf8mb4でしょうね。

それぞれのシステム変数のドキュメントと説明は、こちら。

  • character_set_server … サーバーのデフォルトの文字セット
  • character_set_database … デフォルトデータベースで使用される文字セット
    • 現在は非推奨の設定
  • character_set_client … クライアントから到達するステートメントの文字セット
  • character_set_connection … 文字セットイントロデューサなしで指定されたリテラルおよび数値から文字列への変換に使用される文字セット
  • character_set_results … クエリー結果をクライアントに返すために使用される文字セット。 これには、カラム値、結果メタデータ (カラム名など)、エラーメッセージなどの結果データが含まれます。
  • collation_server … サーバーのデフォルトの照合順序
  • collation_database … デフォルトデータベースで使用される照合
    • 現在は非推奨の設定
  • collation_connection … 接続文字セットの照合順序。collation_connection は、リテラル文字列の比較に重要です。 カラム値と文字列を比較する場合、collation_connection は関係ありません。これは、カラムには照合優先度の高い独自の照合があるためです

MySQL Connector/Jに話を戻すと

ここまでの話から、MySQL Connector/Jの設定に話を戻すと、characterEncodingcharacterSetResultsconnectionCollationのそれぞれを
どう指定すればいいのか?ということなのですが。

MySQLサーバー側のCharacter Setをutf8mb4に統一するのなら

  • characterEncodingUTF-8
  • characterSetResults … 指定なし
  • connectionCollation … 指定しなくても実害はなさそう(文字列リテラルの比較のみの話なので)だが、気になるならサーバーと同じCollationを指定

といったところでしょうか。

characterEncodingについてはMySQL Connector/Jの説明(デフォルト値の部分)が気になるので、この後にテストコードを書いて
確認してみることにします。

環境

今回の環境はこちら。

$ java --version
openjdk 17.0.2 2022-01-18
OpenJDK Runtime Environment (build 17.0.2+8-Ubuntu-120.04)
OpenJDK 64-Bit Server VM (build 17.0.2+8-Ubuntu-120.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.8.5 (3599d3414f046de2324203b78ddcf9b5e4388aa0)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 17.0.2, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-109-generic", arch: "amd64", family: "unix"

MySQLはこちらのバージョンで、172.17.0.2で動作しているものとします。

$ mysql --version
mysql  Ver 8.0.28 for Linux on x86_64 (MySQL Community Server - GPL)

また、サーバーのCharacter SetおよびCollationは以下の設定としておきます。

character-set-server = utf8mb4
collation-server = utf8mb4_0900_bin

準備

作成したMavenプロジェクトの依存関係等は、こちら。

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.28</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.8.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.22.0</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.22.2</version>
            </plugin>
        </plugins>
    </build>

次に、テストコードの雛形を作成します。

src/test/java/org/littlewings/mysql/ConnectorCharacterSetTest.java

package org.littlewings.mysql;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.*;

public class ConnectorCharacterSetTest {
    List<String> characterSetVariables = List.of(
            "character_set_connection",
            "character_set_client",
            "character_set_database",
            "character_set_filesystem",
            "character_set_results",
            "character_set_server",
            "character_set_system"
    );

    List<String> collationVariables = List.of(
            "collation_connection",
            "collation_database",
            "collation_server"
    );

    private Map<String, String> collectCharacterSets(Connection conn) throws SQLException {
        Map<String, String> characterSets = new LinkedHashMap<>();

        for (String characterSetVariable : characterSetVariables) {
            try (PreparedStatement ps = conn.prepareStatement("show variables where variable_name = ?")) {
                ps.setString(1, characterSetVariable);

                try (ResultSet rs = ps.executeQuery()) {
                    while (rs.next()) {
                        characterSets.put(rs.getString(1), rs.getString(2));
                    }
                }
            }
        }

        return characterSets;
    }

    private Map<String, String> collectCollations(Connection conn) throws SQLException {
        Map<String, String> collations = new LinkedHashMap<>();

        for (String collationVariable : collationVariables) {
            try (PreparedStatement ps = conn.prepareStatement("show variables where variable_name = ?")) {
                ps.setString(1, collationVariable);

                try (ResultSet rs = ps.executeQuery()) {
                    while (rs.next()) {
                        collations.put(rs.getString(1), rs.getString(2));
                    }
                }
            }
        }

        return collations;
    }

    // ここに、テストを書く!!
}

現在の接続内でのCharacter SetおよびCollationを収集するメソッドを用意して、以降に作成するテストで接続プロパティを変更するとともに、
これらのシステム変数がどのように変化していくかを見ていくことにします。

characterEncodingを確認してみる

とりあえず、なにも指定しない場合。

    @Test
    public void nonSettings() throws SQLException {
        String url = "jdbc:mysql://172.17.0.2:3306/practice";
        String username = "kazuhira";
        String password = "password";

        try (Connection conn = DriverManager.getConnection(url, username, password)) {
            Map<String, String> characterSets = collectCharacterSets(conn);
            Map<String, String> collations = collectCollations(conn);

            assertThat(characterSets).containsExactly(
                    entry("character_set_connection", "utf8mb4"),
                    entry("character_set_client", "utf8mb4"),
                    entry("character_set_database", "utf8mb4"),
                    entry("character_set_filesystem", "binary"),
                    entry("character_set_results", ""),
                    entry("character_set_server", "utf8mb4"),
                    entry("character_set_system", "utf8mb3")
            );

            assertThat(collations).containsExactly(
                    entry("collation_connection", "utf8mb4_0900_ai_ci"),
                    entry("collation_database", "utf8mb4_0900_bin"),
                    entry("collation_server", "utf8mb4_0900_bin")
            );
        }
    }

character_set_connectionutf8mb4になっていますが、MySQL Connector/J 8.0.25以前の場合はサーバー側のデフォルトの
Character Setが使われることになりますがこれはutf8mb4にしていますし、今回使用しているMySQL Connector/Jは8.0.28(8.0.26以降)
なのでどちらにしろutf8mb4です。

collation_connectionutf8mb4のデフォルトのCollationである、utf8mb4_0900_ai_ciですね。

characterEncodingutf8mb4を指定してみます。

    @Test
    public void utf8mb4CharacterEncoding() throws SQLException {
        String url = "jdbc:mysql://172.17.0.2:3306/practice?" +
                "characterEncoding=utf8mb4";
        String username = "kazuhira";
        String password = "password";

        assertThatThrownBy(() -> DriverManager.getConnection(url, username, password))
                .isInstanceOf(SQLException.class)
                .hasMessage("Unsupported character encoding 'utf8mb4'");
    }

これは、例外がスローされます。JavaCharsetとしては指定できないからでしょうか。

UTF-8を指定した場合は、utf8mb4になっていますね。

    @Test
    public void utf8CharacterEncoding() throws SQLException {
        String url = "jdbc:mysql://172.17.0.2:3306/practice?" +
                "characterEncoding=UTF-8";
        String username = "kazuhira";
        String password = "password";

        try (Connection conn = DriverManager.getConnection(url, username, password)) {
            Map<String, String> characterSets = collectCharacterSets(conn);
            Map<String, String> collations = collectCollations(conn);

            assertThat(characterSets).containsExactly(
                    entry("character_set_connection", "utf8mb4"),
                    entry("character_set_client", "utf8mb4"),
                    entry("character_set_database", "utf8mb4"),
                    entry("character_set_filesystem", "binary"),
                    entry("character_set_results", ""),
                    entry("character_set_server", "utf8mb4"),
                    entry("character_set_system", "utf8mb3")
            );

            assertThat(collations).containsExactly(
                    entry("collation_connection", "utf8mb4_0900_ai_ci"),
                    entry("collation_database", "utf8mb4_0900_bin"),
                    entry("collation_server", "utf8mb4_0900_bin")
            );
        }
    }

今では使うことはないと思いますが、試しにWindows-31Jにしてみます。

    @Test
    public void windows31jCharacterEncoding() throws SQLException {
        String url = "jdbc:mysql://172.17.0.2:3306/practice?" +
                "characterEncoding=Windows-31J";
        String username = "kazuhira";
        String password = "password";

        try (Connection conn = DriverManager.getConnection(url, username, password)) {
            Map<String, String> characterSets = collectCharacterSets(conn);
            Map<String, String> collations = collectCollations(conn);

            assertThat(characterSets).containsExactly(
                    entry("character_set_connection", "cp932"),  // changed
                    entry("character_set_client", "cp932"),  // changed
                    entry("character_set_database", "utf8mb4"),
                    entry("character_set_filesystem", "binary"),
                    entry("character_set_results", ""),
                    entry("character_set_server", "utf8mb4"),
                    entry("character_set_system", "utf8mb3")
            );

            assertThat(collations).containsExactly(
                    entry("collation_connection", "cp932_japanese_ci"),  // changed
                    entry("collation_database", "utf8mb4_0900_bin"),
                    entry("collation_server", "utf8mb4_0900_bin")
            );
        }
    }

すると、character_set_connectioncharacter_set_clientcp932に、collation_connectioncp932_japanese_ciに変化しました。

というわけで、ドキュメントに書かれているとおり、characterEncodingJavaエンコーディングで指定するのが正しいみたいですね。

characterEncodingutf8mb4のようなJavaCharsetとしては無効な値を指定すると例外がスローされるのは、String#getBytes
確認しているからのようです。

https://github.com/mysql/mysql-connector-j/blob/8.0.28/src/main/user-impl/java/com/mysql/cj/jdbc/JdbcPropertySetImpl.java#L61-L67

https://github.com/mysql/mysql-connector-j/blob/8.0.28/src/main/core-api/java/com/mysql/cj/util/StringUtils.java#L229

また、UTF-8utf8mb4のようになるのは、MySQL Connector/Jの中でJavaCharsetとして有効なエンコーディング
MySQLのCharacter Setに対する変換表を持っているからみたいですね。

https://github.com/mysql/mysql-connector-j/blob/8.0.28/src/main/core-impl/java/com/mysql/cj/NativeCharsetSettings.java#L704

https://github.com/mysql/mysql-connector-j/blob/8.0.28/src/main/core-api/java/com/mysql/cj/CharsetMapping.java#L113-L170

UTF-8の場合は、こちら。対応するのが2つありますが、最終的に選択されるのはutf8mb4になります。

                new MysqlCharset(MYSQL_CHARSET_NAME_utf8, 3, 0, new String[] { "UTF-8" }),
                new MysqlCharset(MYSQL_CHARSET_NAME_utf8mb4, 4, 1, new String[] { "UTF-8" }), // "UTF-8 = *> 5.5.2 utf8mb4"

これで、characterEncodingについての挙動はわかりました。

characterSetResultsを確認してみる

次は、characterSetResultsを確認してみます。

characterSetResultsを指定しない場合のcharacter_set_resultsは、未設定でした。

    @Test
    public void nonSettings() throws SQLException {
        String url = "jdbc:mysql://172.17.0.2:3306/practice";
        String username = "kazuhira";
        String password = "password";

        try (Connection conn = DriverManager.getConnection(url, username, password)) {
            Map<String, String> characterSets = collectCharacterSets(conn);
            Map<String, String> collations = collectCollations(conn);

            assertThat(characterSets).containsExactly(
                    entry("character_set_connection", "utf8mb4"),
                    entry("character_set_client", "utf8mb4"),
                    entry("character_set_database", "utf8mb4"),
                    entry("character_set_filesystem", "binary"),
                    entry("character_set_results", ""),
                    entry("character_set_server", "utf8mb4"),
                    entry("character_set_system", "utf8mb3")
            );

            assertThat(collations).containsExactly(
                    entry("collation_connection", "utf8mb4_0900_ai_ci"),
                    entry("collation_database", "utf8mb4_0900_bin"),
                    entry("collation_server", "utf8mb4_0900_bin")
            );
        }
    }

utf8mb4を指定してみます。

    @Test
    public void utf8mb4CharacterSetResults() throws SQLException {
        String url = "jdbc:mysql://172.17.0.2:3306/practice?" +
                "characterSetResults=utf8mb4";
        String username = "kazuhira";
        String password = "password";

        try (Connection conn = DriverManager.getConnection(url, username, password)) {
            Map<String, String> characterSets = collectCharacterSets(conn);
            Map<String, String> collations = collectCollations(conn);

            assertThat(characterSets).containsExactly(
                    entry("character_set_connection", "utf8mb4"),
                    entry("character_set_client", "utf8mb4"),
                    entry("character_set_database", "utf8mb4"),
                    entry("character_set_filesystem", "binary"),
                    entry("character_set_results", "utf8mb4"),  // changed
                    entry("character_set_server", "utf8mb4"),
                    entry("character_set_system", "utf8mb3")
            );

            assertThat(collations).containsExactly(
                    entry("collation_connection", "utf8mb4_0900_ai_ci"),
                    entry("collation_database", "utf8mb4_0900_bin"),
                    entry("collation_server", "utf8mb4_0900_bin")
            );
        }
    }

すると、こちらは通ります。character_set_resultsutf8mb4になりました。

では、UTF-8を指定してみましょう。

    @Test
    public void utf8CharacterSetResults() throws SQLException {
        String url = "jdbc:mysql://172.17.0.2:3306/practice?" +
                "characterSetResults=UTF-8";
        String username = "kazuhira";
        String password = "password";

        try (Connection conn = DriverManager.getConnection(url, username, password)) {
            Map<String, String> characterSets = collectCharacterSets(conn);
            Map<String, String> collations = collectCollations(conn);

            assertThat(characterSets).containsExactly(
                    entry("character_set_connection", "utf8mb4"),
                    entry("character_set_client", "utf8mb4"),
                    entry("character_set_database", "utf8mb4"),
                    entry("character_set_filesystem", "binary"),
                    entry("character_set_results", "utf8mb4"),  // changed
                    entry("character_set_server", "utf8mb4"),
                    entry("character_set_system", "utf8mb3")
            );

            assertThat(collations).containsExactly(
                    entry("collation_connection", "utf8mb4_0900_ai_ci"),
                    entry("collation_database", "utf8mb4_0900_bin"),
                    entry("collation_server", "utf8mb4_0900_bin")
            );
        }
    }

これも通ります。そして、こちらもcharacter_set_resultsutf8mb4になっています。

Windows-31Jを指定すると、cp932になっています。

    @Test
    public void windows31jCharacterSetResults() throws SQLException {
        String url = "jdbc:mysql://172.17.0.2:3306/practice?" +
                "characterSetResults=Windows-31J";
        String username = "kazuhira";
        String password = "password";

        try (Connection conn = DriverManager.getConnection(url, username, password)) {
            Map<String, String> characterSets = collectCharacterSets(conn);
            Map<String, String> collations = collectCollations(conn);

            assertThat(characterSets).containsExactly(
                    entry("character_set_connection", "utf8mb4"),
                    entry("character_set_client", "utf8mb4"),
                    entry("character_set_database", "utf8mb4"),
                    entry("character_set_filesystem", "binary"),
                    entry("character_set_results", "cp932"),  // changed
                    entry("character_set_server", "utf8mb4"),
                    entry("character_set_system", "utf8mb3")
            );

            assertThat(collations).containsExactly(
                    entry("collation_connection", "utf8mb4_0900_ai_ci"),
                    entry("collation_database", "utf8mb4_0900_bin"),
                    entry("collation_server", "utf8mb4_0900_bin")
            );
        }
    }

さて、どうなっているんでしょう?

こちらもやはり、JavaCharsetMySQLのCharacter Setに変換しようとするみたいです。

https://github.com/mysql/mysql-connector-j/blob/8.0.28/src/main/core-impl/java/com/mysql/cj/NativeCharsetSettings.java#L397-L398

https://github.com/mysql/mysql-connector-j/blob/8.0.28/src/main/core-impl/java/com/mysql/cj/NativeCharsetSettings.java#L704

反対に、MySQLのCharacter Setとしか解釈できない値を指定した場合は、1度JavaCharsetとして有効なエンコーディング
変換するようです。

https://github.com/mysql/mysql-connector-j/blob/8.0.28/src/main/core-impl/java/com/mysql/cj/NativeCharsetSettings.java#L193

https://github.com/mysql/mysql-connector-j/blob/8.0.28/src/main/core-impl/java/com/mysql/cj/NativeCharsetSettings.java#L217-L223

https://github.com/mysql/mysql-connector-j/blob/8.0.28/src/main/core-api/java/com/mysql/cj/CharsetMapping.java#L595

https://github.com/mysql/mysql-connector-j/blob/8.0.28/src/main/core-api/java/com/mysql/cj/CharsetMapping.java#L600

この時に使う変換表も、characterEncodingの時と同じですね。

https://github.com/mysql/mysql-connector-j/blob/8.0.28/src/main/core-api/java/com/mysql/cj/CharsetMapping.java#L113-L170

connectionCollationを確認してみる

最後に、connectionCollationを確認してみます。

なにも指定していない時は、collation_connectionはデフォルトのCharacter Setであるutf8mb4のデフォルトのCollation、utf8mb4_0900_ai_ci
なっていました。

    @Test
    public void nonSettings() throws SQLException {
        String url = "jdbc:mysql://172.17.0.2:3306/practice";
        String username = "kazuhira";
        String password = "password";

        try (Connection conn = DriverManager.getConnection(url, username, password)) {
            Map<String, String> characterSets = collectCharacterSets(conn);
            Map<String, String> collations = collectCollations(conn);

            assertThat(characterSets).containsExactly(
                    entry("character_set_connection", "utf8mb4"),
                    entry("character_set_client", "utf8mb4"),
                    entry("character_set_database", "utf8mb4"),
                    entry("character_set_filesystem", "binary"),
                    entry("character_set_results", ""),
                    entry("character_set_server", "utf8mb4"),
                    entry("character_set_system", "utf8mb3")
            );

            assertThat(collations).containsExactly(
                    entry("collation_connection", "utf8mb4_0900_ai_ci"),
                    entry("collation_database", "utf8mb4_0900_bin"),
                    entry("collation_server", "utf8mb4_0900_bin")
            );
        }
    }

connectionCollationutf8mb4_0900_binを指定してみます。

    @Test
    public void utf8mb4_0900_bin_connectionCollation() throws SQLException {
        String url = "jdbc:mysql://172.17.0.2:3306/practice?" +
                "connectionCollation=utf8mb4_0900_bin";
        String username = "kazuhira";
        String password = "password";

        try (Connection conn = DriverManager.getConnection(url, username, password)) {
            Map<String, String> characterSets = collectCharacterSets(conn);
            Map<String, String> collations = collectCollations(conn);

            assertThat(characterSets).containsExactly(
                    entry("character_set_connection", "utf8mb4"),
                    entry("character_set_client", "utf8mb4"),
                    entry("character_set_database", "utf8mb4"),
                    entry("character_set_filesystem", "binary"),
                    entry("character_set_results", ""),
                    entry("character_set_server", "utf8mb4"),
                    entry("character_set_system", "utf8mb3")
            );

            assertThat(collations).containsExactly(
                    entry("collation_connection", "utf8mb4_0900_bin"),  // changed
                    entry("collation_database", "utf8mb4_0900_bin"),
                    entry("collation_server", "utf8mb4_0900_bin")
            );
        }
    }

collation_connectionutf8mb4_0900_binになりました。

cp932_binを指定してみます。

    @Test
    public void cp932_bin_connectionCollation() throws SQLException {
        String url = "jdbc:mysql://172.17.0.2:3306/practice?" +
                "connectionCollation=cp932_bin";
        String username = "kazuhira";
        String password = "password";

        try (Connection conn = DriverManager.getConnection(url, username, password)) {
            Map<String, String> characterSets = collectCharacterSets(conn);
            Map<String, String> collations = collectCollations(conn);

            assertThat(characterSets).containsExactly(
                    entry("character_set_connection", "cp932"),  // changed
                    entry("character_set_client", "cp932"),  // changed
                    entry("character_set_database", "utf8mb4"),
                    entry("character_set_filesystem", "binary"),
                    entry("character_set_results", ""),
                    entry("character_set_server", "utf8mb4"),
                    entry("character_set_system", "utf8mb3")
            );

            assertThat(collations).containsExactly(
                    entry("collation_connection", "cp932_bin"),  // changed
                    entry("collation_database", "utf8mb4_0900_bin"),
                    entry("collation_server", "utf8mb4_0900_bin")
            );
        }
    }

こちらも反映されました。

最後に、characterEncodingにはUTF-8connectionCollationにはcp932_binと矛盾した内容を設定してみます。

    @Test
    public void utf8CharacterEncoding_cp932_bin_connectionCollation() throws SQLException {
        String url = "jdbc:mysql://172.17.0.2:3306/practice?" +
                "characterEncoding=UTF-8&connectionCollation=cp932_bin";
        String username = "kazuhira";
        String password = "password";

        try (Connection conn = DriverManager.getConnection(url, username, password)) {
            Map<String, String> characterSets = collectCharacterSets(conn);
            Map<String, String> collations = collectCollations(conn);

            assertThat(characterSets).containsExactly(
                    entry("character_set_connection", "cp932"),  // changed & override
                    entry("character_set_client", "cp932"),  // changed & override
                    entry("character_set_database", "utf8mb4"),
                    entry("character_set_filesystem", "binary"),
                    entry("character_set_results", ""),
                    entry("character_set_server", "utf8mb4"),
                    entry("character_set_system", "utf8mb3")
            );

            assertThat(collations).containsExactly(
                    entry("collation_connection", "cp932_bin"),  // changed
                    entry("collation_database", "utf8mb4_0900_bin"),
                    entry("collation_server", "utf8mb4_0900_bin")
            );
        }
    }

これは、ドキュメントに書かれている通り、characterEncodingの値がconnectionCollationで指定したCharacter Setで上書きされます。
今回は、character_set_connectioncharacter_set_clientcp932になりましたね。

このようなケースは、以下の部分でCharacter Setの値が補正されます。

https://github.com/mysql/mysql-connector-j/blob/8.0.28/src/main/core-impl/java/com/mysql/cj/NativeCharsetSettings.java#L316

https://github.com/mysql/mysql-connector-j/blob/8.0.28/src/main/core-impl/java/com/mysql/cj/NativeCharsetSettings.java#L659-L671

こんな感じで、実際の挙動が確認できました。

まとめ

今回は、MySQL Connector/Jの設定を見ていて、Character Set/Character Set Results/Connection Collationに関する項目と、
そもそもこれらの意味がちゃんとわかっていなかったなと思ってちょっと調べてみました。

ちゃんとドキュメントを見てみると、心配しすぎだったかな、という気がしないでもないですが。いつももやもやしていたので、
この機会に見ておいて意味はあったかなと思います。

ちなみに、このあたりを見ていると、これらの変数で指定した値は最終的にはSET NAMESSET character_set_resultsとして
実行されるようですね。

https://github.com/mysql/mysql-connector-j/blob/8.0.28/src/main/core-impl/java/com/mysql/cj/NativeCharsetSettings.java#L360

https://github.com/mysql/mysql-connector-j/blob/8.0.28/src/main/core-impl/java/com/mysql/cj/NativeCharsetSettings.java#L388

https://github.com/mysql/mysql-connector-j/blob/8.0.28/src/main/core-impl/java/com/mysql/cj/NativeCharsetSettings.java#L406

最後に、今回作成したテストコードの全体を載せておきます。

src/test/java/org/littlewings/mysql/ConnectorCharacterSetTest.java

package org.littlewings.mysql;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.*;

public class ConnectorCharacterSetTest {
    List<String> characterSetVariables = List.of(
            "character_set_connection",
            "character_set_client",
            "character_set_database",
            "character_set_filesystem",
            "character_set_results",
            "character_set_server",
            "character_set_system"
    );

    List<String> collationVariables = List.of(
            "collation_connection",
            "collation_database",
            "collation_server"
    );

    private Map<String, String> collectCharacterSets(Connection conn) throws SQLException {
        Map<String, String> characterSets = new LinkedHashMap<>();

        for (String characterSetVariable : characterSetVariables) {
            try (PreparedStatement ps = conn.prepareStatement("show variables where variable_name = ?")) {
                ps.setString(1, characterSetVariable);

                try (ResultSet rs = ps.executeQuery()) {
                    while (rs.next()) {
                        characterSets.put(rs.getString(1), rs.getString(2));
                    }
                }
            }
        }

        return characterSets;
    }

    private Map<String, String> collectCollations(Connection conn) throws SQLException {
        Map<String, String> collations = new LinkedHashMap<>();

        for (String collationVariable : collationVariables) {
            try (PreparedStatement ps = conn.prepareStatement("show variables where variable_name = ?")) {
                ps.setString(1, collationVariable);

                try (ResultSet rs = ps.executeQuery()) {
                    while (rs.next()) {
                        collations.put(rs.getString(1), rs.getString(2));
                    }
                }
            }
        }

        return collations;
    }


    @Test
    public void nonSettings() throws SQLException {
        String url = "jdbc:mysql://172.17.0.2:3306/practice";
        String username = "kazuhira";
        String password = "password";

        try (Connection conn = DriverManager.getConnection(url, username, password)) {
            Map<String, String> characterSets = collectCharacterSets(conn);
            Map<String, String> collations = collectCollations(conn);

            assertThat(characterSets).containsExactly(
                    entry("character_set_connection", "utf8mb4"),
                    entry("character_set_client", "utf8mb4"),
                    entry("character_set_database", "utf8mb4"),
                    entry("character_set_filesystem", "binary"),
                    entry("character_set_results", ""),
                    entry("character_set_server", "utf8mb4"),
                    entry("character_set_system", "utf8mb3")
            );

            assertThat(collations).containsExactly(
                    entry("collation_connection", "utf8mb4_0900_ai_ci"),
                    entry("collation_database", "utf8mb4_0900_bin"),
                    entry("collation_server", "utf8mb4_0900_bin")
            );
        }
    }

    @Test
    public void utf8mb4CharacterEncoding() throws SQLException {
        String url = "jdbc:mysql://172.17.0.2:3306/practice?" +
                "characterEncoding=utf8mb4";
        String username = "kazuhira";
        String password = "password";

        assertThatThrownBy(() -> DriverManager.getConnection(url, username, password))
                .isInstanceOf(SQLException.class)
                .hasMessage("Unsupported character encoding 'utf8mb4'");
    }

    @Test
    public void utf8CharacterEncoding() throws SQLException {
        String url = "jdbc:mysql://172.17.0.2:3306/practice?" +
                "characterEncoding=UTF-8";
        String username = "kazuhira";
        String password = "password";

        try (Connection conn = DriverManager.getConnection(url, username, password)) {
            Map<String, String> characterSets = collectCharacterSets(conn);
            Map<String, String> collations = collectCollations(conn);

            assertThat(characterSets).containsExactly(
                    entry("character_set_connection", "utf8mb4"),
                    entry("character_set_client", "utf8mb4"),
                    entry("character_set_database", "utf8mb4"),
                    entry("character_set_filesystem", "binary"),
                    entry("character_set_results", ""),
                    entry("character_set_server", "utf8mb4"),
                    entry("character_set_system", "utf8mb3")
            );

            assertThat(collations).containsExactly(
                    entry("collation_connection", "utf8mb4_0900_ai_ci"),
                    entry("collation_database", "utf8mb4_0900_bin"),
                    entry("collation_server", "utf8mb4_0900_bin")
            );
        }
    }

    @Test
    public void windows31jCharacterEncoding() throws SQLException {
        String url = "jdbc:mysql://172.17.0.2:3306/practice?" +
                "characterEncoding=Windows-31J";
        String username = "kazuhira";
        String password = "password";

        try (Connection conn = DriverManager.getConnection(url, username, password)) {
            Map<String, String> characterSets = collectCharacterSets(conn);
            Map<String, String> collations = collectCollations(conn);

            assertThat(characterSets).containsExactly(
                    entry("character_set_connection", "cp932"),  // changed
                    entry("character_set_client", "cp932"),  // changed
                    entry("character_set_database", "utf8mb4"),
                    entry("character_set_filesystem", "binary"),
                    entry("character_set_results", ""),
                    entry("character_set_server", "utf8mb4"),
                    entry("character_set_system", "utf8mb3")
            );

            assertThat(collations).containsExactly(
                    entry("collation_connection", "cp932_japanese_ci"),  // changed
                    entry("collation_database", "utf8mb4_0900_bin"),
                    entry("collation_server", "utf8mb4_0900_bin")
            );
        }
    }

    @Test
    public void utf8mb4CharacterSetResults() throws SQLException {
        String url = "jdbc:mysql://172.17.0.2:3306/practice?" +
                "characterSetResults=utf8mb4";
        String username = "kazuhira";
        String password = "password";

        try (Connection conn = DriverManager.getConnection(url, username, password)) {
            Map<String, String> characterSets = collectCharacterSets(conn);
            Map<String, String> collations = collectCollations(conn);

            assertThat(characterSets).containsExactly(
                    entry("character_set_connection", "utf8mb4"),
                    entry("character_set_client", "utf8mb4"),
                    entry("character_set_database", "utf8mb4"),
                    entry("character_set_filesystem", "binary"),
                    entry("character_set_results", "utf8mb4"),  // changed
                    entry("character_set_server", "utf8mb4"),
                    entry("character_set_system", "utf8mb3")
            );

            assertThat(collations).containsExactly(
                    entry("collation_connection", "utf8mb4_0900_ai_ci"),
                    entry("collation_database", "utf8mb4_0900_bin"),
                    entry("collation_server", "utf8mb4_0900_bin")
            );
        }
    }

    @Test
    public void utf8CharacterSetResults() throws SQLException {
        String url = "jdbc:mysql://172.17.0.2:3306/practice?" +
                "characterSetResults=UTF-8";
        String username = "kazuhira";
        String password = "password";

        try (Connection conn = DriverManager.getConnection(url, username, password)) {
            Map<String, String> characterSets = collectCharacterSets(conn);
            Map<String, String> collations = collectCollations(conn);

            assertThat(characterSets).containsExactly(
                    entry("character_set_connection", "utf8mb4"),
                    entry("character_set_client", "utf8mb4"),
                    entry("character_set_database", "utf8mb4"),
                    entry("character_set_filesystem", "binary"),
                    entry("character_set_results", "utf8mb4"),  // changed
                    entry("character_set_server", "utf8mb4"),
                    entry("character_set_system", "utf8mb3")
            );

            assertThat(collations).containsExactly(
                    entry("collation_connection", "utf8mb4_0900_ai_ci"),
                    entry("collation_database", "utf8mb4_0900_bin"),
                    entry("collation_server", "utf8mb4_0900_bin")
            );
        }
    }

    @Test
    public void windows31jCharacterSetResults() throws SQLException {
        String url = "jdbc:mysql://172.17.0.2:3306/practice?" +
                "characterSetResults=Windows-31J";
        String username = "kazuhira";
        String password = "password";

        try (Connection conn = DriverManager.getConnection(url, username, password)) {
            Map<String, String> characterSets = collectCharacterSets(conn);
            Map<String, String> collations = collectCollations(conn);

            assertThat(characterSets).containsExactly(
                    entry("character_set_connection", "utf8mb4"),
                    entry("character_set_client", "utf8mb4"),
                    entry("character_set_database", "utf8mb4"),
                    entry("character_set_filesystem", "binary"),
                    entry("character_set_results", "cp932"),  // changed
                    entry("character_set_server", "utf8mb4"),
                    entry("character_set_system", "utf8mb3")
            );

            assertThat(collations).containsExactly(
                    entry("collation_connection", "utf8mb4_0900_ai_ci"),
                    entry("collation_database", "utf8mb4_0900_bin"),
                    entry("collation_server", "utf8mb4_0900_bin")
            );
        }
    }

    @Test
    public void utf8mb4_0900_bin_connectionCollation() throws SQLException {
        String url = "jdbc:mysql://172.17.0.2:3306/practice?" +
                "connectionCollation=utf8mb4_0900_bin";
        String username = "kazuhira";
        String password = "password";

        try (Connection conn = DriverManager.getConnection(url, username, password)) {
            Map<String, String> characterSets = collectCharacterSets(conn);
            Map<String, String> collations = collectCollations(conn);

            assertThat(characterSets).containsExactly(
                    entry("character_set_connection", "utf8mb4"),
                    entry("character_set_client", "utf8mb4"),
                    entry("character_set_database", "utf8mb4"),
                    entry("character_set_filesystem", "binary"),
                    entry("character_set_results", ""),
                    entry("character_set_server", "utf8mb4"),
                    entry("character_set_system", "utf8mb3")
            );

            assertThat(collations).containsExactly(
                    entry("collation_connection", "utf8mb4_0900_bin"),  // changed
                    entry("collation_database", "utf8mb4_0900_bin"),
                    entry("collation_server", "utf8mb4_0900_bin")
            );
        }
    }

    @Test
    public void cp932_bin_connectionCollation() throws SQLException {
        String url = "jdbc:mysql://172.17.0.2:3306/practice?" +
                "connectionCollation=cp932_bin";
        String username = "kazuhira";
        String password = "password";

        try (Connection conn = DriverManager.getConnection(url, username, password)) {
            Map<String, String> characterSets = collectCharacterSets(conn);
            Map<String, String> collations = collectCollations(conn);

            assertThat(characterSets).containsExactly(
                    entry("character_set_connection", "cp932"),  // changed
                    entry("character_set_client", "cp932"),  // changed
                    entry("character_set_database", "utf8mb4"),
                    entry("character_set_filesystem", "binary"),
                    entry("character_set_results", ""),
                    entry("character_set_server", "utf8mb4"),
                    entry("character_set_system", "utf8mb3")
            );

            assertThat(collations).containsExactly(
                    entry("collation_connection", "cp932_bin"),  // changed
                    entry("collation_database", "utf8mb4_0900_bin"),
                    entry("collation_server", "utf8mb4_0900_bin")
            );
        }
    }

    @Test
    public void utf8CharacterEncoding_cp932_bin_connectionCollation() throws SQLException {
        String url = "jdbc:mysql://172.17.0.2:3306/practice?" +
                "characterEncoding=UTF-8&connectionCollation=cp932_bin";
        String username = "kazuhira";
        String password = "password";

        try (Connection conn = DriverManager.getConnection(url, username, password)) {
            Map<String, String> characterSets = collectCharacterSets(conn);
            Map<String, String> collations = collectCollations(conn);

            assertThat(characterSets).containsExactly(
                    entry("character_set_connection", "cp932"),  // changed & override
                    entry("character_set_client", "cp932"),  // changed & override
                    entry("character_set_database", "utf8mb4"),
                    entry("character_set_filesystem", "binary"),
                    entry("character_set_results", ""),
                    entry("character_set_server", "utf8mb4"),
                    entry("character_set_system", "utf8mb3")
            );

            assertThat(collations).containsExactly(
                    entry("collation_connection", "cp932_bin"),  // changed
                    entry("collation_database", "utf8mb4_0900_bin"),
                    entry("collation_server", "utf8mb4_0900_bin")
            );
        }
    }
}