Introduce Stream variant methods for SqlQuery

Closes GH-34474

Signed-off-by: Yanming Zhou <zhouyanming@gmail.com>
This commit is contained in:
Yanming Zhou
2025-02-24 11:43:08 +08:00
committed by Juergen Hoeller
parent b7a9bee020
commit 8819c749ce
2 changed files with 150 additions and 7 deletions

View File

@@ -18,6 +18,8 @@ package org.springframework.jdbc.object;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.stream.Stream;
import javax.sql.DataSource;
@@ -25,6 +27,7 @@ import org.jspecify.annotations.Nullable;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.support.DataAccessUtils;
import org.springframework.jdbc.core.PreparedStatementCreator;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterUtils;
@@ -52,6 +55,7 @@ import org.springframework.jdbc.core.namedparam.ParsedSql;
* @author Rod Johnson
* @author Juergen Hoeller
* @author Thomas Risberg
* @author Yanming Zhou
* @param <T> the result type
* @see SqlUpdate
*/
@@ -94,6 +98,23 @@ public abstract class SqlQuery<T> extends SqlOperation {
return getJdbcTemplate().query(newPreparedStatementCreator(params), rowMapper);
}
/**
* Central stream method. All un-named parameter execution goes through this method.
* @param params parameters, similar to JDO query parameters.
* Primitive parameters must be represented by their Object wrapper type.
* The ordering of parameters is significant.
* @param context the contextual information passed to the {@code mapRow}
* callback method. The JDBC operation itself doesn't rely on this parameter,
* but it can be useful for creating the objects of the result list.
* @return a result Stream of objects, one per row of the ResultSet. Normally all these
* will be of the same class, although it is possible to use different types.
*/
public Stream<T> stream(Object @Nullable [] params, @Nullable Map<?, ?> context) throws DataAccessException {
validateParameters(params);
RowMapper<T> rowMapper = newRowMapper(params, context);
return getJdbcTemplate().queryForStream(newPreparedStatementCreator(params), rowMapper);
}
/**
* Convenient method to execute without context.
* @param params parameters for the query. Primitive parameters must
@@ -104,6 +125,16 @@ public abstract class SqlQuery<T> extends SqlOperation {
return execute(params, null);
}
/**
* Convenient method to stream without context.
* @param params parameters for the query. Primitive parameters must
* be represented by their Object wrapper type. The ordering of parameters is
* significant.
*/
public Stream<T> stream(Object... params) throws DataAccessException {
return stream(params, null);
}
/**
* Convenient method to execute without parameters.
* @param context the contextual information for object creation
@@ -112,6 +143,14 @@ public abstract class SqlQuery<T> extends SqlOperation {
return execute((Object[]) null, context);
}
/**
* Convenient method to stream without parameters.
* @param context the contextual information for object creation
*/
public Stream<T> stream(Map<?, ?> context) throws DataAccessException {
return stream(null, context);
}
/**
* Convenient method to execute without parameters nor context.
*/
@@ -119,6 +158,13 @@ public abstract class SqlQuery<T> extends SqlOperation {
return execute((Object[]) null, null);
}
/**
* Convenient method to stream without parameters nor context.
*/
public Stream<T> stream() throws DataAccessException {
return stream(null, null);
}
/**
* Convenient method to execute with a single int parameter and context.
* @param p1 single int parameter
@@ -202,13 +248,23 @@ public abstract class SqlQuery<T> extends SqlOperation {
* will be of the same class, although it is possible to use different types.
*/
public List<T> executeByNamedParam(Map<String, ?> paramMap, @Nullable Map<?, ?> context) throws DataAccessException {
validateNamedParameters(paramMap);
ParsedSql parsedSql = getParsedSql();
MapSqlParameterSource paramSource = new MapSqlParameterSource(paramMap);
String sqlToUse = NamedParameterUtils.substituteNamedParameters(parsedSql, paramSource);
@Nullable Object[] params = NamedParameterUtils.buildValueArray(parsedSql, paramSource, getDeclaredParameters());
RowMapper<T> rowMapper = newRowMapper(params, context);
return getJdbcTemplate().query(newPreparedStatementCreator(sqlToUse, params), rowMapper);
return queryByNamedParam(paramMap, context, getJdbcTemplate()::query);
}
/**
* Central stream method. All named parameter execution goes through this method.
* @param paramMap parameters associated with the name specified while declaring
* the SqlParameters. Primitive parameters must be represented by their Object wrapper
* type. The ordering of parameters is not significant since they are supplied in a
* SqlParameterMap which is an implementation of the Map interface.
* @param context the contextual information passed to the {@code mapRow}
* callback method. The JDBC operation itself doesn't rely on this parameter,
* but it can be useful for creating the objects of the result list.
* @return a Stream of objects, one per row of the ResultSet. Normally all these
* will be of the same class, although it is possible to use different types.
*/
public Stream<T> streamByNamedParam(Map<String, ?> paramMap, @Nullable Map<?, ?> context) throws DataAccessException {
return queryByNamedParam(paramMap, context, getJdbcTemplate()::queryForStream);
}
/**
@@ -221,6 +277,15 @@ public abstract class SqlQuery<T> extends SqlOperation {
return executeByNamedParam(paramMap, null);
}
/**
* Convenient method to stream without context.
* @param paramMap parameters associated with the name specified while declaring
* the SqlParameters. Primitive parameters must be represented by their Object wrapper
* type. The ordering of parameters is not significant.
*/
public Stream<T> streamByNamedParam(Map<String, ? extends @Nullable Object> paramMap) throws DataAccessException {
return streamByNamedParam(paramMap, null);
}
/**
* Generic object finder method, used by all other {@code findObject} methods.
@@ -342,4 +407,14 @@ public abstract class SqlQuery<T> extends SqlOperation {
*/
protected abstract RowMapper<T> newRowMapper(@Nullable Object @Nullable [] parameters, @Nullable Map<?, ?> context);
private <R> R queryByNamedParam(Map<String, ?> paramMap, @Nullable Map<?, ?> context, BiFunction<PreparedStatementCreator, RowMapper<T>, R> queryFunction) {
validateNamedParameters(paramMap);
ParsedSql parsedSql = getParsedSql();
MapSqlParameterSource paramSource = new MapSqlParameterSource(paramMap);
String sqlToUse = NamedParameterUtils.substituteNamedParameters(parsedSql, paramSource);
@Nullable Object[] params = NamedParameterUtils.buildValueArray(parsedSql, paramSource, getDeclaredParameters());
RowMapper<T> rowMapper = newRowMapper(params, context);
return queryFunction.apply(newPreparedStatementCreator(sqlToUse, params), rowMapper);
}
}

