Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;

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

/**
* Query given SQL to create a prepared statement from SQL and a list of
* arguments to bind to the query, expecting a result map.
* Execute a query given static SQL, mapping a single result row to a Java
* object via a RowMapper.
* <p>Uses a JDBC Statement, not a PreparedStatement. If you want to
* execute a static query with a PreparedStatement, use the overloaded
* {@link #queryForObject(String, RowMapper, Object...)} method with
* {@code null} as argument array.
* @param sql the SQL query to execute
* @param rowMapper object that will map one object per row
* @return optional with the single mapped object, empty optional if
* no row or {@code null} is returned
* @throws IncorrectResultSizeDataAccessException if the query does
* return more than one row
* @throws DataAccessException if there is any problem executing the query
* @see #queryForOptional(String, Object[], RowMapper)
*/
<T> Optional<T> queryForOptional(String sql, RowMapper<T> rowMapper) throws DataAccessException;

/**
* Execute a query for a result object, given static SQL.
* <p>Uses a JDBC Statement, not a PreparedStatement. If you want to
* execute a static query with a PreparedStatement, use the overloaded
* {@link #queryForOptional(String, Class, Object...)} method with
* {@code null} as argument array.
* <p>This method is useful for running static SQL with a known outcome.
* The query is expected to be a single row/single column query; the returned
* result will be directly mapped to the corresponding object type.
* @param sql the SQL query to execute
* @param requiredType the type that the result object is expected to match
* @return optional with the result object of the required type, empty
* optional if no row is returned or in case of SQL NULL
* @throws IncorrectResultSizeDataAccessException if the query does return
* more than one row, or does not return exactly one column in that row
* @throws DataAccessException if there is any problem executing the query
* @see #queryForOptional(String, Object[], Class)
*/
<T> Optional<T> queryForOptional(String sql, Class<T> requiredType) throws DataAccessException;

/**
* Query given SQL to create a prepared statement from SQL and a list
* of arguments to bind to the query, mapping a single result row to a
* Java object via a RowMapper.
* @param sql the SQL query to execute
* @param args arguments to bind to the query
* (leaving it to the PreparedStatement to guess the corresponding SQL type)
* @param argTypes the SQL types of the arguments
* (constants from {@code java.sql.Types})
* @param rowMapper object that will map one object per row
* @return optional with the single mapped object, empty optional if
* no row or {@code null} is returned
* @throws IncorrectResultSizeDataAccessException if the query does
* return more than one row
* @throws DataAccessException if the query fails
*/
<T> Optional<T> queryForOptional(String sql, Object[] args, int[] argTypes, RowMapper<T> rowMapper) throws DataAccessException;

/**
* Query given SQL to create a prepared statement from SQL and a list
* of arguments to bind to the query, mapping a single result row to a
* Java object via a RowMapper.
* @param sql the SQL query to execute
* @param args arguments to bind to the query
* (leaving it to the PreparedStatement to guess the corresponding SQL type);
* may also contain {@link SqlParameterValue} objects which indicate not
* only the argument value but also the SQL type and optionally the scale
* @param rowMapper object that will map one object per row
* @return optional with the single mapped object, empty optional if
* no row or {@code null} is returned
* @throws IncorrectResultSizeDataAccessException if the query does
* return more than one row
* @throws DataAccessException if the query fails
*/
<T> Optional<T> queryForOptional(String sql, Object[] args, RowMapper<T> rowMapper) throws DataAccessException;

/**
* Query given SQL to create a prepared statement from SQL and a list
* of arguments to bind to the query, mapping a single result row to a
* Java object via a RowMapper.
* @param sql the SQL query to execute
* @param rowMapper object that will map one object per row
* @param args arguments to bind to the query
* (leaving it to the PreparedStatement to guess the corresponding SQL type);
* may also contain {@link SqlParameterValue} objects which indicate not
* only the argument value but also the SQL type and optionally the scale
* @return optional with the single mapped object, empty optional if
* no row or {@code null} is returned
* @throws IncorrectResultSizeDataAccessException if the query does
* return more than one row
* @throws DataAccessException if the query fails
*/
<T> Optional<T> queryForOptional(String sql, RowMapper<T> rowMapper, Object... args) throws DataAccessException;

/**
* Query given SQL to create a prepared statement from SQL and a
* list of arguments to bind to the query, expecting a result object.
* <p>The query is expected to be a single row/single column query; the returned
* result will be directly mapped to the corresponding object type.
* @param sql the SQL query to execute
* @param args arguments to bind to the query
* @param argTypes the SQL types of the arguments
* (constants from {@code java.sql.Types})
* @param requiredType the type that the result object is expected to match
* @return optional with the result object of the required type, empty optional if
* no row is returned or in case of SQL NULL
* @throws IncorrectResultSizeDataAccessException if the query does return
* more than one row, or does not return exactly one column in that row
* @throws DataAccessException if the query fails
* @see #queryForObject(String, Class)
* @see java.sql.Types
*/
<T> Optional<T> queryForOptional(String sql, Object[] args, int[] argTypes, Class<T> requiredType) throws DataAccessException;

