解决 Spring Data JPA ConverterNotFoundException: No converter found
1、概览
在使用 Spring Data JPA 时,我们经常会利用派生和自定义查询,以我们喜欢的格式返回结果。一个典型的例子就是 DTO 投影,它提供了一种只 SELECT 某些特定列以减少不必要数据开销的好方法。
然而,DTO 投影并不总是那么容易,如果实现不当,可能会导致 ConverterNotFoundException
异常。本文将带你了解 ConverterNotFoundException
异常出现的原因,以及如何在使用 Spring Data JPA 时避免 ConverterNotFoundException
异常。
2、在实践中理解异常
通过一个实际例子来理解异常。
为了简单起见,使用 H2 数据库。首先,在 pom.xml 文件中添加其依赖:
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.2.224</version>
</dependency>
2.1、H2 配置
Spring Boot 提供了对 H2 嵌入式数据库的支持。默认情况下,它会配置应用使用用户名 sa
和空密码连接到 H2。
将数据库连接凭证添加到 application.properties
文件中:
spring.datasource.url=jdbc:h2:mem:mydb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
如上就是使用 Spring Boot 设置 H2 配置所需的全部内容。
2.2、Entity 类
们定义一个 JPA 实体类 Employee
:
@Entity
public class Employee {
@Id
private int id;
@Column
private String firstName;
@Column
private String lastName;
@Column
private double salary;
// Getter/Setter 方法省略
}
如上,员工类(Employee
)定义了 id
、firstName
、lastName
和 salary
属性。
@Entity
注解来表示 Employee
类是一个 JPA Entity。@Id
标记主键字段,@Column
用于映射数据列和实体字段。
2.3、JPA Repository
接下来,创建一个 Spring Data JPA Repository
来处理存储和检索员工的逻辑:
@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Integer> {
}
在这里,假设只需要显示员工的全名。因此,使用 DTO 投影来只 SELECT firstName
和 lastName
字段。
由于 Employee
类包含了额外的字段,因此创建一个名为 EmployeeFullName
的新类,其中只包含 firstName
和 lastName
字段:
public class EmployeeFullName {
private String firstName;
private String lastName;
// get/set 方法省略
public String fullName() {
return getFirstName()
.concat(" ")
.concat(getLastName());
}
}
如上,创建了一个自定义方法 fullName()
来显示员工的全名。现在,向 EmployeeRepository
添加一个派生查询,返回员工的全名:
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 无法找到合适的 Converter(转换器)将预期的 Employee
对象转换为新的 EmployeeFullName
对象。
3、解决办法
在使用类实现 DTO 投影时,Spring Data JPA 默认使用构造函数来确定要检索的字段。因此,这里 最简单的解决方案是为 EmployeeFullName
类添加一个带参数的构造函数:
public EmployeeFullName(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
这等于告诉了 Spring Data JPA 只 SELECT 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");
}
不出所料,测试成功通过。
另一种解决方案是使用基于接口的投影。这样就不必担心构造函数了。因此,可以使用一个接口来公开要读取字段的 getter 方法:
public interface IEmployeeFullName {
String getFirstName();
String getLastName();
default String fullName() {
return getFirstName().concat(" ")
.concat(getLastName());
}
}
如上,使用 default
方法来显示全名。接下来,创建另一个派生查询,返回 IEmployeeFullName
类型的实例:
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
异常的原因,以及解决该异常的两种办法。
Ref:https://www.baeldung.com/spring-jpa-converter-exception