Skip to content

Commit 8819c74

Browse files
quaffjhoeller
authored andcommitted
Introduce Stream variant methods for SqlQuery
Closes GH-34474 Signed-off-by: Yanming Zhou <[email protected]>
1 parent b7a9bee commit 8819c74

File tree

2 files changed

+150
-7
lines changed

2 files changed

+150
-7
lines changed

spring-jdbc/src/main/java/org/springframework/jdbc/object/SqlQuery.java

+82-7
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,16 @@
1818

1919
import java.util.List;
2020
import java.util.Map;
21+
import java.util.function.BiFunction;
22+
import java.util.stream.Stream;
2123

2224
import javax.sql.DataSource;
2325

2426
import org.jspecify.annotations.Nullable;
2527

2628
import org.springframework.dao.DataAccessException;
2729
import org.springframework.dao.support.DataAccessUtils;
30+
import org.springframework.jdbc.core.PreparedStatementCreator;
2831
import org.springframework.jdbc.core.RowMapper;
2932
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
3033
import org.springframework.jdbc.core.namedparam.NamedParameterUtils;
@@ -52,6 +55,7 @@
5255
* @author Rod Johnson
5356
* @author Juergen Hoeller
5457
* @author Thomas Risberg
58+
* @author Yanming Zhou
5559
* @param <T> the result type
5660
* @see SqlUpdate
5761
*/
@@ -94,6 +98,23 @@ public List<T> execute(Object @Nullable [] params, @Nullable Map<?, ?> context)
9498
return getJdbcTemplate().query(newPreparedStatementCreator(params), rowMapper);
9599
}
96100

101+
/**
102+
* Central stream method. All un-named parameter execution goes through this method.
103+
* @param params parameters, similar to JDO query parameters.
104+
* Primitive parameters must be represented by their Object wrapper type.
105+
* The ordering of parameters is significant.
106+
* @param context the contextual information passed to the {@code mapRow}
107+
* callback method. The JDBC operation itself doesn't rely on this parameter,
108+
* but it can be useful for creating the objects of the result list.
109+
* @return a result Stream of objects, one per row of the ResultSet. Normally all these
110+
* will be of the same class, although it is possible to use different types.
111+
*/
112+
public Stream<T> stream(Object @Nullable [] params, @Nullable Map<?, ?> context) throws DataAccessException {
113+
validateParameters(params);
114+
RowMapper<T> rowMapper = newRowMapper(params, context);
115+
return getJdbcTemplate().queryForStream(newPreparedStatementCreator(params), rowMapper);
116+
}
117+
97118
/**
98119
* Convenient method to execute without context.
99120
* @param params parameters for the query. Primitive parameters must
@@ -104,6 +125,16 @@ public List<T> execute(Object... params) throws DataAccessException {
104125
return execute(params, null);
105126
}
106127

128+
/**
129+
* Convenient method to stream without context.
130+
* @param params parameters for the query. Primitive parameters must
131+
* be represented by their Object wrapper type. The ordering of parameters is
132+
* significant.
133+
*/
134+
public Stream<T> stream(Object... params) throws DataAccessException {
135+
return stream(params, null);
136+
}
137+
107138
/**
108139
* Convenient method to execute without parameters.
109140
* @param context the contextual information for object creation
@@ -112,13 +143,28 @@ public List<T> execute(Map<?, ?> context) throws DataAccessException {
112143
return execute((Object[]) null, context);
113144
}
114145

146+
/**
147+
* Convenient method to stream without parameters.
148+
* @param context the contextual information for object creation
149+
*/
150+
public Stream<T> stream(Map<?, ?> context) throws DataAccessException {
151+
return stream(null, context);
152+
}
153+
115154
/**
116155
* Convenient method to execute without parameters nor context.
117156
*/
118157
public List<T> execute() throws DataAccessException {
119158
return execute((Object[]) null, null);
120159
}
121160

