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

如何在Spring Data中查询JSONB字段并映射为Java对象

问题描述

我有一个读取产品信息的Repository,代码如下:

import org.springframework.data.jdbc.repository.query.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface ProductRepository extends org.springframework.data.repository.Repository<Product, Long> {

    @Query("SELECT\n" +
        "   p.id,\n" +
        "   p.locations \n" +
        " FROM product p" +
        " WHERE p.id = :productId \n")
    List<Product> queryProducts(@Param("productId") Long productId);
}

数据库中locations字段类型为jsonb,存储对象数组,示例值:[{"city":"New York", "country": "USA"}, {"city":"Madrid", "country": "Spain"}]

对应的实体类:

Product.java

import lombok.Value;
import org.springframework.data.relational.core.mapping.Table;

import java.util.List;

@Value
@Table
public class Product {
    Long           id;
    List<Location> locations;
}

Location.java

import lombok.Value;

@Value
public class Location {
    String city;
    String country;
}

运行时抛出异常:

java.lang.IllegalStateException: We need at least one condition
at org.springframework.util.Assert.state(Assert.java:76)
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:

Original Stack Trace:
at org.springframework.util.Assert.state(Assert.java:76)
at org.springframework.data.jdbc.core.convert.SqlGenerator.buildConditionForBackReference(SqlGenerator.java:311)
at org.springframework.data.jdbc.core.convert.SqlGenerator.getFindAllByProperty(SqlGenerator.java:289)
at org.springframework.data.jdbc.core.convert.DefaultDataAccessStrategy.findAllByPath(DefaultDataAccessStrategy.java:303)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:351)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:213)
at jdk.proxy4/jdk.proxy4.$Proxy101.findAllByPath(Unknown Source)
at org.springframework.data.jdbc.core.convert.MappingJdbcConverter$ResolvingRelationalPropertyValueProvider.getPropertyValue(MappingJdbcConverter.java:379)
at org.springframework.data.jdbc.core.convert.MappingJdbcConverter$ResolvingRelationalPropertyValueProvider.getPropertyValue(MappingJdbcConverter.java:310)
at org.springframework.data.relational.core.conversion.MappingRelationalConverter$2.getPropertyValue(MappingRelationalConverter.java:480)
at org.springframework.data.relational.core.conversion.MappingRelationalConverter$2.getPropertyValue(MappingRelationalConverter.java:461)
at org.springframework.data.mapping.model.PersistentEntityParameterValueProvider.getParameterValue(PersistentEntityParameterValueProvider.java:71)
at org.springframework.data.relational.core.conversion.MappingRelationalConverter$ConvertingParameterValueProvider.getParameterValue(MappingRelationalConverter.java:1164)
at org.springframework.data.mapping.model.SpELExpressionParameterValueProvider.getParameterValue(SpELExpressionParameterValueProvider.java:49)
at org.springframework.data.mapping.model.ClassGeneratingEntityInstantiator.extractInvocationArguments(ClassGeneratingEntityInstantiator.java:301)
at org.springframework.data.mapping.model.ClassGeneratingEntityInstantiator$EntityInstantiatorAdapter.createInstance(ClassGeneratingEntityInstantiator.java:273)
at org.springframework.data.mapping.model.ClassGeneratingEntityInstantiator.createInstance(ClassGeneratingEntityInstantiator.java:98)
at org.springframework.data.relational.core.conversion.MappingRelationalConverter.read(MappingRelationalConverter.java:444)
at org.springframework.data.relational.core.conversion.MappingRelationalConverter.readAggregate(MappingRelationalConverter.java:339)
at org.springframework.data.relational.core.conversion.MappingRelationalConverter.readAggregate(MappingRelationalConverter.java:302)
at org.springframework.data.jdbc.core.convert.MappingJdbcConverter.readAndResolve(MappingJdbcConverter.java:287)
at org.springframework.data.jdbc.core.convert.JdbcConverter.readAndResolve(JdbcConverter.java:106)
at org.springframework.data.jdbc.core.convert.EntityRowMapper.mapRow(EntityRowMapper.java:82)
at org.springframework.data.jdbc.repository.support.JdbcQueryLookupStrategy$PostProcessingRowMapper.mapRow(JdbcQueryLookupStrategy.java:335)
at org.springframework.data.jdbc.repository.query.AbstractJdbcQuery$ConvertingRowMapper.mapRow(AbstractJdbcQuery.java:196)
at org.springframework.jdbc.core.RowMapperResultSetExtractor.extractData(RowMapperResultSetExtractor.java:94)
at org.springframework.jdbc.core.RowMapperResultSetExtractor.extractData(RowMapperResultSetExtractor.java:61)
at org.springframework.jdbc.core.JdbcTemplate$1.doInPreparedStatement(JdbcTemplate.java:733)
at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:658)
at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:723)
at org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:748)
at org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate.query(NamedParameterJdbcTemplate.java:178)
at org.springframework.data.jdbc.repository.query.AbstractJdbcQuery.lambda$createSingleReadingQueryExecution$3(AbstractJdbcQuery.java:165)
at org.springframework.data.jdbc.repository.query.StringBasedJdbcQuery.execute(StringBasedJdbcQuery.java:145)
at org.springframework.data.repository.core.support.RepositoryMethodInvoker.doInvoke(RepositoryMethodInvoker.java:170)
at org.springframework.data.repository.core.support.RepositoryMethodInvoker.invoke(RepositoryMethodInvoker.java:158)
at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.doInvoke(QueryExecutorMethodInterceptor.java:164)
at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.invoke(QueryExecutorMethodInterceptor.java:143)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:385)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:137)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:220)

