From c416d3d0eae78c3b417226b1f5ba42c73ce7838b Mon Sep 17 00:00:00 2001 From: Dennis Effing Date: Thu, 7 Jan 2021 09:22:29 +0100 Subject: [PATCH] Add support for streamed query results. Use queryForStream for streamed query results. Since ResultSetExtractor cannot be reasonably be used together with streams it falls back to the existing collection behaviour. Closes #578 Original pull request #903 --- .../repository/query/AbstractJdbcQuery.java | 12 ++++++- .../QueryAnnotationHsqlIntegrationTests.java | 20 +++++++++++ .../query/StringBasedJdbcQueryUnitTests.java | 34 ++++++++++++++++++- 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/AbstractJdbcQuery.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/AbstractJdbcQuery.java index a4fea0d2..2d7df924 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/AbstractJdbcQuery.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/AbstractJdbcQuery.java @@ -18,6 +18,7 @@ package org.springframework.data.jdbc.repository.query; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; +import java.util.stream.Stream; import org.springframework.core.convert.converter.Converter; import org.springframework.dao.EmptyResultDataAccessException; @@ -40,6 +41,7 @@ import org.springframework.util.Assert; * @author Oliver Gierke * @author Maciej Walkowiak * @author Mark Paluch + * @author Dennis Effing * @since 2.0 */ public abstract class AbstractJdbcQuery implements RepositoryQuery { @@ -88,10 +90,14 @@ public abstract class AbstractJdbcQuery implements RepositoryQuery { return createModifyingQueryExecutor(); } - if (queryMethod.isCollectionQuery() || queryMethod.isStreamQuery()) { + if (queryMethod.isCollectionQuery()) { return extractor != null ? getQueryExecution(extractor) : collectionQuery(rowMapper); } + if (queryMethod.isStreamQuery()) { + return extractor != null ? getQueryExecution(extractor) : streamQuery(rowMapper); + } + return extractor != null ? getQueryExecution(extractor) : singleObjectQuery(rowMapper); } @@ -140,6 +146,10 @@ public abstract class AbstractJdbcQuery implements RepositoryQuery { : returnedType.getReturnedType(); } + private JdbcQueryExecution> streamQuery(RowMapper rowMapper) { + return (query, parameters) -> operations.queryForStream(query, parameters, rowMapper); + } + private JdbcQueryExecution getQueryExecution(ResultSetExtractor resultSetExtractor) { return (query, parameters) -> operations.query(query, parameters, resultSetExtractor); } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/QueryAnnotationHsqlIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/QueryAnnotationHsqlIntegrationTests.java index b4acc943..28aac128 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/QueryAnnotationHsqlIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/QueryAnnotationHsqlIntegrationTests.java @@ -51,6 +51,7 @@ import org.springframework.transaction.annotation.Transactional; * @author Jens Schauder * @author Kazuki Shimizu * @author Mark Paluch + * @author Dennis Effing */ @Transactional @ActiveProfiles("hsql") @@ -173,6 +174,21 @@ public class QueryAnnotationHsqlIntegrationTests { .containsExactlyInAnyOrder("a", "b"); } + @Test // DATAJDBC-356 + public void executeCustomQueryWithNamedParameterAndReturnTypeIsStream() { + + repository.save(dummyEntity("a")); + repository.save(dummyEntity("b")); + repository.save(dummyEntity("c")); + + Stream entities = repository.findByNamedRangeWithNamedParameterAndReturnTypeIsStream("a", "c"); + + assertThat(entities) // + .extracting(e -> e.name) // + .containsExactlyInAnyOrder("b"); + + } + @Test // DATAJDBC-175 public void executeCustomQueryWithReturnTypeIsNumber() { @@ -292,6 +308,10 @@ public class QueryAnnotationHsqlIntegrationTests { @Query("SELECT * FROM DUMMY_ENTITY") Stream findAllWithReturnTypeIsStream(); + @Query("SELECT * FROM DUMMY_ENTITY WHERE name < :upper and name > :lower") + Stream findByNamedRangeWithNamedParameterAndReturnTypeIsStream(@Param("lower") String lower, + @Param("upper") String upper); + // DATAJDBC-175 @Query("SELECT count(*) FROM DUMMY_ENTITY WHERE name like concat('%', :name, '%')") int countByNameContaining(@Param("name") String name); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java index 0d72eca8..d5704c13 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java @@ -22,11 +22,12 @@ import java.lang.reflect.Method; import java.sql.ResultSet; import java.util.List; import java.util.Properties; +import java.util.stream.Stream; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; - +import org.mockito.ArgumentCaptor; import org.springframework.dao.DataAccessException; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -39,9 +40,11 @@ import org.springframework.data.relational.core.mapping.RelationalMappingContext import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries; +import org.springframework.data.repository.query.DefaultParameters; import org.springframework.jdbc.core.ResultSetExtractor; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; import org.springframework.util.ReflectionUtils; /** @@ -52,6 +55,7 @@ import org.springframework.util.ReflectionUtils; * @author Maciej Walkowiak * @author Evgeni Dimitrov * @author Mark Paluch + * @author Dennis Effing */ public class StringBasedJdbcQueryUnitTests { @@ -127,6 +131,28 @@ public class StringBasedJdbcQueryUnitTests { "RowMapper is not expected to be custom"); } + @Test // DATAJDBC-356 + public void streamQueryCallsQueryForStreamOnOperations() { + JdbcQueryMethod queryMethod = createMethod("findAllWithStreamReturnType"); + StringBasedJdbcQuery query = createQuery(queryMethod); + + query.execute(new Object[] {}); + + verify(operations).queryForStream(eq("some sql statement"), any(SqlParameterSource.class), any(RowMapper.class)); + } + + @Test // DATAJDBC-356 + void streamQueryFallsBackToCollectionQueryWhenCustomResultSetExtractorIsSpecified() { + JdbcQueryMethod queryMethod = createMethod("findAllWithStreamReturnTypeAndResultSetExtractor"); + StringBasedJdbcQuery query = createQuery(queryMethod); + + query.execute(new Object[] {}); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ResultSetExtractor.class); + verify(operations).query(eq("some sql statement"), any(SqlParameterSource.class), captor.capture()); + assertThat(captor.getValue()).isInstanceOf(CustomResultSetExtractor.class); + } + @Test // GH-774 public void sliceQueryNotSupported() { @@ -173,6 +199,12 @@ public class StringBasedJdbcQueryUnitTests { resultSetExtractorClass = CustomResultSetExtractor.class) List findAllWithCustomRowMapperAndResultSetExtractor(); + @Query(value = "some sql statement") + Stream findAllWithStreamReturnType(); + + @Query(value = "some sql statement", resultSetExtractorClass = CustomResultSetExtractor.class) + Stream findAllWithStreamReturnTypeAndResultSetExtractor(); + List noAnnotation(); @Query(value = "some sql statement")