この記事はJava Advent Calendar 2025 シリーズ 2の記事になりました(Javaが1行も登場しませんが他に適切なカレンダーも見つけられず、ご容赦下さい)。
TL;DR
-
PostgresqlConnectionConfigurationに対し、jOOQで生成されたEnumのEnumCodecを登録することで、エラー無く取り扱える
前置き
PostgreSQL上でEnum配列として定義されている列をjOOQで取得すると、Caused by: java.lang.ClassCastException: class java.lang.String cannot be cast to class [Ljava.lang.Object; (java.lang.String and [Ljava.lang.Object; are in module java.base of loader 'bootstrap')というエラーになります。
この問題は以前より知られており、少なくとも1年半以上解決されていません。
この問題に対して見つけた対処方法をまとめます。
バージョン
-
Java: 21 -
r2dbc-postgresql: 1.0.9.RELEASE -
jOOQ: 3.19.28 -
PostgreSQL: 15.12
やり方
サンプルコードはKotlinになっていますが、Javaでもほぼそのまま書けます。
以下のように、EnumCodecを登録したConnectionFactoryを用いることで実現できます。
YourEnumには、それぞれ処理対象としたいEnumを設定して下さい。
import io.r2dbc.postgresql.PostgresqlConnectionFactory
import io.r2dbc.postgresql.PostgresqlConnectionFactoryProvider
import io.r2dbc.postgresql.codec.EnumCodec
import io.r2dbc.spi.ConnectionFactoryOptions
val url: String = TODO("接続用URL")
val factoryOptions = PostgresqlConnectionFactoryProvider
.builder(ConnectionFactoryOptions.parse(url))
.codecRegistrar(
EnumCodec.builder()
.withEnum(
// PostgreSQL上のEnum名。jOOQの生成結果のgetNameと一致するため、適当な値から読み出している
YourEnum.FOO.getName(),
YourEnum::class.java
)
.withEnum(
/* 複数登録したい場合はチェーンさせる */
)
.build()
)
.build()
val connectionFactory = PostgresqlConnectionFactory(factoryOptions)
解説
r2dbc-postgresqlにおけるCodecについて
READMEに書かれている通り、r2dbc-postgresqlでは独自の型マッピングをCodecとして登録することができます。
EnumCodecの配列サポートについて
こちらもREADMEに書かれている通り、EnumCodecは可能なら配列をサポートしてくれます。
jOOQ経由で読み出した際にEnum配列を扱えるようになったのはこの効果によります。
登録の省力化について
サンプルコードでは個別にEnumCodecを登録する方法を紹介しましたが、以下のようにすることで、登録の自動化も可能です。
// テーブル定義一覧からenum配列で定義された列を抽出し、PostgreSQL上のenum定義名とjOOQのenumクラスへ変換
val enumDefinitions: List<Pair<String, Class<Enum<*>>>> = DEFAULT_CATALOG.schemas.flatMap { schema ->
schema.tables.flatMap { table ->
table.fields().mapNotNull { field ->
field
// 全てのテーブル定義から、enum配列で定義された列のみを抽出
.takeIf { it.type.isArray && it.type.componentType.isEnum }
?.let { _ ->
// isEnumのためキャスト可能
@Suppress("UNCHECKED_CAST")
val enumType = field.type.componentType as Class<Enum<*>>
// enumに対するjOOQの生成結果はEnumType.getNameとしてPostgreSQL上のenum定義名を持つため、
// 値一覧中の適当な値から当該ゲッターを呼び出している
enumType
.getEnumConstants()
?.firstOrNull()
?.let { (it as EnumType).name }
?.let { it to enumType }
}
}
}
}
val enumCodecs = enumDefinitions
.fold(EnumCodec.builder()) { acc, (name, clazz) ->
acc.withEnum(name, clazz)
}
.build()
val factoryOptions = PostgresqlConnectionFactoryProvider
.builder(ConnectionFactoryOptions.parse(r2dbcProperties.url))
// 生成したコーデックを登録
.codecRegistrar(enumCodecs)
.build()
val connectionFactory = PostgresqlConnectionFactory(factoryOptions)
全テーブル・全列を舐めることになるため、初期化速度は低下しますが、それを無視できる場合にはお試し下さい。
関連
同じような読み出し時の問題で、jOOQ側に設定を追加することで解消できたケースも有ったため、関連として貼ります。