161+
/**
162+
* Convenient method to stream without parameters nor context.
163+
*/
164+
public Stream<T> stream() throws DataAccessException {
165+
return stream(null, null);
166+
}
167+
122168
/**
123169
* Convenient method to execute with a single int parameter and context.
124170
* @param p1 single int parameter
@@ -202,13 +248,23 @@ public List<T> execute(String p1) throws DataAccessException {
202248
* will be of the same class, although it is possible to use different types.
203249
*/
204250
public List<T> executeByNamedParam(Map<String, ?> paramMap, @Nullable Map<?, ?> context) throws DataAccessException {
205-
validateNamedParameters(paramMap);
206-
ParsedSql parsedSql = getParsedSql();
207-
MapSqlParameterSource paramSource = new MapSqlParameterSource(paramMap);
208-
String sqlToUse = NamedParameterUtils.substituteNamedParameters(parsedSql, paramSource);
209-
@Nullable Object[] params = NamedParameterUtils.buildValueArray(parsedSql, paramSource, getDeclaredParameters());
210-
RowMapper<T> rowMapper = newRowMapper(params, context);
211-
return getJdbcTemplate().query(newPreparedStatementCreator(sqlToUse, params), rowMapper);
251+
return queryByNamedParam(paramMap, context, getJdbcTemplate()::query);
252+
}
253+
254+
/**
255+
* Central stream method. All named parameter execution goes through this method.
256+
* @param paramMap parameters associated with the name specified while declaring
257+
* the SqlParameters. Primitive parameters must be represented by their Object wrapper
258+
* type. The ordering of parameters is not significant since they are supplied in a
259+
* SqlParameterMap which is an implementation of the Map interface.
260+
* @param context the contextual information passed to the {@code mapRow}
261+
* callback method. The JDBC operation itself doesn't rely on this parameter,
262+
* but it can be useful for creating the objects of the result list.
263+
* @return a Stream of objects, one per row of the ResultSet. Normally all these
264+
* will be of the same class, although it is possible to use different types.
265+
*/
266+
public Stream<T> streamByNamedParam(Map<String, ?> paramMap, @Nullable Map<?, ?> context) throws DataAccessException {
267+
return queryByNamedParam(paramMap, context, getJdbcTemplate()::queryForStream);
212268
}
213269

214270
/**
@@ -221,6 +277,15 @@ public List<T> executeByNamedParam(Map<String, ? extends @Nullable Object> param
221277
return executeByNamedParam(paramMap, null);
222278
}
223279

280+
/**
281+
* Convenient method to stream without context.
282+
* @param paramMap parameters associated with the name specified while declaring
283+
* the SqlParameters. Primitive parameters must be represented by their Object wrapper
284+
* type. The ordering of parameters is not significant.
285+
*/
286+
public Stream<T> streamByNamedParam(Map<String, ? extends @Nullable Object> paramMap) throws DataAccessException {
287+
return streamByNamedParam(paramMap, null);
288+
}
224289

225290
/**
226291
* Generic object finder method, used by all other {@code findObject} methods.
@@ -342,4 +407,14 @@ public List<T> executeByNamedParam(Map<String, ? extends @Nullable Object> param
342407
*/
343408
protected abstract RowMapper<T> newRowMapper(@Nullable Object @Nullable [] parameters, @Nullable Map<?, ?> context);
344409

410+
private <R> R queryByNamedParam(Map<String, ?> paramMap, @Nullable Map<?, ?> context, BiFunction<PreparedStatementCreator, RowMapper<T>, R> queryFunction) {
411+
validateNamedParameters(paramMap);
412+
ParsedSql parsedSql = getParsedSql();
413+
MapSqlParameterSource paramSource = new MapSqlParameterSource(paramMap);
414+
String sqlToUse = NamedParameterUtils.substituteNamedParameters(parsedSql, paramSource);
415+
@Nullable Object[] params = NamedParameterUtils.buildValueArray(parsedSql, paramSource, getDeclaredParameters());
416+
RowMapper<T> rowMapper = newRowMapper(params, context);
417+
return queryFunction.apply(newPreparedStatementCreator(sqlToUse, params), rowMapper);
418+
}
419+
345420
}

spring-jdbc/src/test/java/org/springframework/jdbc/object/SqlQueryTests.java

