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 投影只取 firstNamelastName。创建 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 只需查询 firstNamelastName 字段。验证测试:

@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 的成因:

  • 根本原因:返回类型与实体类型不匹配时缺少转换器
  • 解决方案
    1. ✅ 为 DTO 类添加全参构造函数
    2. ✅ 使用接口投影替代类投影

最佳实践建议

  • 简单投影优先使用接口方案
  • 复杂业务逻辑选择类投影
  • 始终通过单元测试验证投影逻辑

完整示例代码见 GitHub 仓库


原始标题:Solving Spring Data JPA ConverterNotFoundException: No converter found | Baeldung