/**
* Query given SQL to create a prepared statement from SQL and a
* list of arguments to bind to the query, expecting a result object.
* <p>The query is expected to be a single row/single column query; the returned
* result will be directly mapped to the corresponding object type.
* @param sql the SQL query to execute
* @param args arguments to bind to the query
* (leaving it to the PreparedStatement to guess the corresponding SQL type);
* may also contain {@link SqlParameterValue} objects which indicate not
* only the argument value but also the SQL type and optionally the scale
* @param requiredType the type that the result object is expected to match
* @return optional with the result object of the required type, empty optional if
* no row is returned or in case of SQL NULL
* @throws IncorrectResultSizeDataAccessException if the query does return
* more than one row, or does not return exactly one column in that row
* @throws DataAccessException if the query fails
* @see #queryForObject(String, Class)
*/
<T> Optional<T> queryForOptional(String sql, Object[] args, Class<T> requiredType) throws DataAccessException;

/**
* Query given SQL to create a prepared statement from SQL and a
* list of arguments to bind to the query, expecting a result object.
* <p>The query is expected to be a single row/single column query; the returned
* result will be directly mapped to the corresponding object type.
* @param sql the SQL query to execute
* @param requiredType the type that the result object is expected to match
* @param args arguments to bind to the query
* (leaving it to the PreparedStatement to guess the corresponding SQL type);
* may also contain {@link SqlParameterValue} objects which indicate not
* only the argument value but also the SQL type and optionally the scale
* @return optional with the result object of the required type, empty optional if
* no row is returned or in case of SQL NULL
* @throws IncorrectResultSizeDataAccessException if the query does return
* more than one row, or does not return exactly one column in that row
* @throws DataAccessException if the query fails
* @see #queryForOptional(String, Class)
*/
<T> Optional<T> queryForOptional(String sql, Class<T> requiredType, Object... args) throws DataAccessException;