+68
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import java.util.HashMap;
2727
import java.util.List;
2828
import java.util.Map;
29+
import java.util.stream.Stream;
2930

3031
import javax.sql.DataSource;
3132

@@ -52,6 +53,7 @@
5253
* @author Trevor Cook
5354
* @author Thomas Risberg
5455
* @author Juergen Hoeller
56+
* @author Yanming Zhou
5557
*/
5658
class SqlQueryTests {
5759

@@ -125,6 +127,72 @@ protected Integer mapRow(ResultSet rs, int rownum, Object @Nullable [] params, @
125127
verify(preparedStatement).close();
126128
}
127129

130+
@Test
131+
void testStreamWithoutParams() throws SQLException {
132+
given(resultSet.next()).willReturn(true, false);
133+
given(resultSet.getInt(1)).willReturn(1);
134+
135+
SqlQuery<Integer> query = new MappingSqlQueryWithParameters<>() {
136+
@Override
137+
protected Integer mapRow(ResultSet rs, int rownum, Object @Nullable [] params, @Nullable Map<? ,?> context)
138+
throws SQLException {
139+
assertThat(params).as("params were null").isNull();
140+
assertThat(context).as("context was null").isNull();
141+
return rs.getInt(1);
142+
}
143+
};
144+
query.setDataSource(dataSource);
145+
query.setSql(SELECT_ID);
146+
query.compile();
147+
try (Stream<Integer> stream = query.stream()) {
148+
List<Integer> list = stream.toList();
149+
assertThat(list).containsExactly(1);
150+
}
151+
verify(connection).prepareStatement(SELECT_ID);
152+
verify(resultSet).close();
153+
verify(preparedStatement).close();
154+
}
155+
156+
@Test
157+
void testStreamByNamedParam() throws SQLException {
158+
given(resultSet.next()).willReturn(true, false);
159+
given(resultSet.getInt("id")).willReturn(1);
160+
given(resultSet.getString("forename")).willReturn("rod");
161+
given(connection.prepareStatement(SELECT_ID_FORENAME_NAMED_PARAMETERS_PARSED,
162+
ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY)
163+
).willReturn(preparedStatement);
164+
165+
SqlQuery<Customer> query = new MappingSqlQueryWithParameters<>() {
166+
@Override
167+
protected Customer mapRow(ResultSet rs, int rownum, Object @Nullable [] params, @Nullable Map<? ,?> context)
168+
throws SQLException {
169+
assertThat(params).as("params were not null").isNotNull();
170+
assertThat(context).as("context was null").isNull();
171+
Customer cust = new Customer();
172+
cust.setId(rs.getInt(COLUMN_NAMES[0]));
173+
cust.setForename(rs.getString(COLUMN_NAMES[1]));
174+
return cust;
175+
}
176+
};
177+
query.declareParameter(new SqlParameter("id", Types.NUMERIC));
178+
query.declareParameter(new SqlParameter("country", Types.VARCHAR));
179+
query.setDataSource(dataSource);
180+
query.setSql(SELECT_ID_FORENAME_NAMED_PARAMETERS);
181+
query.compile();
182+
try (Stream<Customer> stream = query.streamByNamedParam(Map.of("id", 1, "country", "UK"))) {
183+
List<Customer> list = stream.toList();
184+
assertThat(list).hasSize(1);
185+
Customer customer = list.get(0);
186+
assertThat(customer.getId()).isEqualTo(1);
187+
assertThat(customer.getForename()).isEqualTo("rod");
188+
}
189+
verify(connection).prepareStatement(SELECT_ID_FORENAME_NAMED_PARAMETERS_PARSED);
190+
verify(preparedStatement).setObject(1, 1, Types.NUMERIC);
191+
verify(preparedStatement).setString(2, "UK");
192+
verify(resultSet).close();
193+
verify(preparedStatement).close();
194+
}
195+
128196
@Test
129197
void testQueryWithoutEnoughParams() {
130198
MappingSqlQuery<Integer> query = new MappingSqlQuery<>() {

0 commit comments

Comments
 (0)