You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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类型不匹配:

  1. 实体类blob字段声明为Any类型,测试代码传入的是字符串"{}",仅加@TypeDef(type = DataType.JSON)注解不足以让驱动自动将参数识别为JSONB类型,驱动仍默认按字符串类型传参。
  2. 缺少JSON类型到Postgres JSONB类型的绑定逻辑,没有告诉R2DBC驱动该参数需要按JSONB OID类型传递。
解决方案

两种方案可选,优先推荐第一种:

方案1:直接使用R2DBC Postgres原生JSON类型

r2dbc-postgresql驱动自带Json类型,会自动识别映射为JSONB类型,不需要额外配置:

  1. 修改实体类,将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
)
  1. 修改测试代码,构造参数时用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,可以自定义属性转换器:

  1. 编写转换器,实现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()
    }
}
  1. 修改实体类,给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
)
  1. 测试代码不需要额外包装,直接传入JSON字符串即可正常插入。

注意:如果要支持任意业务对象自动序列化为JSONB,在转换器中注入ObjectMapper,将传入的对象序列化为字节数组/字符串后再包装为Json类型即可,不建议使用Any作为字段类型,避免反序列化时出现类型丢失问题。


内容的提问来源于stack exchange,提问作者jay

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 15:42:23