/**
* Query given SQL to create a prepared statement from SQL and a
* list of arguments to bind to the query, expecting a result Map.
* <p>The query is expected to be a single row query; the result row will be
* mapped to a Map (one entry for each column, using the column name as the key).
* @param sql the SQL query to execute
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Spliterator;
import java.util.function.Consumer;
import java.util.stream.Stream;
Expand Down Expand Up @@ -905,6 +906,50 @@ public <T> T queryForObject(String sql, Class<T> requiredType, @Nullable Object.
return queryForObject(sql, args, getSingleColumnRowMapper(requiredType));
}

@Override
public <T> Optional<T> queryForOptional(String sql, RowMapper<T> rowMapper) throws DataAccessException {
List<T> results = query(sql, rowMapper);
return DataAccessUtils.optional(results);
}

@Override
public <T> Optional<T> queryForOptional(String sql, Class<T> requiredType) throws DataAccessException {
return queryForOptional(sql, getSingleColumnRowMapper(requiredType));
}

@Override
public <T> Optional<T> queryForOptional(String sql, Object[] args, int[] argTypes, RowMapper<T> rowMapper) throws DataAccessException {
List<T> results = query(sql, args, argTypes, new RowMapperResultSetExtractor<T>(rowMapper, 1));
return DataAccessUtils.optional(results);
}

@Override
public <T> Optional<T> queryForOptional(String sql, Object[] args, RowMapper<T> rowMapper) throws DataAccessException {
List<T> results = query(sql, args, new RowMapperResultSetExtractor<T>(rowMapper, 1));
return DataAccessUtils.optional(results);
}

@Override
public <T> Optional<T> queryForOptional(String sql, RowMapper<T> rowMapper, Object... args) throws DataAccessException {
List<T> results = query(sql, args, new RowMapperResultSetExtractor<T>(rowMapper, 1));
return DataAccessUtils.optional(results);
}

@Override
public <T> Optional<T> queryForOptional(String sql, Object[] args, int[] argTypes, Class<T> requiredType) throws DataAccessException {
return queryForOptional(sql, args, argTypes, getSingleColumnRowMapper(requiredType));
}

@Override
public <T> Optional<T> queryForOptional(String sql, Object[] args, Class<T> requiredType) throws DataAccessException {
return queryForOptional(sql, args, getSingleColumnRowMapper(requiredType));
}

@Override
public <T> Optional<T> queryForOptional(String sql, Class<T> requiredType, Object... args) throws DataAccessException {
return queryForOptional(sql, args, getSingleColumnRowMapper(requiredType));
}

@Override
public Map<String, Object> queryForMap(String sql, Object[] args, int[] argTypes) throws DataAccessException {
return result(queryForObject(sql, args, argTypes, getColumnMapRowMapper()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,15 @@
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import javax.sql.DataSource;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import org.springframework.dao.DataAccessException;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.jdbc.BadSqlGrammarException;
import org.springframework.jdbc.CannotGetJdbcConnectionException;
Expand Down Expand Up @@ -85,6 +87,8 @@ public class JdbcTemplateTests {

private ResultSet resultSet;

private ResultSetMetaData resultSetMetaData;

private JdbcTemplate template;

private CallableStatement callableStatement;
Expand All @@ -97,6 +101,7 @@ public void setup() throws Exception {
this.preparedStatement = mock(PreparedStatement.class);
this.statement = mock(Statement.class);
this.resultSet = mock(ResultSet.class);
this.resultSetMetaData = mock(ResultSetMetaData.class);
this.template = new JdbcTemplate(this.dataSource);
this.callableStatement = mock(CallableStatement.class);
given(this.dataSource.getConnection()).willReturn(this.connection);
Expand All @@ -108,6 +113,7 @@ public void setup() throws Exception {
given(this.statement.executeQuery(anyString())).willReturn(this.resultSet);
given(this.connection.prepareCall(anyString())).willReturn(this.callableStatement);
given(this.callableStatement.getResultSet()).willReturn(this.resultSet);
given(this.resultSet.getMetaData()).willReturn(this.resultSetMetaData);
}


Expand Down Expand Up @@ -248,6 +254,52 @@ public String[] getStrings() {
verify(this.connection).close();
}

@Test
public void testQueryForOptionalNoRow() throws SQLException {
given(this.resultSet.next()).willReturn(false);
given(this.connection.createStatement()).willReturn(this.preparedStatement);
given(this.resultSetMetaData.getColumnCount()).willReturn(1);

Optional<String> optional = this.template.queryForOptional("SELECT * FROM dual", String.class);
assertThat(optional).isNotPresent();
}

@Test
public void testQueryForOptionalOneRow() throws SQLException {
String result = "X";
given(this.resultSet.next()).willReturn(true, false);
given(this.resultSet.getString(1)).willReturn(result);
given(this.connection.createStatement()).willReturn(this.preparedStatement);
given(this.resultSetMetaData.getColumnCount()).willReturn(1);

Optional<String> optional = this.template.queryForOptional("SELECT * FROM dual", String.class);
assertThat(optional).isPresent();
assertThat(optional).hasValue(result);
}

@Test
public void testQueryForOptionalNull() throws SQLException {
given(this.resultSet.next()).willReturn(true, false);
given(this.resultSet.getString(1)).willReturn(null);
given(this.connection.createStatement()).willReturn(this.preparedStatement);
given(this.resultSetMetaData.getColumnCount()).willReturn(1);

Optional<String> optional = this.template.queryForOptional("SELECT NULL FROM dual", String.class);
assertThat(optional).isNotPresent();
}

@Test
public void testQueryForOptionalTwoRows() throws SQLException {
String result = "X";
given(this.resultSet.next()).willReturn(true, true, false);
given(this.resultSet.getString(1)).willReturn(result);
given(this.connection.createStatement()).willReturn(this.preparedStatement);
given(this.resultSetMetaData.getColumnCount()).willReturn(1);

assertThatExceptionOfType(IncorrectResultSizeDataAccessException.class).isThrownBy(() ->
this.template.queryForOptional("SELECT * FROM dual UNION ALL SELECT * FROM dual", String.class));
}

@Test
public void testLeaveConnectionOpenOnRequest() throws Exception {
String sql = "SELECT ID, FORENAME FROM CUSTMR WHERE ID < 3";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -17,6 +17,7 @@
package org.springframework.dao.support;

import java.util.Collection;
import java.util.Optional;

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

/**
* Return a single result optional from the given Collection.
* <p>Throws an exception if more than 1 element found.
* @param results the result Collection (can be {@code null})
* @return the single result object
* @throws IncorrectResultSizeDataAccessException if more than one
* element has been found in the given Collection
*/
public static <T> Optional<T> optional(Collection<T> results) {
int size = (results != null ? results.size() : 0);
if (size == 0) {
return Optional.empty();
}
if (size > 1) {
throw new IncorrectResultSizeDataAccessException(1, size);
}
return Optional.ofNullable(results.iterator().next());
}

/**
* Return a unique result object from the given Collection.
* <p>Returns {@code null} if 0 result objects found;
Expand Down