Skip to content

Commit d82bd0c

Browse files
committed
Add Optional Support to JdbcTemplate
Extend `JdbcTemplate` to support `Optional` for cases where queries return one or no row. - Add a `queryForOptional` method for every `queryForObject` method in `JdbcOperations`. - Implement the new methods in `JdbcTemplate`. - Add tests for the new methods in `JdbcTemplate`. Issue: SPR-12662
1 parent 51cc719 commit d82bd0c

File tree

4 files changed

+273
-3
lines changed

4 files changed

+273
-3
lines changed

spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcOperations.java

Lines changed: 155 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.Collection;
2020
import java.util.List;
2121
import java.util.Map;
22+
import java.util.Optional;
2223
import java.util.stream.Stream;
2324

2425
import org.springframework.dao.DataAccessException;
@@ -717,8 +718,160 @@ <T> T queryForObject(String sql, Object[] args, int[] argTypes, Class<T> require
717718
<T> T queryForObject(String sql, Class<T> requiredType, @Nullable Object... args) throws DataAccessException;
718719

719720
/**
720-
* Query given SQL to create a prepared statement from SQL and a list of
721-
* arguments to bind to the query, expecting a result map.
721+
* Execute a query given static SQL, mapping a single result row to a Java
722+
* object via a RowMapper.
723+
* <p>Uses a JDBC Statement, not a PreparedStatement. If you want to
724+
* execute a static query with a PreparedStatement, use the overloaded
725+
* {@link #queryForObject(String, RowMapper, Object...)} method with
726+
* {@code null} as argument array.
727+
* @param sql the SQL query to execute
728+
* @param rowMapper object that will map one object per row
729+
* @return optional with the single mapped object, empty optional if
730+
* no row or {@code null} is returned
731+
* @throws IncorrectResultSizeDataAccessException if the query does
732+
* return more than one row
733+
* @throws DataAccessException if there is any problem executing the query
734+
* @see #queryForOptional(String, Object[], RowMapper)
735+
*/
736+
<T> Optional<T> queryForOptional(String sql, RowMapper<T> rowMapper) throws DataAccessException;
737+
738+
/**
739+
* Execute a query for a result object, given static SQL.
740+
* <p>Uses a JDBC Statement, not a PreparedStatement. If you want to
741+
* execute a static query with a PreparedStatement, use the overloaded
742+
* {@link #queryForOptional(String, Class, Object...)} method with
743+
* {@code null} as argument array.
744+
* <p>This method is useful for running static SQL with a known outcome.
745+
* The query is expected to be a single row/single column query; the returned
746+
* result will be directly mapped to the corresponding object type.
747+
* @param sql the SQL query to execute
748+
* @param requiredType the type that the result object is expected to match
749+
* @return optional with the result object of the required type, empty
750+
* optional if no row is returned or in case of SQL NULL
751+
* @throws IncorrectResultSizeDataAccessException if the query does return
752+
* more than one row, or does not return exactly one column in that row
753+
* @throws DataAccessException if there is any problem executing the query
754+
* @see #queryForOptional(String, Object[], Class)
755+
*/
756+
<T> Optional<T> queryForOptional(String sql, Class<T> requiredType) throws DataAccessException;
757+
758+
/**
759+
* Query given SQL to create a prepared statement from SQL and a list
760+
* of arguments to bind to the query, mapping a single result row to a
761+
* Java object via a RowMapper.
762+
* @param sql the SQL query to execute
763+
* @param args arguments to bind to the query
764+
* (leaving it to the PreparedStatement to guess the corresponding SQL type)
765+
* @param argTypes the SQL types of the arguments
766+
* (constants from {@code java.sql.Types})
767+
* @param rowMapper object that will map one object per row
768+
* @return optional with the single mapped object, empty optional if
769+
* no row or {@code null} is returned
770+
* @throws IncorrectResultSizeDataAccessException if the query does
771+
* return more than one row
772+
* @throws DataAccessException if the query fails
773+
*/
774+
<T> Optional<T> queryForOptional(String sql, Object[] args, int[] argTypes, RowMapper<T> rowMapper) throws DataAccessException;
775+
776+
/**
777+
* Query given SQL to create a prepared statement from SQL and a list
778+
* of arguments to bind to the query, mapping a single result row to a
779+
* Java object via a RowMapper.
780+
* @param sql the SQL query to execute
781+
* @param args arguments to bind to the query
782+
* (leaving it to the PreparedStatement to guess the corresponding SQL type);
783+
* may also contain {@link SqlParameterValue} objects which indicate not
784+
* only the argument value but also the SQL type and optionally the scale
785+
* @param rowMapper object that will map one object per row
786+
* @return optional with the single mapped object, empty optional if
787+
* no row or {@code null} is returned
788+
* @throws IncorrectResultSizeDataAccessException if the query does
789+
* return more than one row
790+
* @throws DataAccessException if the query fails
791+
*/
792+
<T> Optional<T> queryForOptional(String sql, Object[] args, RowMapper<T> rowMapper) throws DataAccessException;
793+
794+
/**
795+
* Query given SQL to create a prepared statement from SQL and a list
796+
* of arguments to bind to the query, mapping a single result row to a
797+
* Java object via a RowMapper.
798+
* @param sql the SQL query to execute
799+
* @param rowMapper object that will map one object per row
800+
* @param args arguments to bind to the query
801+
* (leaving it to the PreparedStatement to guess the corresponding SQL type);
802+
* may also contain {@link SqlParameterValue} objects which indicate not
803+
* only the argument value but also the SQL type and optionally the scale
804+
* @return optional with the single mapped object, empty optional if
805+
* no row or {@code null} is returned
806+
* @throws IncorrectResultSizeDataAccessException if the query does
807+
* return more than one row
808+
* @throws DataAccessException if the query fails
809+
*/
810+
<T> Optional<T> queryForOptional(String sql, RowMapper<T> rowMapper, Object... args) throws DataAccessException;
811+
812+
/**
813+
* Query given SQL to create a prepared statement from SQL and a
814+
* list of arguments to bind to the query, expecting a result object.
815+
* <p>The query is expected to be a single row/single column query; the returned
816+
* result will be directly mapped to the corresponding object type.
817+
* @param sql the SQL query to execute
818+
* @param args arguments to bind to the query
819+
* @param argTypes the SQL types of the arguments
820+
* (constants from {@code java.sql.Types})
821+
* @param requiredType the type that the result object is expected to match
822+
* @return optional with the result object of the required type, empty optional if
823+
* no row is returned or in case of SQL NULL
824+
* @throws IncorrectResultSizeDataAccessException if the query does return
825+
* more than one row, or does not return exactly one column in that row
826+
* @throws DataAccessException if the query fails
827+
* @see #queryForObject(String, Class)
828+
* @see java.sql.Types
829+
*/
830+
<T> Optional<T> queryForOptional(String sql, Object[] args, int[] argTypes, Class<T> requiredType) throws DataAccessException;
831+
832+
/**
833+
* Query given SQL to create a prepared statement from SQL and a
834+
* list of arguments to bind to the query, expecting a result object.
835+
* <p>The query is expected to be a single row/single column query; the returned
836+
* result will be directly mapped to the corresponding object type.
837+
* @param sql the SQL query to execute
838+
* @param args arguments to bind to the query
839+
* (leaving it to the PreparedStatement to guess the corresponding SQL type);
840+
* may also contain {@link SqlParameterValue} objects which indicate not
841+
* only the argument value but also the SQL type and optionally the scale
842+
* @param requiredType the type that the result object is expected to match
843+
* @return optional with the result object of the required type, empty optional if
844+
* no row is returned or in case of SQL NULL
845+
* @throws IncorrectResultSizeDataAccessException if the query does return
846+
* more than one row, or does not return exactly one column in that row
847+
* @throws DataAccessException if the query fails
848+
* @see #queryForObject(String, Class)
849+
*/
850+
<T> Optional<T> queryForOptional(String sql, Object[] args, Class<T> requiredType) throws DataAccessException;
851+
852+
/**
853+
* Query given SQL to create a prepared statement from SQL and a
854+
* list of arguments to bind to the query, expecting a result object.
855+
* <p>The query is expected to be a single row/single column query; the returned
856+
* result will be directly mapped to the corresponding object type.
857+
* @param sql the SQL query to execute
858+
* @param requiredType the type that the result object is expected to match
859+
* @param args arguments to bind to the query
860+
* (leaving it to the PreparedStatement to guess the corresponding SQL type);
861+
* may also contain {@link SqlParameterValue} objects which indicate not
862+
* only the argument value but also the SQL type and optionally the scale
863+
* @return optional with the result object of the required type, empty optional if
864+
* no row is returned or in case of SQL NULL
865+
* @throws IncorrectResultSizeDataAccessException if the query does return
866+
* more than one row, or does not return exactly one column in that row
867+
* @throws DataAccessException if the query fails
868+
* @see #queryForOptional(String, Class)
869+
*/
870+
<T> Optional<T> queryForOptional(String sql, Class<T> requiredType, Object... args) throws DataAccessException;
871+
872+
/**
873+
* Query given SQL to create a prepared statement from SQL and a
874+
* list of arguments to bind to the query, expecting a result Map.
722875
* <p>The query is expected to be a single row query; the result row will be
723876
* mapped to a Map (one entry for each column, using the column name as the key).
724877
* @param sql the SQL query to execute

spring-jdbc/src/main/java/org/springframework/jdbc/core/JdbcTemplate.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import java.util.LinkedHashMap;
3535
import java.util.List;
3636
import java.util.Map;
37+
import java.util.Optional;
3738
import java.util.Spliterator;
3839
import java.util.function.Consumer;
3940
import java.util.stream.Stream;
@@ -905,6 +906,50 @@ public <T> T queryForObject(String sql, Class<T> requiredType, @Nullable Object.
905906
return queryForObject(sql, args, getSingleColumnRowMapper(requiredType));
906907
}
907908

909+
@Override
910+
public <T> Optional<T> queryForOptional(String sql, RowMapper<T> rowMapper) throws DataAccessException {
911+
List<T> results = query(sql, rowMapper);
912+
return DataAccessUtils.optional(results);
913+
}
914+
915+
@Override
916+
public <T> Optional<T> queryForOptional(String sql, Class<T> requiredType) throws DataAccessException {
917+
return queryForOptional(sql, getSingleColumnRowMapper(requiredType));
918+
}
919+
920+
@Override
921+
public <T> Optional<T> queryForOptional(String sql, Object[] args, int[] argTypes, RowMapper<T> rowMapper) throws DataAccessException {
922+
List<T> results = query(sql, args, argTypes, new RowMapperResultSetExtractor<T>(rowMapper, 1));
923+
return DataAccessUtils.optional(results);
924+
}
925+
926+
@Override
927+
public <T> Optional<T> queryForOptional(String sql, Object[] args, RowMapper<T> rowMapper) throws DataAccessException {
928+
List<T> results = query(sql, args, new RowMapperResultSetExtractor<T>(rowMapper, 1));
929+
return DataAccessUtils.optional(results);
930+
}
931+
932+
@Override
933+
public <T> Optional<T> queryForOptional(String sql, RowMapper<T> rowMapper, Object... args) throws DataAccessException {
934+
List<T> results = query(sql, args, new RowMapperResultSetExtractor<T>(rowMapper, 1));
935+
return DataAccessUtils.optional(results);
936+
}
937+
938+
@Override
939+
public <T> Optional<T> queryForOptional(String sql, Object[] args, int[] argTypes, Class<T> requiredType) throws DataAccessException {
940+
return queryForOptional(sql, args, argTypes, getSingleColumnRowMapper(requiredType));
941+
}
942+
943+
@Override
944+
public <T> Optional<T> queryForOptional(String sql, Object[] args, Class<T> requiredType) throws DataAccessException {
945+
return queryForOptional(sql, args, getSingleColumnRowMapper(requiredType));
946+
}
947+
948+
@Override
949+
public <T> Optional<T> queryForOptional(String sql, Class<T> requiredType, Object... args) throws DataAccessException {
950+
return queryForOptional(sql, args, getSingleColumnRowMapper(requiredType));
951+
}
952+
908953
@Override
909954
public Map<String, Object> queryForMap(String sql, Object[] args, int[] argTypes) throws DataAccessException {
910955
return result(queryForObject(sql, args, argTypes, getColumnMapRowMapper()));

spring-jdbc/src/test/java/org/springframework/jdbc/core/JdbcTemplateTests.java

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,15 @@
3333
import java.util.LinkedList;
3434
import java.util.List;
3535
import java.util.Map;
36+
import java.util.Optional;
3637

3738
import javax.sql.DataSource;
3839

3940
import org.junit.jupiter.api.BeforeEach;
4041
import org.junit.jupiter.api.Test;
4142

4243
import org.springframework.dao.DataAccessException;
44+
import org.springframework.dao.IncorrectResultSizeDataAccessException;
4345
import org.springframework.dao.InvalidDataAccessApiUsageException;
4446
import org.springframework.jdbc.BadSqlGrammarException;
4547
import org.springframework.jdbc.CannotGetJdbcConnectionException;
@@ -85,6 +87,8 @@ public class JdbcTemplateTests {
8587

8688
private ResultSet resultSet;
8789

90+
private ResultSetMetaData resultSetMetaData;
91+
8892
private JdbcTemplate template;
8993

9094
private CallableStatement callableStatement;
@@ -97,6 +101,7 @@ public void setup() throws Exception {
97101
this.preparedStatement = mock(PreparedStatement.class);
98102
this.statement = mock(Statement.class);
99103
this.resultSet = mock(ResultSet.class);
104+
this.resultSetMetaData = mock(ResultSetMetaData.class);
100105
this.template = new JdbcTemplate(this.dataSource);
101106
this.callableStatement = mock(CallableStatement.class);
102107
given(this.dataSource.getConnection()).willReturn(this.connection);
@@ -108,6 +113,7 @@ public void setup() throws Exception {
108113
given(this.statement.executeQuery(anyString())).willReturn(this.resultSet);
109114
given(this.connection.prepareCall(anyString())).willReturn(this.callableStatement);
110115
given(this.callableStatement.getResultSet()).willReturn(this.resultSet);
116+
given(this.resultSet.getMetaData()).willReturn(this.resultSetMetaData);
111117
}
112118

113119

@@ -248,6 +254,52 @@ public String[] getStrings() {
248254
verify(this.connection).close();
249255
}
250256

257+
@Test
258+
public void testQueryForOptionalNoRow() throws SQLException {
259+
given(this.resultSet.next()).willReturn(false);
260+
given(this.connection.createStatement()).willReturn(this.preparedStatement);
261+
given(this.resultSetMetaData.getColumnCount()).willReturn(1);
262+
263+
Optional<String> optional = this.template.queryForOptional("SELECT * FROM dual", String.class);
264+
assertThat(optional).isNotPresent();
265+
}
266+
267+
@Test
268+
public void testQueryForOptionalOneRow() throws SQLException {
269+
String result = "X";
270+
given(this.resultSet.next()).willReturn(true, false);
271+
given(this.resultSet.getString(1)).willReturn(result);
272+
given(this.connection.createStatement()).willReturn(this.preparedStatement);
273+
given(this.resultSetMetaData.getColumnCount()).willReturn(1);
274+
275+
Optional<String> optional = this.template.queryForOptional("SELECT * FROM dual", String.class);
276+
assertThat(optional).isPresent();
277+
assertThat(optional).hasValue(result);
278+
}
279+
280+
@Test
281+
public void testQueryForOptionalNull() throws SQLException {
282+
given(this.resultSet.next()).willReturn(true, false);
283+
given(this.resultSet.getString(1)).willReturn(null);
284+
given(this.connection.createStatement()).willReturn(this.preparedStatement);
285+
given(this.resultSetMetaData.getColumnCount()).willReturn(1);
286+
287+
Optional<String> optional = this.template.queryForOptional("SELECT NULL FROM dual", String.class);
288+
assertThat(optional).isNotPresent();
289+
}
290+
291+
@Test
292+
public void testQueryForOptionalTwoRows() throws SQLException {
293+
String result = "X";
294+
given(this.resultSet.next()).willReturn(true, true, false);
295+
given(this.resultSet.getString(1)).willReturn(result);
296+
given(this.connection.createStatement()).willReturn(this.preparedStatement);
297+
given(this.resultSetMetaData.getColumnCount()).willReturn(1);
298+
299+
assertThatExceptionOfType(IncorrectResultSizeDataAccessException.class).isThrownBy(() ->
300+
this.template.queryForOptional("SELECT * FROM dual UNION ALL SELECT * FROM dual", String.class));
301+
}
302+
251303
@Test
252304
public void testLeaveConnectionOpenOnRequest() throws Exception {
253305
String sql = "SELECT ID, FORENAME FROM CUSTMR WHERE ID < 3";

spring-tx/src/main/java/org/springframework/dao/support/DataAccessUtils.java

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
1717
package org.springframework.dao.support;
1818

1919
import java.util.Collection;
20+
import java.util.Optional;
2021

2122
import org.springframework.dao.DataAccessException;
2223
import org.springframework.dao.EmptyResultDataAccessException;
@@ -102,6 +103,25 @@ public static <T> T nullableSingleResult(@Nullable Collection<T> results) throws
102103
return results.iterator().next();
103104
}
104105

106+
/**
107+
* Return a single result optional from the given Collection.
108+
* <p>Throws an exception if more than 1 element found.
109+
* @param results the result Collection (can be {@code null})
110+
* @return the single result object
111+
* @throws IncorrectResultSizeDataAccessException if more than one
112+
* element has been found in the given Collection
113+
*/
114+
public static <T> Optional<T> optional(Collection<T> results) {
115+
int size = (results != null ? results.size() : 0);
116+
if (size == 0) {
117+
return Optional.empty();
118+
}
119+
if (size > 1) {
120+
throw new IncorrectResultSizeDataAccessException(1, size);
121+
}
122+
return Optional.ofNullable(results.iterator().next());
123+
}
124+
105125
/**
106126
* Return a unique result object from the given Collection.
107127
* <p>Returns {@code null} if 0 result objects found;

0 commit comments

Comments
 (0)