1. 概述

在这个简短教程中,我们将探讨Spring的JdbcTemplate中出现的异常EmptyResultDataAccessException: Incorrect result size: expected 1, actual 0

首先,我们将详细分析这个异常的根本原因。然后,通过一个实际示例来演示如何重现它,并学习如何解决这个问题。

2. 原因

Spring的JdbcTemplate类提供了执行SQL查询并获取结果的便捷方式。它底层使用了JDBC API。

通常情况下,当预期至少会返回一行结果,但实际返回的是0行时,JdbcTemplate会抛出EmptyResultDataAccessException

现在我们了解了异常含义,接下来将通过实例来重现并修复它。

3. 重现异常

例如,考虑Employee类:

public class Employee {

    private int id;
    private String firstName;
    private String lastName;

    public Employee(int id, String firstName, String lastName) {
        this.id = id;
        this.firstName = firstName;
        this.lastName = lastName;
    }

    // standard getters and setters

}

接着,我们创建一个使用JdbcTemplate处理SQL查询的数据访问对象(DAO)类:

public class EmployeeDAO {
    private JdbcTemplate jdbcTemplate;

    public void setDataSource(DataSource dataSource) {
        jdbcTemplate = new JdbcTemplate(dataSource);
    }
    
}

值得注意的是,因为JdbcTemplate需要一个DataSource对象,我们在setter方法中注入了它。

现在,我们将添加一个方法,根据id获取Employee对象。我们可以使用JdbcTemplate提供的queryForObject()方法:

public Employee getEmployeeById(int id) {
    RowMapper<Employee> employeeRowMapper = (rs, rowNum) -> new Employee(rs.getInt("ID"), rs.getString("FIRST_NAME"), rs.getString("LAST_NAME"));

    return jdbcTemplate.queryForObject("SELECT * FROM EMPLOYEE WHERE id=?", employeeRowMapper, id);
}

如你所见,第一个参数是SQL查询,第二个参数是将ResultSet映射为Employee对象的RowMapper

实际上,queryForObject()期望返回恰好一行数据。因此,如果传递的id不存在,它就会直接抛出EmptyResultDataAccessException

让我们用一个测试来验证这一点:

@Test(expected = EmptyResultDataAccessException.class)
public void whenIdNotExist_thenThrowEmptyResultDataAccessException() {
    EmployeeDAO employeeDAO = new EmployeeDAO();
    ReflectionTestUtils.setField(employeeDAO, "jdbcTemplate", jdbcTemplate);
    Mockito.when(jdbcTemplate.queryForObject(anyString(), ArgumentMatchers.<RowMapper<Employee>> any(), anyInt()))
        .thenThrow(EmptyResultDataAccessException.class);

    employeeDAO.getEmployeeById(1);
}

由于没有对应给定id的员工,指定的查询返回了0行数据。因此,JdbcTemplate会因EmptyResultDataAccessException而失败。

4. 解决异常

最简单的解决方案是捕获异常并返回null

public Employee getEmployeeByIdV2(int id) {
    RowMapper<Employee> employeeRowMapper = (rs, rowNum) -> new Employee(rs.getInt("ID"), rs.getString("FIRST_NAME"), rs.getString("LAST_NAME"));

    try {
        return jdbcTemplate.queryForObject("SELECT * FROM EMPLOYEE WHERE id=?", employeeRowMapper, id);
    } catch (EmptyResultDataAccessException e) {
        return null;
    }
}

这样,当SQL查询的结果为空时,我们确保返回null

现在,让我们添加另一个测试用例以确认一切按预期工作:

@Test
public void whenIdNotExist_thenReturnNull() {
    EmployeeDAO employeeDAO = new EmployeeDAO();
    ReflectionTestUtils.setField(employeeDAO, "jdbcTemplate", jdbcTemplate);
    Mockito.when(jdbcTemplate.queryForObject(anyString(), ArgumentMatchers.<RowMapper<Employee>> any(), anyInt()))
        .thenReturn(null);

    assertNull(employeeDAO.getEmployeeByIdV2(1));
}

5. 总结

在这篇简短教程中,我们详细讨论了JdbcTemplate抛出"EmptyResultDataAccessException: Incorrect result size: expected 1, actual 0"异常的原因。

在过程中,我们看到了如何产生这个异常,以及如何通过实际示例来解决它。

如往常一样,所有示例的完整源代码可以在GitHub上找到:https://github.com/eugenp/tutorials/tree/master/persistence-modules/spring-jdbc