Spring BootからMySQLに繋ごうとして、application.ymlに何を書けばいいのか迷っていませんか?

接続自体はできたのに、保存した日時が9時間ずれる、日本語が「???」になる、起動した瞬間に Public Key Retrieval is not allowed で落ちる、という相談もよく受けます。

この記事では、Docker ComposeでMySQL 8を起動するところから、依存追加、application.ymlのdatasource設定、疎通確認までを一気通貫でやります。そのうえでJDBC URLパラメータの意味と推奨値、定番エラー、タイムゾーンと文字コードのずれの直し方を整理します。

エラーメッセージで検索してたどり着いた方は、「定番エラーと対処法」の節から読んでもらって大丈夫です。

この記事のゴールと前提環境

前提は次のとおりです。

  • Spring Boot 3.x(Java 21)
  • MySQL 8.0 または 8.4
  • JDBCドライバは com.mysql:mysql-connector-j

ドライバの座標は、昔の mysql:mysql-connector-java から com.mysql:mysql-connector-j に変わっています。Spring Boot 3.x は旧座標をバージョン管理していないので、バージョン指定なしで書くと依存解決に失敗します。古い記事からコピーするときは com.mysql:mysql-connector-j に読み替えてください。

ゴールは「MySQL起動 → 依存追加 → application.yml → 疎通確認」の4ステップです。datasource設定はJPAでもMyBatisでも共通なので、どちらを使う予定でもそのまま読み進められます。

Docker ComposeでMySQL 8を起動する

MySQLのインストール方法はいろいろありますが、ここではDocker Composeに絞ります。文字コードとタイムゾーンを最初から固定しておくのがポイントです。

# compose.yaml
services:
  mysql:
    image: mysql:8.4
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: appdb
      MYSQL_USER: app
      MYSQL_PASSWORD: secret
      TZ: Asia/Tokyo
    command:
      - --character-set-server=utf8mb4
      - --collation-server=utf8mb4_0900_ai_ci
    ports:
      - "3306:3306"
    volumes:
      - mysql-data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-proot"]
      interval: 5s
      timeout: 3s
      retries: 10

volumes:
  mysql-data:

MYSQL_DATABASEMYSQL_USER を指定しておくと、初回起動時にデータベースと、そのDBへの権限を持つユーザーが自動で作られます。named volumeを付けているので、コンテナを消してもデータは残ります。TZ が効くのは公式の mysql イメージに tzdata が入っているためで、軽量ビルドのイメージでは効かないことがあります。

docker compose up -d のあと、一度だけ中身を確認しておきましょう。

docker compose exec mysql mysql -uroot -proot \
  -e "SHOW VARIABLES LIKE 'character_set_server'; SELECT @@global.time_zone, @@system_time_zone;"

utf8mb4JST が出ていればOKです。ここを最初に見ておくと、あとで「化けた」「ずれた」となったときにサーバ側を疑わなくて済みます。

Spring Bootアプリ自体もコンテナで動かしたい場合は Spring BootアプリをDockerコンテナで動かす方法 が参考になります。

依存関係を追加する(Gradle / Maven)

ドライバは runtimeOnly で十分です。アプリのコードはJDBC APIにしか依存しないので、ドライバの実装クラスをコンパイル時に見せる必要がありません。バージョンはSpring Bootの依存管理に任せます。

// build.gradle
dependencies {
    // JPAを使うなら
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    // MyBatisを使うならこちら(どちらか一方でOK)
    // Boot の依存管理外なので、Maven Central で最新の 3.0.x を確認して指定する
    // implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.4'

    runtimeOnly 'com.mysql:mysql-connector-j'
}

Mavenならこうです。

<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>

JPAとMyBatisのどちらを選んでも、次に書くdatasource設定は同じです。

application.ymlの最小構成

まず接続できる最小構成はこの3項目です。

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/appdb?connectionTimeZone=Asia/Tokyo&characterEncoding=UTF-8
    username: app
    password: ${DB_PASSWORD:secret}

driver-class-name は書いていません。Spring BootがURLの jdbc:mysql: から com.mysql.cj.jdbc.Driver を自動判別してくれるためです。明示しても害はありませんが、書かなくて動くものは書かない方が設定は減ります。

パスワードは ${DB_PASSWORD} の形で環境変数から読みます。${DB_PASSWORD:secret} のようにデフォルト値を付ける書き方は便利ですが、本番のYAMLに残すと事故のもとなので開発用に限定しましょう。

URLの後ろに付けたパラメータは、次の節で説明します。

