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
相关产品推荐
相关产品推荐

