Micronaut で MySQL へコネクションプール有りの接続をする設定方法

概要

Micronaut フレームワークを利用して、MySQL サーバへの接続設定を行う。
データベースへの接続は、コネクションプールを利用する。
コネクションプールには、HikariCP ライブラリを利用する。
コネクションプール設定は、複数定義する。
設定は、環境変数やシステムプロパティなどで、上書きできるようにする。

環境

Java  1.8.0_242
Micronaut  1.3.3
Micronaut CLI  1.3.3
MySQL Connector/J  8.0.19
MySQL Server  5.7.29

Micronaut とは、フルスタックのマイクロサービスフレームワークのこと。
MySQL Server のタイムゾーンは、UTC で設定している。

前提

MySQL サーバ、Java および、Micronaut CLI は、インストール済みとする。
MySQL サーバは、ローカルで構築し、ユーザは root 、パスワード無しとする。

手順

プロジェクト作成

Micronaut CLI でプロジェクトを作成する。
Gradle プロジェクトとなる。
言語指定が、Kotlin となっているが、変更しても設定に影響はない。
mn create-app --lang=kotlin micronaut-db-sample

ライブラリ追加

「build.gradle」の「dependencies」に以下の設定を追加
HikariCP および、MySQL Connector/J が利用可能になる。
HikariCP などのライブラリは、依存関係で一緒に導入される。
    runtimeOnly platform("io.micronaut:micronaut-bom:$micronautVersion")
    runtimeOnly "io.micronaut.configuration:micronaut-jdbc-hikari"
    runtimeOnly "mysql:mysql-connector-java:8.0.19"

接続設定の追加

Micronaut の設定ファイルにコネクションプールの設定を追加する。
追加した設定は、ソース中でインジェクションを行い利用する。
「src/main/resources/application.yml」に以下の設定を追加する。
設定中にある「serverTimezone=UTC」は、必須。当方の MySQL サーバは、UTC で設定してある。SYSTEM 設定(デフォルト)の場合は、JST などを指定する。
datasources:
  # デフォルトのコネクションプール設定
  default:
    # ${...} は、プレースホルダーであり、環境変数または、システムプロパティで設定可能
    # プレースホルダー中のコロンより後ろは、デフォルト値
    # デフォルト値は、バッククォート「`」で囲んだ値を指定する。
    # デフォルト値を指定せず(password のようにコロンの後を空にする)、
    # 環境変数などで設定しない場合 null 扱いとなる。エラーとはならない。
    # デフォルト値を指定しない(コロンを付けない)場合は、
    # 環境変数などで設定をしないと起動時にエラーとなる。
    # 環境変数で設定する場合「DB_DEFAULT_URL」で指定すると url が指定できる。
    # 環境変数の「_」は、プレースホルダーキーの「.-」と見なされる。
    # システムプロパティの場合は、キャメルケースも「-」で分割して一致する。
    # 設定とキーの一致法則については、以下の「Table 1. Property Value Normalization」を参照。
    # https://docs.micronaut.io/latest/guide/index.html#propertySource
    url: ${db.default.url:`jdbc:mysql://localhost:3306?serverTimezone=UTC`}
    username: ${db.default.username:`root`}
    # 【注意】デフォルト値に「``」と設定すると、パスワードありで空のパスワードを指定することになる。
    # パスワード無しの設定にするには、デフォルト値には何も設定しないこと
    password: ${db.default.password:}
    # デフォルトでは、古いクラス指定になってしまうため、指定が必須となる
    driverClassName: com.mysql.cj.jdbc.Driver
    # デフォルトは、自動コミットになる
    autoCommit: false
    # データベース指定
    catalog: ${db.database:`lab`}
    # コネクション時のタイムアウト(ms)
    connectionTimeout: 1000
    # 未使用(アイドル状態)コネクションの切断時間(ms)
    idleTimeout: ${db.idle.timeout:`10000`}
    # 最大コネクション保持数
    maximumPoolSize: ${db.maximum.pool.size:`10`}
    # この設定時間を超えて、コネクションを継続利用すると切断される(ms)
    # バッチ処理など長時間稼働する処理の場合は、設定値を増やす必要がある。
    maxLifetime: ${db.max.lifetime:`1800000`}
    # 最小コネクション保持数
    minimumIdle: ${db.minimum.idle:`0`}
    # ログなどに出力されるプール名
    # デフォルトでは、「HikariPool-1」のような名称となる
    poolName: Pool-RW
    # 読み込み専用設定
    readOnly: false
    # トランザクション分離レベル
    # MySQL のデフォルトは、「TRANSACTION_REPEATABLE_READ」
    # デフォルト値の場合、同一処理内では、default 側で Commit した内容を readonly 側では参照できない。
    transactionIsolation: TRANSACTION_READ_COMMITTED
    # MySQL Connector/J のプロパティを設定
    # HikariDataSource の dataSourceProperties に格納され、最終的に MySQL Connector/J に伝播する。
    # この設定は、構造化せずに以下のようにピリオドでキーを結合して値を定義することもできる。
    # data-source-properties.cachePropStmts: true
    # 設定の内容は、HikariCP が推奨している設定項目となる。
    # 詳細は、以下を参照。
    # https://github.com/brettwooldridge/HikariCP/wiki/MySQL-Configuration
    # MySQL Connector/J に設定できるプロパティは、以下を参照
    # https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-configuration-properties.html
    data-source-properties:
      cachePropStmts: true
      prepStmtCacheSize: 250
      prepStmtCacheSqlLimit: 2048
      useServerPrepStmts: true
      useLocalSessionState: true
      rewriteBatchedStatements: true
      cacheResultSetMetadata: true
      cacheServerConfiguration: true
      elideSetAutoCommits: true
      maintainTimeStats: false
      # ログに SQL のプロファイル情報を出力
      profileSQL: true

  # 任意の名称で設定可能。
  # @Named アノテーションでこの名称を指定する。
  readonly:
    url: ${db.default.url:`jdbc:mysql://localhost:3306?serverTimezone=UTC`}
    username: ${db.default.username:`root`}
    password: ${db.default.password:}
    driverClassName: com.mysql.cj.jdbc.Driver
    autoCommit: false
    catalog: ${db.database:`lab`}
    poolName: Pool-R
    readOnly: true
    transactionIsolation: TRANSACTION_READ_COMMITTED