JDBC URLパラメータの意味と推奨値

Connector/J 8.x でよく使うパラメータをまとめます。古い記事に出てくる useUnicode=trueserverTimezone は、今は不要か別名扱いです。

パラメータ意味推奨値用途
connectionTimeZoneJDBCが日時を変換する基準のタイムゾーンAsia/TokyoUTC共通
forceConnectionTimeZoneToSessionセッションの time_zone も同じ値に揃えるtrue(TIMESTAMP型を使うなら)共通
characterEncoding接続の文字コード。UTF-8utf8mb4 として扱われるUTF-8共通
sslModeSSLの扱い。useSSL の後継開発 DISABLED / 本番 REQUIRED 以上両方
allowPublicKeyRetrieval非SSL時にサーバ公開鍵の取得を許可する開発のみ true開発
createDatabaseIfNotExistDBがなければ作成する(CREATE権限が必要)開発のみ true開発

serverTimezone は Connector/J 8.0.23 以降 connectionTimeZone の別名として残っているだけなので、新しく書くなら connectionTimeZone にしましょう。useSSL も同様に sslMode へ置き換わっていて、両方書くと sslMode が優先されます。

createDatabaseIfNotExist は接続ユーザーにグローバルな CREATE 権限が必要です。MYSQL_USER で作った app ユーザーは MYSQL_DATABASE に対する権限しか持たないので、別名のDBを作ろうとすると Access denied ... to database になります。

開発用と本番用の完成形を並べるとこうなります。

spring:
  datasource:
    # 開発用
    url: jdbc:mysql://localhost:3306/appdb?connectionTimeZone=Asia/Tokyo&characterEncoding=UTF-8&sslMode=DISABLED&allowPublicKeyRetrieval=true&createDatabaseIfNotExist=true
    # 本番用(コメントを外して差し替える)
    # url: jdbc:mysql://db.example.internal:3306/appdb?connectionTimeZone=Asia/Tokyo&characterEncoding=UTF-8&sslMode=REQUIRED

本番では allowPublicKeyRetrievalcreateDatabaseIfNotExist を外す、と覚えておけば十分です。

疎通確認(SELECT 1とテーブル作成)

まずはJPAにもMyBatisにも依存しない方法で確認しましょう。JdbcTemplate はどちらのスターターにも含まれているので、そのまま使えます。

@Component
public class ConnectionCheckRunner implements CommandLineRunner {

    private static final Logger log = LoggerFactory.getLogger(ConnectionCheckRunner.class);

    private final JdbcTemplate jdbcTemplate;

