1、概览 {#1概览}
在使用 Spring Data JPA 时,我们经常会利用派生和自定义查询,以我们喜欢的格式返回结果。一个典型的例子就是 DTO 投影,它提供了一种只 SELECT 某些特定列以减少不必要数据开销的好方法。
然而,DTO 投影并不总是那么容易,如果实现不当,可能会导致 ConverterNotFoundException
异常。本文将带你了解 ConverterNotFoundException
异常出现的原因,以及如何在使用 Spring Data JPA 时避免 ConverterNotFoundException
异常。
2、在实践中理解异常 {#2在实践中理解异常}
通过一个实际例子来理解异常。
为了简单起见,使用 H2 数据库。首先,在 pom.xml 文件中添加其依赖:
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.2.224</version>
</dependency>
2.1、H2 配置 {#21h2-配置}
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 类 {#22entity-类}
们定义一个 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 {#23jpa-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、解决办法 {#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、总结 {#4总结}
本文介绍了 Spring Data JPA 出现 ConverterNotFoundException
异常的原因,以及解决该异常的两种办法。
Ref:https://www.baeldung.com/spring-jpa-converter-exception