我猜测需要自定义映射器实现jsonb到Java对象的转换,但不知道具体操作方法,求解决思路。


解决思路与步骤

1. 问题根源

Spring Data JDBC默认会把List<Location>识别为关联实体集合,尝试执行关联查询(比如查找关联的location表数据),但实际上locations是存储在同一个表的jsonb字段里,不是关联表,因此触发了错误的查询逻辑,导致断言失败。

2. 实现自定义类型转换器

需要实现Spring Data JDBC的Converter接口,完成数据库jsonb字段与List<Location>的双向转换,这里用Spring默认集成的Jackson处理Json序列化/反序列化。

步骤1:编写Json转换器

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.ReadingConverter;
import org.springframework.data.convert.WritingConverter;

import java.util.List;

// 从数据库读取jsonb时转换为List<Location>
@ReadingConverter
public class JsonbToListLocationConverter implements Converter<String, List<Location>> {

    private final ObjectMapper objectMapper;

    public JsonbToListLocationConverter(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    @Override
    public List<Location> convert(String source) {
        try {
            return objectMapper.readValue(source, new TypeReference<List<Location>>() {});
        } catch (Exception e) {
            throw new RuntimeException("Failed to convert jsonb to List<Location>", e);
        }
    }
}

// 写入数据库时把List<Location>转换为json字符串
@WritingConverter
public class ListLocationToJsonbConverter implements Converter<List<Location>, String> {

    private final ObjectMapper objectMapper;

    public ListLocationToJsonbConverter(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    @Override
    public String convert(List<Location> source) {
        try {
            return objectMapper.writeValueAsString(source);
        } catch (Exception e) {
            throw new RuntimeException("Failed to convert List<Location> to jsonb", e);
        }
    }
}

如果是PostgreSQL环境,读取时可能会拿到PGobject类型,需要额外添加读取转换器:

import org.postgresql.util.PGobject;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.ReadingConverter;

import java.sql.SQLException;
import java.util.List;

@ReadingConverter
public class PGobjectToListLocationConverter implements Converter<PGobject, List<Location>> {

    private final ObjectMapper objectMapper;

    public PGobjectToListLocationConverter(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    @Override
    public List<Location> convert(PGobject source) {
        try {
            return objectMapper.readValue(source.getValue(), new TypeReference<List<Location>>() {});
        } catch (SQLException | com.fasterxml.jackson.core.JsonProcessingException e) {
            throw new RuntimeException("Failed to convert PGobject to List<Location>", e);
        }
    }
}

步骤2:注册转换器到Spring Data JDBC

创建配置类,将自定义转换器注册到JdbcCustomConversions中:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jdbc.core.convert.JdbcCustomConversions;

import java.util.Arrays;

@Configuration
public class JdbcConverterConfig {

    @Bean
    public JdbcCustomConversions jdbcCustomConversions(ObjectMapper objectMapper) {
        return new JdbcCustomConversions(Arrays.asList(
                new JsonbToListLocationConverter(objectMapper),
                new ListLocationToJsonbConverter(objectMapper),
                new PGobjectToListLocationConverter(objectMapper) // PostgreSQL环境需要
        ));
    }
}

3. 明确字段映射关系

修改Product类,用@Column注解明确指定locations对应的数据库字段名,避免Spring Data JDBC将其识别为关联实体:

import lombok.Value;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.Table;

import java.util.List;

@Value
@Table
public class Product {
    Long id;
    @Column("locations")
    List<Location> locations;
}

4. 验证查询

此时Repository的自定义查询会正常工作,Spring Data JDBC会使用自定义转换器将jsonb字段转换成List<Location>,不会再尝试执行关联查询。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 09:34:53