    public ConnectionCheckRunner(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Override
    public void run(String... args) {
        Integer one = jdbcTemplate.queryForObject("SELECT 1", Integer.class);
        String tz = jdbcTemplate.queryForObject("SELECT @@session.time_zone", String.class);
        log.info("SELECT 1 = {}, session time_zone = {}", one, tz);
    }
}

起動ログに HikariPool-1 - Start completed. が出ていれば、接続そのものは成功しています。そのあとに SELECT 1 = 1 が出れば疎通完了です。JdbcTemplate の使い方は Spring BootのJdbcTemplateで素のSQLを実行する方法 にまとめています。

JPAとMyBatisそれぞれの最小追加設定はこれだけです。

spring:
  jpa:
    hibernate:
      ddl-auto: update   # 開発用。本番では validate か none
    show-sql: true

# MyBatisの場合はこちら
mybatis:
  mapper-locations: classpath:mapper/*.xml
  configuration:
    map-underscore-to-camel-case: true

ddl-auto: update にしておけば、@Entity を1つ書いて起動するだけでテーブルが作られます。本番で update のままにしておくと、エンティティの変更が勝手にスキーマへ反映されて怖いので、validatenone にして後述のFlywayに任せましょう。Mapperの書き方は Spring BootでMyBatisを使う方法 を参照してください。

定番エラーと対処法

エラー文で検索してきた方のために、原因と修正を短くまとめます。

Public Key Retrieval is not allowed

原因は、MySQL 8 の既定認証 caching_sha2_password と非SSL接続(sslMode=DISABLED)の組み合わせです。開発なら allowPublicKeyRetrieval=true を付けるか、SSLを有効にすれば直ります。

The server time zone value 'JST' is unrecognized or represents more than one time zone.

原因は、サーバの system_time_zoneJST のような略称で、Connector/Jが解釈できないことです。主に 8.0.22 以前のドライバか serverTimezone 未指定の旧構成で出ます。8.0.23 以降は connectionTimeZone の既定が LOCAL(JVMのタイムゾーン)で、サーバ側を参照するのは connectionTimeZone=SERVER を指定したときだけなので、既定では出ません。古い記事の依存をコピーしていないか確認したうえで、connectionTimeZone=Asia/Tokyo(旧ドライバなら serverTimezone)を明示しておくのが確実です。

Communications link failure

原因は、ホスト名・ポートの誤りか、MySQLコンテナが起動しきっていないことです。Compose内のアプリからはサービス名 mysql で繋ぎ、depends_oncondition: service_healthy を付けて起動を待たせましょう。

Access denied for user 'app'@'172.18.0.1'

原因は、パスワード違いか、接続元ホストが許可されていないことです。MYSQL_USER のユーザーは 'app'@'%' ですが、手動で 'app'@'localhost' と作るとコンテナ外からは弾かれます。

Unknown database 'appdb'

原因は、DBが未作成なことです。MYSQL_DATABASE で作るか、CREATE権限のあるユーザーで createDatabaseIfNotExist=true を付けてください。

日時がずれる原因と対処

接続できたのに保存した日時が9時間ずれる、という相談は本当に多いです。関係する層が3つあるのを押さえましょう。

  1. JVMのデフォルトタイムゾーン(user.timezone
  2. MySQLサーバとセッションの time_zone
  3. Connector/Jの変換基準(connectionTimeZone

さらに、MySQLの TIMESTAMP 型はUTCで保存されてセッションの time_zone で変換されるのに対し、DATETIME 型は変換されずそのまま保存されます。「同じ値を入れたのに列の型で結果が違う」のはこれが原因です。

Java側の型は、DATETIME 列には変換のない LocalDateTime を、絶対時刻として扱いたい値は InstantTIMESTAMP 列に対応させるのが分かりやすいです。OffsetDateTime はオフセット付きで受け渡ししたいAPI境界向けで、MySQLにはオフセットを保持する型がないので保存時は TIMESTAMP 相当に変換される点に注意してください。JPAなら spring.jpa.properties.hibernate.jdbc.time_zone でHibernateがJDBCに渡すときの基準も固定できます。

JVM層は起動時の java -Duser.timezone=Asia/Tokyo -jar app.jar、コンテナなら環境変数 TZ=Asia/Tokyo で固定できます。迷ったらサーバ・JVM・接続の3つを同じタイムゾーンに揃えるか、すべてUTCに揃えるかのどちらかにしてください。ずれているときは SELECT @@session.time_zone, NOW() で現状を確認できます。確認クエリは次の節にまとめて載せておきます。

日本語が文字化けする原因と対処

文字化けも同じく、サーバ側と接続側の2か所を見ます。

サーバ側は character_set_server と、データベース・テーブルの CHARACTER SET / COLLATION です。Docker Composeの commandutf8mb4 を固定していれば、あとから作るDBとテーブルは自動でそれを引き継ぎます。接続側は characterEncoding=UTF-8 で、Connector/Jはこれを utf8mb4 にマッピングします。

注意したいのは、MySQLの utf8 が実は3バイトまでしか扱えない utf8mb3 の別名だということです。古いテーブルが utf8 で作られていると、絵文字を入れた瞬間に Incorrect string value で落ちます。既存テーブルは SHOW CREATE TABLE で確認し、utf8mb3 なら utf8mb4 に変換しておきましょう。

-- タイムゾーンの確認
SELECT @@global.time_zone, @@session.time_zone, @@system_time_zone, NOW();

-- 文字コードの確認と変換
SHOW VARIABLES LIKE 'character_set%';
SHOW CREATE TABLE users;
ALTER TABLE users CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;

HikariCPとの関係とmaxLifetime設定

ここまで一度もプールの設定をしていませんが、Spring Bootはデフォルトで HikariCP を使うので、すでにコネクションプール経由で接続しています。起動ログの HikariPool-1 がその証拠です。

MySQLで1つだけ気にしてほしいのが max-lifetime です。MySQLは wait_timeout(デフォルト28800秒)を過ぎたアイドル接続をサーバ側で切ります。HikariCP側の寿命がそれより長いと、貸し出し時の isValid() 検証に失敗して Failed to validate connection ... Possibly consider using a shorter maxLifetime value. という WARN ログが出続けます。クエリの実行中にサーバ側で切られた場合は Communications link failure になります。HikariCPのデフォルトは30分なので普段は問題ありませんが、サーバ側の wait_timeout を短くしている環境では必ず下回るように設定してください。

spring:
  datasource:
    hikari:
      max-lifetime: 570000   # wait_timeout=600 なら数十秒短くする
      maximum-pool-size: 10

connection-test-query: SELECT 1 を書いている設定をたまに見ますが、Connector/Jは JDBC4 の isValid() に対応しているので不要です。書くとむしろ検証が遅くなります。プールサイズの算出やActuatorでの監視は Spring BootのHikariCPコネクションプールを正しく設定・チューニングする方法 で詳しく扱っています。

Flywayでスキーマを管理する場合

ddl-auto: update は手軽ですが、本番のスキーマ管理には向きません。Flywayに任せる場合、MySQLでは flyway-core だけでは足りず flyway-mysql が必要です。これがないと起動時に Unsupported Database: MySQL で落ちます。

implementation 'org.flywaydb:flyway-core'
implementation 'org.flywaydb:flyway-mysql'

あわせて spring.jpa.hibernate.ddl-autovalidate にして、スキーマはFlywayが管理しHibernateは検証だけ、という役割分担にします。あとは src/main/resources/db/migration/V1__create_users.sql を1本置けば、起動時に自動で適用されます。MySQL 8.4 に対して Spring Boot が管理する Flyway が古いと Flyway upgrade recommended: MySQL 8.4 is newer than this version of Flyway と出ますが、失敗ではなく警告なので慌てなくて大丈夫です。バージョニングの運用や本番適用の流れは Spring BootでFlywayを使ったデータベースマイグレーション管理 にまとめてあります。

本番向けの設定(SSLとプロファイルの切り替え)

開発用のURLをそのまま本番に持ち込まないよう、プロファイルで分けておきましょう。

# application-dev.yml
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/appdb?connectionTimeZone=Asia/Tokyo&characterEncoding=UTF-8&sslMode=DISABLED&allowPublicKeyRetrieval=true&createDatabaseIfNotExist=true
  jpa:
    hibernate:
      ddl-auto: update

# application-prod.yml
spring:
  datasource:
    url: jdbc:mysql://${DB_HOST}:3306/appdb?connectionTimeZone=Asia/Tokyo&characterEncoding=UTF-8&sslMode=REQUIRED
    # 証明書まで検証する場合。CA証明書を入れたトラストストアが必要
    # url: jdbc:mysql://${DB_HOST}:3306/appdb?connectionTimeZone=Asia/Tokyo&characterEncoding=UTF-8&sslMode=VERIFY_CA&trustCertificateKeyStoreUrl=file:/etc/app/ca-truststore.p12&trustCertificateKeyStorePassword=${TRUSTSTORE_PASSWORD}
    password: ${DB_PASSWORD}
  jpa:
    hibernate:
      ddl-auto: validate

sslMode=REQUIRED は通信を暗号化しますが、サーバ証明書の検証まではしません。VERIFY_CAVERIFY_IDENTITY に上げるなら、サーバ証明書を発行したCA証明書をトラストストアに登録しておく必要があります。方法は trustCertificateKeyStoreUrl でPKCS12のトラストストアを指定するか、JVMの cacerts にCAを取り込んでおくかのどちらかです(fallbackToSystemTrustStore が既定で true なので、後者はURLに何も足さなくても参照されます)。RDSやCloud SQLのような私設CAで、登録なしに VERIFY_CA にすると PKIX path building failed で接続に失敗します。

本番のポイントは3つです。sslModeREQUIRED 以上にすること、allowPublicKeyRetrievalcreateDatabaseIfNotExist を外すこと、パスワードを環境変数やシークレット管理から注入してYAMLに書かないことです。プロファイルの切り替え方は Spring BootのProfileを使って環境によって違う設定を安全に切り替える方法 を参照してください。

まとめ

Spring BootとMySQLの接続でつまずくポイントは、だいたい「繋がらない」「ずれる」「化ける」の3つに集約されます。繋がらないのはURLパラメータ(allowPublicKeyRetrievalsslMode)、ずれるのはJVM・サーバ・接続の3層のタイムゾーン、化けるのは utf8mb4 の統一で解決できます。

開発用URLには sslMode=DISABLED&allowPublicKeyRetrieval=true&createDatabaseIfNotExist=true を付けて楽をし、本番では sslMode=REQUIRED 以上にしてそれらを外す。この切り替えさえ守れば、接続まわりで困ることはほぼなくなるはずです。

接続ができたら、次は REST APIのCRUD実装 や、初期データを入れるための data.sql・schema.sqlの使い方 に進んでみてください。