DB 接続例

データベースの準備

スキーマとテーブルを準備
CREATE DATABASE  IF NOT EXISTS `lab` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE `lab`;
SET NAMES utf8 ;
DROP TABLE IF EXISTS `books`;
SET character_set_client = utf8mb4;
CREATE TABLE `books` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `name` varchar(200) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '名称',
  `price` int(11) NOT NULL COMMENT '価格',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='本';

データベースアクセスの実装

データベースにアクセスする場合、以下のように実装する。
「src/main/kotlin/micronaut/db/sample」ディレクトリに以下のクラスを作成
「DbAccessController.kt」
package micronaut.db.sample

import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import java.sql.Connection
import javax.inject.Inject
import javax.inject.Named
import javax.sql.DataSource

@Controller("/db")
class DbAccessController(
        @Inject private val dataSource: DataSource,
        @Inject @Named("readonly") private val dataSourceReadonly: DataSource
) {
    @Get(produces = ["${MediaType.TEXT_PLAIN};${MediaType.CHARSET_PARAMETER}=utf-8"])
    fun index(): String {
        val response = StringBuilder()
        // 読み書き用 Connection を生成
        // use は、リソースを自動クローズする構文
        dataSource.connection.use { con ->
            // 読み込み専用 Connection を生成
            dataSourceReadonly.connection.use { conR ->
                // 読み書き用 Statement を生成
                con.createStatement().use { stmt ->
                    // Books テーブルに Insert
                    stmt.execute("INSERT INTO books (id, name, price) VALUES (0, 'foo', 1000)")
                    // Books テーブルを全件取得
                    // コミット前なので 0 件
                    response.append("最初の SELECT: ")
                    response.append(select(conR))
                    // コミット
                    con.commit()
                    // Books テーブルを全件取得
                    // コミット後なので 1 件
                    response.append("Commit 後の SELECT: ")
                    response.append(select(conR))
                    // レコードを削除
                    stmt.execute("DELETE FROM books")
                    con.commit()
                }
            }
        }
        return response.toString()
    }
}

fun select(con: Connection): String = con.createStatement().use { stmt ->
    stmt.executeQuery("SELECT * FROM books").use { rset ->
        val response = StringBuilder()
        while (rset.next()) {
            response.appendln("id:${rset.getInt("id")}" +
                    ", name:${rset.getString("name")}" +
                    ", price:${rset.getInt("price")}"
            )
        }
        rset.last()
        return "${rset.row} 件\n" + response.toString()
    }
}

動作確認

以下のコマンドで起動する。
gradlew run

起動後、以下の URL にブラウザでアクセスする。
http://localhost:8080/db
ブラウザ上に以下のような表示がされる。
id は、自動採番のため、実行する度に変わる。
最初の SELECT: 0 件
Commit 後の SELECT: 1 件
id:0, name:foo, price:1000

参考