Keywords: JdbcTemplate | EmptyResultDataAccessException | Spring Framework | Database Query | Exception Handling
Abstract: This article provides an in-depth analysis of the EmptyResultDataAccessException encountered when using Spring JdbcTemplate for single-row queries. It explores the root causes of the exception, Spring's design philosophy, and presents multiple solution approaches. By comparing the usage scenarios of queryForObject, query methods, and ResultSetExtractor, the article demonstrates how to properly handle queries that may return empty results. The discussion extends to modern Java 8 functional programming features for building reusable query components and explores the use of Optional types as alternatives to null values in contemporary programming practices.
Problem Context and Exception Analysis
When using Spring Framework's JdbcTemplate for database queries, developers frequently encounter the EmptyResultDataAccessException: Incorrect result size: expected 1, actual 0 exception. The fundamental cause of this exception lies in the design philosophy of the queryForObject method: it expects the query to return exactly one row of data. When a query returns zero rows or more than one row, Spring throws IncorrectResultSizeDataAccessException or its subclasses to enforce explicit handling of boundary cases.
Exception Generation Mechanism
Spring Framework's JdbcTemplate.queryForObject method internally implements strict validation of result set size. After executing an SQL query, if the ResultSet contains no records, the framework throws EmptyResultDataAccessException; if multiple records are found, it throws IncorrectResultSizeDataAccessException. This design embodies the "fail-fast" principle, ensuring early exposure of data consistency issues.
Basic Solution: Using the Query Method
The most straightforward solution involves using the JdbcTemplate.query method in combination with RowMapper, which allows flexible handling of various result set scenarios:
public String test() {
String sql = "select ID_NMB_SRZ from codb_owner.TR_LTM_SLS_RTN " +
"where id_str_rt = '999' and ID_NMB_SRZ = '60230009999999'";
List<String> resultList = jdbc.query(sql, new RowMapper<String>() {
@Override
public String mapRow(ResultSet rs, int rowNum) throws SQLException {
return rs.getString(1);
}
});
if (resultList.isEmpty()) {
return null;
} else if (resultList.size() == 1) {
return resultList.get(0);
} else {
throw new IncorrectResultSizeDataAccessException(1, resultList.size());
}
}
This approach provides complete control over result processing logic, allowing decisions based on business requirements to return null, the first element, or throw an exception.
Advanced Approach: Application of ResultSetExtractor
Another elegant solution utilizes ResultSetExtractor, which offers finer-grained control over the result set:
public String test() {
String sql = "select ID_NMB_SRZ from codb_owner.TR_LTM_SLS_RTN " +
"where id_str_rt = '999' and ID_NMB_SRZ = '60230009999999'";
return jdbc.query(sql, new ResultSetExtractor<String>() {
@Override
public String extractData(ResultSet rs) throws SQLException, DataAccessException {
return rs.next() ? rs.getString("ID_NMB_SRZ") : null;
}
});
}
The advantage of ResultSetExtractor lies in its direct manipulation of the ResultSet, avoiding the creation of intermediate collections and offering better performance in sensitive scenarios.
Modern Improvements: Leveraging Java 8 Features
With the widespread adoption of Java 8, we can utilize lambda expressions and functional programming to simplify code. First, define a reusable RowMapper:
private static final RowMapper<String> STRING_MAPPER =
(rs, rowNum) -> rs.getString("ID_NMB_SRZ");
Then create a generic single-result extractor:
public static <T> ResultSetExtractor<T> singletonExtractor(RowMapper<? extends T> mapper) {
return rs -> rs.next() ? mapper.mapRow(rs, 1) : null;
}
The actual usage code becomes remarkably concise:
private static final ResultSetExtractor<String> EXTRACTOR =
singletonExtractor(STRING_MAPPER);
public String test() {
String sql = "select ID_NMB_SRZ from codb_owner.TR_LTM_SLS_RTN " +
"where id_str_rt = '999' and ID_NMB_SRZ = '60230009999999'";
return jdbc.query(sql, EXTRACTOR);
}
Best Practice: Using Optional to Avoid Null
In modern Java development, using the Optional type is recommended to explicitly represent potentially absent values:
public static <T> ResultSetExtractor<Optional<T>> singletonOptionalExtractor(
RowMapper<? extends T> mapper) {
return rs -> rs.next() ? Optional.of(mapper.mapRow(rs, 1)) : Optional.empty();
}
private static final ResultSetExtractor<Optional<String>> OPTIONAL_EXTRACTOR =
singletonOptionalExtractor(STRING_MAPPER);
public String test() {
String sql = "select ID_NMB_SRZ from codb_owner.TR_LTM_SLS_RTN " +
"where id_str_rt = '999' and ID_NMB_SRZ = '60230009999999'";
return jdbc.query(sql, OPTIONAL_EXTRACTOR).orElse(null);
}
Design Principles and Performance Considerations
Spring's choice to throw exceptions in queryForObject rather than returning null reflects a design philosophy of explicit failure. This approach forces developers to consider all possible execution paths, avoiding potential null pointer exceptions. In terms of performance, directly using ResultSetExtractor is generally more efficient than creating a List first and then extracting elements, especially when handling large numbers of queries.
Conclusion
The key to handling JdbcTemplate empty result exceptions lies in understanding Spring Framework's design intentions and selecting appropriate solutions based on specific scenarios. For simple single-value queries, the query method with conditional checks is the most intuitive choice; for scenarios requiring high performance or complex result processing, ResultSetExtractor offers better flexibility. By combining modern Java features, we can build database access layer code that is both safe and efficient.