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

Spring Data JPA原生SQL查询Record/类投影转换失败问题求助

问题描述

我有Instructor和Section两个实体,二者为多对多关联,中间表为teaches。我需要查询指定系的所有讲师及其在指定年份授课的章节数,为此定义了如下Record:

public record InstructorWithNumberOfSections(Integer instructorId, Long numberOfSections)  {}  

并通过以下Repository方法执行原生SQL查询:

@Transactional(readOnly = true)
@Query(value = """
SELECT i.id AS instructor_id, COUNT(i.id) AS number_of_sections FROM instructor i LEFT JOIN teaches t ON i.id = t.instructor_id WHERE i.dept_id = ?1 AND t.year = ?2 GROUP BY i.id
""", nativeQuery = true)
List<InstructorWithNumberOfSections> findNumberOfSectionsForInstructors(String departmentName, Integer year);

调用REST接口触发该查询时,抛出异常:

org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [org.springframework.data.jpa.repository.query.AbstractJpaQuery$TupleConverter$TupleBackedMap] to type [com.example.university.model.dto.InstructorWithNumberOfSections] 

将其改为接口形式后查询正常工作:

public interface InstructorWithNumberOfSections {
    Integer getInstructorId();
    Long getNumberOfSections();
}

但不清楚原生SQL查询中使用Record/类的问题所在,且JPQL查询中使用Record是正常的,希望得到帮助。

原因分析

Spring Data JPA对原生SQL和JPQL的结果映射逻辑存在明显差异:

  • JPQL场景:JPQL是面向实体的查询,框架能直接将查询结果与Record的构造函数参数匹配(基于名称或位置)——JPQL的结果会被解析为实体属性或明确的投影字段,Spring Data可以通过反射找到Record的构造方法完成实例化,因此能正常工作。
  • 原生SQL场景:原生SQL返回的结果默认会被封装为TupleBackedMap(键是SQL中定义的字段别名,值是对应字段值)。对于自定义类或Record,Spring Data JPA默认没有内置转换器能直接将这种Map结构转换为Record实例;而投影接口是Spring Data专门支持的特性,框架会通过动态代理实现该接口,直接从Map中提取对应字段值返回,因此可以正常运行。

另外,即使原生SQL的字段别名和Record的构造参数名完全匹配(注意大小写,默认Spring Data对驼峰与下划线的映射需要额外配置),框架也不会自动触发Record的实例化,因为原生SQL结果的映射逻辑并未适配Record的构造函数注入逻辑。

解决办法

如果想在原生SQL查询中继续使用Record,有两种可行方案:

方案1:使用@SqlResultSetMapping和@ConstructorResult

在Instructor实体类上定义结果集映射,明确指定原生SQL结果到Record构造函数的映射规则:

@Entity
@SqlResultSetMapping(
    name = "InstructorWithSectionsMapping",
    classes = @ConstructorResult(
        targetClass = InstructorWithNumberOfSections.class,
        columns = {
            @ColumnResult(name = "instructor_id", type = Integer.class),
            @ColumnResult(name = "number_of_sections", type = Long.class)
        }
    )
)
public class Instructor {
    // 实体字段及其他配置...
}

然后在Repository的@Query注解中指定该映射:

@Transactional(readOnly = true)
@Query(value = """
SELECT i.id AS instructor_id, COUNT(i.id) AS number_of_sections FROM instructor i LEFT JOIN teaches t ON i.id = t.instructor_id WHERE i.dept_id = ?1 AND t.year = ?2 GROUP BY i.id
""", 
nativeQuery = true,
resultSetMapping = "InstructorWithSectionsMapping")
List<InstructorWithNumberOfSections> findNumberOfSectionsForInstructors(String departmentName, Integer year);

方案2:自定义类型转换器

实现Converter<TupleBackedMap, InstructorWithNumberOfSections>并注册到Spring容器,让框架自动完成类型转换:

@Component
public class TupleBackedMapToRecordConverter implements Converter<TupleBackedMap, InstructorWithNumberOfSections> {
    @Override
    public InstructorWithNumberOfSections convert(TupleBackedMap source) {
        Integer instructorId = (Integer) source.get("instructor_id");
        Long numberOfSections = (Long) source.get("number_of_sections");
        return new InstructorWithNumberOfSections(instructorId, numberOfSections);
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 17:25:07