View File

@@ -26,6 +26,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import javax.sql.DataSource;
@@ -52,6 +53,7 @@ import static org.mockito.Mockito.verify;
* @author Trevor Cook
* @author Thomas Risberg
* @author Juergen Hoeller
* @author Yanming Zhou
*/
class SqlQueryTests {
@@ -125,6 +127,72 @@ class SqlQueryTests {
verify(preparedStatement).close();
}
@Test
void testStreamWithoutParams() throws SQLException {
given(resultSet.next()).willReturn(true, false);
given(resultSet.getInt(1)).willReturn(1);
SqlQuery<Integer> query = new MappingSqlQueryWithParameters<>() {
@Override
protected Integer mapRow(ResultSet rs, int rownum, Object @Nullable [] params, @Nullable Map<? ,?> context)
throws SQLException {
assertThat(params).as("params were null").isNull();
assertThat(context).as("context was null").isNull();
return rs.getInt(1);
}
};
query.setDataSource(dataSource);
query.setSql(SELECT_ID);
query.compile();
try (Stream<Integer> stream = query.stream()) {
List<Integer> list = stream.toList();
assertThat(list).containsExactly(1);
}
verify(connection).prepareStatement(SELECT_ID);
verify(resultSet).close();
verify(preparedStatement).close();
}
@Test
void testStreamByNamedParam() throws SQLException {
given(resultSet.next()).willReturn(true, false);
given(resultSet.getInt("id")).willReturn(1);
given(resultSet.getString("forename")).willReturn("rod");
given(connection.prepareStatement(SELECT_ID_FORENAME_NAMED_PARAMETERS_PARSED,
ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY)
).willReturn(preparedStatement);
SqlQuery<Customer> query = new MappingSqlQueryWithParameters<>() {
@Override
protected Customer mapRow(ResultSet rs, int rownum, Object @Nullable [] params, @Nullable Map<? ,?> context)
throws SQLException {
assertThat(params).as("params were not null").isNotNull();
assertThat(context).as("context was null").isNull();
Customer cust = new Customer();
cust.setId(rs.getInt(COLUMN_NAMES[0]));
cust.setForename(rs.getString(COLUMN_NAMES[1]));
return cust;
}
};
query.declareParameter(new SqlParameter("id", Types.NUMERIC));
query.declareParameter(new SqlParameter("country", Types.VARCHAR));
query.setDataSource(dataSource);
query.setSql(SELECT_ID_FORENAME_NAMED_PARAMETERS);
query.compile();
try (Stream<Customer> stream = query.streamByNamedParam(Map.of("id", 1, "country", "UK"))) {
List<Customer> list = stream.toList();
assertThat(list).hasSize(1);
Customer customer = list.get(0);
assertThat(customer.getId()).isEqualTo(1);
assertThat(customer.getForename()).isEqualTo("rod");
}
verify(connection).prepareStatement(SELECT_ID_FORENAME_NAMED_PARAMETERS_PARSED);
verify(preparedStatement).setObject(1, 1, Types.NUMERIC);
verify(preparedStatement).setString(2, "UK");
verify(resultSet).close();
verify(preparedStatement).close();
}
@Test
void testQueryWithoutEnoughParams() {
MappingSqlQuery<Integer> query = new MappingSqlQuery<>() {