From 1d6efb706a561a1b1a1650c88f8740ea24b7ad7b Mon Sep 17 00:00:00 2001 From: Dennis Effing Date: Thu, 7 Jan 2021 09:22:29 +0100 Subject: [PATCH 1/2] Add support for streamed query results Use queryForStream for streamed query results. ResultSetExtractor is ignored because it cannot be used together with streams. Closes #356 --- .../repository/query/AbstractJdbcQuery.java | 12 ++++++++++- .../QueryAnnotationHsqlIntegrationTests.java | 20 +++++++++++++++++++ .../query/StringBasedJdbcQueryUnitTests.java | 18 ++++++++++++++++- 3 files changed, 48 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 a4fea0d222..8a5357cf0c 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 @@ 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 @@ * @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 @@ protected JdbcQueryExecution getQueryExecution(JdbcQueryMethod queryMethod, return createModifyingQueryExecutor(); } - if (queryMethod.isCollectionQuery() || queryMethod.isStreamQuery()) { + if (queryMethod.isCollectionQuery()) { return extractor != null ? getQueryExecution(extractor) : collectionQuery(rowMapper); } + if (queryMethod.isStreamQuery()) { + return streamQuery(rowMapper); + } + return extractor != null ? getQueryExecution(extractor) : singleObjectQuery(rowMapper); } @@ -140,6 +146,10 @@ protected Class resolveTypeToRead(ResultProcessor resultProcessor) { : 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 b4acc943e9..28aac12869 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 @@ * @author Jens Schauder * @author Kazuki Shimizu * @author Mark Paluch + * @author Dennis Effing */ @Transactional @ActiveProfiles("hsql") @@ -173,6 +174,21 @@ public void executeCustomQueryWithReturnTypeIsStream() { .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 @@ private interface DummyEntityRepository extends CrudRepository 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 0d72eca896..7db5b2f7e1 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,11 @@ 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.springframework.dao.DataAccessException; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -39,9 +39,11 @@ 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 +54,7 @@ * @author Maciej Walkowiak * @author Evgeni Dimitrov * @author Mark Paluch + * @author Dennis Effing */ public class StringBasedJdbcQueryUnitTests { @@ -127,6 +130,16 @@ public void customResultSetExtractorAndRowMapperGetCombined() { "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 // GH-774 public void sliceQueryNotSupported() { @@ -173,6 +186,9 @@ interface MyRepository extends Repository { resultSetExtractorClass = CustomResultSetExtractor.class) List findAllWithCustomRowMapperAndResultSetExtractor(); + @Query(value = "some sql statement") + Stream findAllWithStreamReturnType(); + List noAnnotation(); @Query(value = "some sql statement") From e23842b005ffd24059184a9751ae4d05fd0027ce Mon Sep 17 00:00:00 2001 From: Dennis Effing Date: Mon, 28 Jun 2021 12:46:36 +0200 Subject: [PATCH 2/2] Fall back to collection if stream query has custom result set extractor Specifying a custom result set extractor doesn't make sense when using streams because the result set extractor always considers the entire result. This loads the entire stream into memory anyways. --- .../jdbc/repository/query/AbstractJdbcQuery.java | 2 +- .../query/StringBasedJdbcQueryUnitTests.java | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) 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 8a5357cf0c..2d7df924ed 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 @@ -95,7 +95,7 @@ protected JdbcQueryExecution getQueryExecution(JdbcQueryMethod queryMethod, } if (queryMethod.isStreamQuery()) { - return streamQuery(rowMapper); + return extractor != null ? getQueryExecution(extractor) : streamQuery(rowMapper); } return extractor != null ? getQueryExecution(extractor) : singleObjectQuery(rowMapper); 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 7db5b2f7e1..d5704c13f9 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 @@ -27,6 +27,7 @@ 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; @@ -140,6 +141,18 @@ public void streamQueryCallsQueryForStreamOnOperations() { 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() { @@ -189,6 +202,9 @@ interface MyRepository extends Repository { @Query(value = "some sql statement") Stream findAllWithStreamReturnType(); + @Query(value = "some sql statement", resultSetExtractorClass = CustomResultSetExtractor.class) + Stream findAllWithStreamReturnTypeAndResultSetExtractor(); + List noAnnotation(); @Query(value = "some sql statement")