Kotlin Micronaut Data R2DBC使用Postgres jsonb类型操作失败
问题描述
基于PostgreSQL搭建Micronaut Data R2DBC应用时,插入数据抛出如下类型不匹配异常:
io.r2dbc.postgresql.ExceptionFactory$PostgresqlBadGrammarException: [42804] column blob is of type jsonb but expression is of type character varying
环境信息
- Micronaut版本:3.5.1
- Kotlin版本:1.6.10
- Java版本:17
相关代码片段
依赖配置(build.gradle.kts)
val coroutinesVersion = "1.6.0" dependencies { kapt("io.micronaut.data:micronaut-data-processor") implementation("io.micronaut:micronaut-jackson-databind") implementation("io.micronaut.data:micronaut-data-r2dbc") implementation("io.micronaut.kotlin:micronaut-kotlin-runtime") implementation("jakarta.annotation:jakarta.annotation-api") implementation("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlinVersion}") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactive:${coroutinesVersion}") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:${coroutinesVersion}") runtimeOnly("ch.qos.logback:logback-classic") runtimeOnly("org.postgresql:r2dbc-postgresql") implementation("io.micronaut:micronaut-validation") implementation("io.micronaut:micronaut-runtime") // 冗余配置:重复引入micronaut-jackson-databind,可删除 implementation("io.micronaut:micronaut-jackson-databind") runtimeOnly("com.fasterxml.jackson.module:jackson-module-kotlin") }
表结构定义
CREATE TABLE entity ( entity_id TEXT PRIMARY KEY, entity_type TEXT NOT NULL, blob JSONB NOT NULL, CREATED_DATE TIMESTAMP(3) DEFAULT now() NOT NULL );
实体类定义
@MappedEntity data class Entity( @field:Id val entityId: String, val entityType: String, @TypeDef(type = DataType.JSON) val blob: Any, val createdDate: Instant )
触发报错的测试代码
@Test fun testInsert() = runBlocking { val now = Instant.now() val entity = Entity("entity-uuid-1", "test-entity-type", "{}", now) Assertions.assertEquals(entity, entityRepository.save(entity)) }
根因分析
报错核心是R2DBC Postgres驱动收到的参数类型为character varying(字符串),和数据库字段的jsonb类型不匹配:
- 实体类
blob字段声明为Any类型,测试代码传入的是字符串"{}",仅加@TypeDef(type = DataType.JSON)注解不足以让驱动自动将参数识别为JSONB类型,驱动仍默认按字符串类型传参。 - 缺少JSON类型到Postgres JSONB类型的绑定逻辑,没有告诉R2DBC驱动该参数需要按JSONB OID类型传递。
解决方案
两种方案可选,优先推荐第一种:
方案1:直接使用R2DBC Postgres原生JSON类型
r2dbc-postgresql驱动自带Json类型,会自动识别映射为JSONB类型,不需要额外配置:
- 修改实体类,将
blob字段类型改为io.r2dbc.postgresql.codec.Json
import io.r2dbc.postgresql.codec.Json import java.time.Instant import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity @MappedEntity data class Entity( @field:Id val entityId: String, val entityType: String, val blob: Json, val createdDate: Instant )
- 修改测试代码,构造参数时用
Json.of()包装JSON字符串
@Test fun testInsert() = runBlocking { val now = Instant.now() val entity = Entity("entity-uuid-1", "test-entity-type", Json.of("{}"), now) Assertions.assertEquals(entity, entityRepository.save(entity)) }
方案2:自定义类型转换器(支持直接传字符串/业务对象)
如果不想使用驱动专属的Json类型,想直接传字符串或者自定义业务对象自动序列化为JSONB,可以自定义属性转换器:
- 编写转换器,实现Micronaut Data的
AttributeConverter接口,将传入的对象序列化为Json类型
import io.micronaut.core.convert.ConversionContext import io.micronaut.data.annotation.TypeDef import io.micronaut.data.model.DataType import io.micronaut.data.model.runtime.convert.AttributeConverter import io.r2dbc.postgresql.codec.Json import jakarta.inject.Singleton @Singleton class StringToJsonbConverter : AttributeConverter<String, Json> { override fun convertToPersistedValue(entityValue: String?, context: ConversionContext): Json? { return entityValue?.let { Json.of(it) } } override fun convertToEntityValue(persistedValue: Json?, context: ConversionContext): String? { return persistedValue?.asString() } }
- 修改实体类,给
blob字段指定转换器,字段类型改为String(如果要传自定义业务对象,修改转换器泛型和序列化逻辑即可)
import io.micronaut.data.annotation.Convert import java.time.Instant import io.micronaut.data.annotation.Id import io.micronaut.data.annotation.MappedEntity @MappedEntity data class Entity( @field:Id val entityId: String, val entityType: String, @field:TypeDef(type = DataType.JSON) @field:Convert(converter = StringToJsonbConverter::class) val blob: String, val createdDate: Instant )
- 测试代码不需要额外包装,直接传入JSON字符串即可正常插入。
注意:如果要支持任意业务对象自动序列化为JSONB,在转换器中注入
ObjectMapper,将传入的对象序列化为字节数组/字符串后再包装为Json类型即可,不建议使用Any作为字段类型,避免反序列化时出现类型丢失问题。
内容的提问来源于stack exchange,提问作者jay
相关产品推荐
相关产品推荐

