diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java index 8351db58b7..141b5c88bc 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java @@ -33,6 +33,7 @@ import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.core.query.Criteria; import org.springframework.data.relational.core.sql.Column; +import org.springframework.data.relational.core.sql.EmptySelectListException; import org.springframework.data.relational.core.sql.Expression; import org.springframework.data.relational.core.sql.Expressions; import org.springframework.data.relational.core.sql.Functions; @@ -177,13 +178,23 @@ protected ParametrizedQuery complete(@Nullable Criteria criteria, Sort sort) { completedBuildSelect = selectOrderBuilder.lock(this.lockMode.get().value()); } - Select select = completedBuildSelect.build(); + Select select = getSelect(completedBuildSelect); String sql = SqlRenderer.create(renderContextFactory.createRenderContext()).render(select); return new ParametrizedQuery(sql, parameterSource); } + private Select getSelect(SelectBuilder.BuildSelect completedBuildSelect) { + + try { + return completedBuildSelect.build(); + } catch (EmptySelectListException cause) { + throw new IllegalStateException( + returnedType.getReturnedType().getName() + " does not define any properties to select", cause); + } + } + SelectBuilder.SelectOrdered applyOrderBy(Sort sort, RelationalPersistentEntity entity, Table table, SelectBuilder.SelectOrdered selectOrdered) { diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java index 008f923208..75cb7e1e09 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java @@ -41,7 +41,6 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.PropertiesFactoryBean; import org.springframework.context.ApplicationListener; @@ -51,16 +50,7 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.data.annotation.Id; -import org.springframework.data.domain.Example; -import org.springframework.data.domain.ExampleMatcher; -import org.springframework.data.domain.Limit; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.ScrollPosition; -import org.springframework.data.domain.Slice; -import org.springframework.data.domain.Sort; -import org.springframework.data.domain.Window; +import org.springframework.data.domain.*; import org.springframework.data.jdbc.core.mapping.AggregateReference; import org.springframework.data.jdbc.repository.query.Modifying; import org.springframework.data.jdbc.repository.query.Query; @@ -572,6 +562,24 @@ public void partTreeQueryProjectionShouldReturnProjectedEntities() { assertThat(result.get(0).getName()).isEqualTo("Entity Name"); } + @Test // GH-1813 + public void partTreeQueryDynamicProjectionShouldReturnProjectedEntities() { + + repository.save(createDummyEntity()); + + List result = repository.findDynamicProjectedByName("Entity Name", DummyProjection.class); + + assertThat(result).hasSize(1); + assertThat(result.get(0).getName()).isEqualTo("Entity Name"); + } + + @Test // GH-1813 + public void partTreeQueryDynamicProjectionWithBrokenProjectionShouldError() { + + assertThatThrownBy(() -> repository.findDynamicProjectedByName("Entity Name", BrokenProjection.class)) + .hasMessageContaining("BrokenProjection does not define any properties to select"); + } + @Test // GH-971 public void pageQueryProjectionShouldReturnProjectedEntities() { @@ -1428,6 +1436,8 @@ interface DummyEntityRepository extends CrudRepository, Query List findProjectedByName(String name); + List findDynamicProjectedByName(String name, Class projection); + @Query(value = "SELECT * FROM DUMMY_ENTITY", rowMapperClass = CustomRowMapper.class) List findAllWithCustomMapper(); @@ -1941,6 +1951,10 @@ interface DummyProjection { String getName(); } + interface BrokenProjection { + String name(); + } + static final class DtoProjection { private final String name; diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/EmptySelectListException.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/EmptySelectListException.java new file mode 100644 index 0000000000..2b5abf9a16 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/EmptySelectListException.java @@ -0,0 +1,29 @@ +/* + * Copyright 2024 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.relational.core.sql; + +/** + * Exception denoting the absence of a select list from a query. + * + * @author Jens Schauder + * @since 3.4 + */ +public class EmptySelectListException extends IllegalStateException { + public EmptySelectListException() { + super("SELECT does not declare a select list"); + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SelectValidator.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SelectValidator.java index cdaef37344..89c09c85ee 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SelectValidator.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/SelectValidator.java @@ -54,7 +54,7 @@ private void doValidate(Select select) { select.visit(this); if (selectFieldCount == 0) { - throw new IllegalStateException("SELECT does not declare a select list"); + throw new EmptySelectListException(); } for (TableLike table : requiredBySelect) {