1. 概述
在使用 Spring Data JPA 时,我们经常通过派生查询和自定义查询返回特定格式的结果。典型场景是DTO 投影,它能精准选择所需字段,避免加载不必要数据。
但 DTO 投影并非总是简单直接,实现不当会导致 ConverterNotFoundException
异常。本文将通过实际案例,深入分析该异常的成因及解决方案。
2. 异常问题实践分析
先通过实例理解异常堆栈的含义。为简化演示,我们使用H2 内存数据库。
2.1. H2 配置
Spring Boot 对 H2 数据库提供原生支持,默认使用用户名 sa
和空密码连接。在 application.properties
中添加配置:
spring.datasource.url=jdbc:h2:mem:mydb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
⚠️ 注意:生产环境切勿使用空密码!
2.2. 实体类
定义一个 Employee
实体类:
@Entity
public class Employee {
@Id
private int id;
@Column
private String firstName;
@Column
private String lastName;
@Column
private double salary;
// 标准 getter/setter
}
关键注解说明:
@Entity
:声明 JPA 实体@Id
:标记主键字段@Column
:绑定实体字段到数据库列
2.3. JPA 仓库
创建 Spring Data JPA 仓库接口:
@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Integer> {
}
假设需要展示员工全名,通过 DTO 投影只取 firstName
和 lastName
。创建 EmployeeFullName
类:
public class EmployeeFullName {
private String firstName;
private String lastName;
// 标准 getter/setter
public String fullName() {
return getFirstName()
.concat(" ")
.concat(getLastName());
}
}
重点:添加了自定义方法 fullName()
拼接全名。接下来在仓库接口中添加派生查询:
EmployeeFullName findEmployeeFullNameById(int id);
执行测试验证:
@Test
void givenEmployee_whenGettingFullName_thenThrowException() {
Employee emp = new Employee();
emp.setId(1);
emp.setFirstName("Adrien");
emp.setLastName("Juguet");
emp.setSalary(4000);
employeeRepository.save(emp);
assertThatThrownBy(() -> employeeRepository
.findEmployeeFullNameById(1))
.isInstanceOfAny(ConverterNotFoundException.class)
.hasMessageContaining("No converter found capable of converting from type"
+ "[com.baeldung.spring.data.noconverterfound.models.Employe");
}
测试结果:抛出 ConverterNotFoundException
。
异常根源:JpaRepository
默认期望返回 Employee
实体对象,当返回类型变为 EmployeeFullName
时,Spring Data JPA 无法找到合适的转换器将 Employee
转换为 EmployeeFullName
。
3. 解决方案
方案一:使用全参构造函数 ✅
Spring Data JPA 默认通过构造函数确定投影字段。给 EmployeeFullName
添加全参构造函数:
public EmployeeFullName(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
原理:通过构造函数参数明确告知 JPA 只需查询 firstName
和 lastName
字段。验证测试:
@Test
void givenEmployee_whenGettingFullNameUsingClass_thenReturnFullName() {
Employee emp = new Employee();
emp.setId(2);
emp.setFirstName("Azhrioun");
emp.setLastName("Abderrahim");
emp.setSalary(3500);
employeeRepository.save(emp);
assertThat(employeeRepository.findEmployeeFullNameById(2).fullName())
.isEqualTo("Azhrioun Abderrahim");
}
测试通过 ✅
方案二:使用接口投影 ✅
更优雅的方案是使用接口投影,无需构造函数:
public interface IEmployeeFullName {
String getFirstName();
String getLastName();
default String fullName() {
return getFirstName().concat(" ")
.concat(getLastName());
}
}
优势:
- 通过 getter 方法自动映射字段
- 支持默认方法实现业务逻辑
在仓库接口中添加新查询:
IEmployeeFullName findIEmployeeFullNameById(int id);
验证测试:
@Test
void givenEmployee_whenGettingFullNameUsingInterface_thenReturnFullName() {
Employee emp = new Employee();
emp.setId(3);
emp.setFirstName("Eva");
emp.setLastName("Smith");
emp.setSalary(6500);
employeeRepository.save(emp);
assertThat(employeeRepository.findIEmployeeFullNameById(3).fullName())
.isEqualTo("Eva Smith");
}
测试通过 ✅
方案对比: | 方案 | 优点 | 缺点 | |------|------|------| | 类投影 | 支持复杂逻辑 | 需显式定义构造函数 | | 接口投影 | 代码简洁 | 无法包含复杂逻辑 |
4. 总结
本文通过实际案例剖析了 Spring Data JPA 中 ConverterNotFoundException
的成因:
- 根本原因:返回类型与实体类型不匹配时缺少转换器
- 解决方案:
- ✅ 为 DTO 类添加全参构造函数
- ✅ 使用接口投影替代类投影
最佳实践建议:
- 简单投影优先使用接口方案
- 复杂业务逻辑选择类投影
- 始终通过单元测试验证投影逻辑
完整示例代码见 GitHub 仓库。