From c7b1ac11eca2a4f56b9c9137a018d772075e9208 Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Mon, 23 Dec 2024 12:47:07 +0100 Subject: [PATCH 1/7] 1803-projection - Prepare branch --- pom.xml | 6 +++--- spring-data-jdbc-distribution/pom.xml | 2 +- spring-data-jdbc/pom.xml | 4 ++-- spring-data-r2dbc/pom.xml | 4 ++-- spring-data-relational/pom.xml | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/pom.xml b/pom.xml index ebd3103251..a1f6e1cb38 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-relational-parent - 4.0.0-SNAPSHOT + 4.0.0-1803-projection-SNAPSHOT pom Spring Data Relational Parent @@ -46,8 +46,8 @@ 1.0.0.RELEASE 1.1.4 1.0.2.RELEASE - 1.4.1 - 1.3.0 + 1.4.0 + 1.2.0 1.3.0 diff --git a/spring-data-jdbc-distribution/pom.xml b/spring-data-jdbc-distribution/pom.xml index b3c39e64c3..e88d0a11c5 100644 --- a/spring-data-jdbc-distribution/pom.xml +++ b/spring-data-jdbc-distribution/pom.xml @@ -14,7 +14,7 @@ org.springframework.data spring-data-relational-parent - 4.0.0-SNAPSHOT + 4.0.0-1803-projection-SNAPSHOT ../pom.xml diff --git a/spring-data-jdbc/pom.xml b/spring-data-jdbc/pom.xml index e61fd64020..39ff7163a5 100644 --- a/spring-data-jdbc/pom.xml +++ b/spring-data-jdbc/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-jdbc - 4.0.0-SNAPSHOT + 4.0.0-1803-projection-SNAPSHOT Spring Data JDBC Spring Data module for JDBC repositories. @@ -15,7 +15,7 @@ org.springframework.data spring-data-relational-parent - 4.0.0-SNAPSHOT + 4.0.0-1803-projection-SNAPSHOT diff --git a/spring-data-r2dbc/pom.xml b/spring-data-r2dbc/pom.xml index 3ee76fd3c1..d73dc4c5b5 100644 --- a/spring-data-r2dbc/pom.xml +++ b/spring-data-r2dbc/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-r2dbc - 4.0.0-SNAPSHOT + 4.0.0-1803-projection-SNAPSHOT Spring Data R2DBC Spring Data module for R2DBC @@ -15,7 +15,7 @@ org.springframework.data spring-data-relational-parent - 4.0.0-SNAPSHOT + 4.0.0-1803-projection-SNAPSHOT diff --git a/spring-data-relational/pom.xml b/spring-data-relational/pom.xml index 8fd6d7a6f0..d14de98951 100644 --- a/spring-data-relational/pom.xml +++ b/spring-data-relational/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-relational - 4.0.0-SNAPSHOT + 4.0.0-1803-projection-SNAPSHOT Spring Data Relational Spring Data Relational support @@ -14,7 +14,7 @@ org.springframework.data spring-data-relational-parent - 4.0.0-SNAPSHOT + 4.0.0-1803-projection-SNAPSHOT From e09b7780dd9f607c65d19d7e1f9ffef487903ab9 Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Mon, 23 Dec 2024 14:09:53 +0100 Subject: [PATCH 2/7] JdbcAggregateTemplate honors columns specified in query. If no columns are given, all columns are selected by default. If columns are specified, only these are selected. Joins normally triggered by columns from 1:1 relationships are not implemented, and the corresponding columns don't get loaded and can't be specified in a query. Limiting columns is not supported for single query loading. Closes #1803 --- .../data/jdbc/core/convert/SqlGenerator.java | 44 +++++++++++++------ ...JdbcAggregateTemplateIntegrationTests.java | 26 ++++++++++- .../core/convert/SqlGeneratorUnitTests.java | 16 ++++++- 3 files changed, 71 insertions(+), 15 deletions(-) diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java index 7ac637e8c3..f771cdf3c1 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java @@ -507,33 +507,50 @@ private String createFindAllSql() { } private SelectBuilder.SelectWhere selectBuilder() { - return selectBuilder(Collections.emptyList()); + return selectBuilder(Collections.emptyList(), null); + } + + + private SelectBuilder.SelectWhere selectBuilder(Query query) { + return selectBuilder(Collections.emptyList(), query); } private SelectBuilder.SelectWhere selectBuilder(Collection keyColumns) { + return selectBuilder(keyColumns, null); + } + + private SelectBuilder.SelectWhere selectBuilder(Collection keyColumns, @Nullable Query query) { Table table = getTable(); Set columnExpressions = new LinkedHashSet<>(); List joinTables = new ArrayList<>(); - for (PersistentPropertyPath path : mappingContext - .findPersistentPropertyPaths(entity.getType(), p -> true)) { - AggregatePath extPath = mappingContext.getAggregatePath(path); - - // add a join if necessary - Join join = getJoin(extPath); - if (join != null) { - joinTables.add(join); + if (query != null && !query.getColumns().isEmpty()) { + for (SqlIdentifier columnName : query.getColumns()) { + columnExpressions.add(Column.create(columnName, table)); } + } else { + for (PersistentPropertyPath path : mappingContext + .findPersistentPropertyPaths(entity.getType(), p -> true)) { + + AggregatePath extPath = mappingContext.getAggregatePath(path); + + // add a join if necessary + Join join = getJoin(extPath); + if (join != null) { + joinTables.add(join); + } - Column column = getColumn(extPath); - if (column != null) { - columnExpressions.add(column); + Column column = getColumn(extPath); + if (column != null) { + columnExpressions.add(column); + } } } + for (SqlIdentifier keyColumn : keyColumns) { columnExpressions.add(table.column(keyColumn).as(keyColumn)); } @@ -876,7 +893,7 @@ public String selectByQuery(Query query, MapSqlParameterSource parameterSource) Assert.notNull(parameterSource, "parameterSource must not be null"); - SelectBuilder.SelectWhere selectBuilder = selectBuilder(); + SelectBuilder.SelectWhere selectBuilder = selectBuilder(query); Select select = applyQueryOnSelect(query, parameterSource, selectBuilder) // .build(); @@ -884,6 +901,7 @@ public String selectByQuery(Query query, MapSqlParameterSource parameterSource) return render(select); } + /** * Constructs a single sql query that performs select based on the provided query and pagination information. * Additional the bindings for the where clause are stored after execution into the parameterSource diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AbstractJdbcAggregateTemplateIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AbstractJdbcAggregateTemplateIntegrationTests.java index 00ce1278c7..cfa0416645 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AbstractJdbcAggregateTemplateIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AbstractJdbcAggregateTemplateIntegrationTests.java @@ -29,6 +29,7 @@ import java.util.stream.IntStream; import java.util.stream.Stream; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; @@ -235,7 +236,25 @@ void findAllByQuery() { Query query = Query.query(criteria); Iterable reloadedById = template.findAll(query, SimpleListParent.class); - assertThat(reloadedById).extracting(e -> e.id, e -> e.content.size()).containsExactly(tuple(two.id, 2)); + assertThat(reloadedById) // + .extracting(e -> e.id, e-> e.name, e -> e.content.size()) // + .containsExactly(tuple(two.id, two.name, 2)); + } + + @Test // GH-1803 + void findAllByQueryWithColumns() { + + template.save(SimpleListParent.of("one", "one_1")); + SimpleListParent two = template.save(SimpleListParent.of("two", "two_1", "two_2")); + template.save(SimpleListParent.of("three", "three_1", "three_2", "three_3")); + + CriteriaDefinition criteria = CriteriaDefinition.from(Criteria.where("id").is(two.id)); + Query query = Query.query(criteria).columns("id"); + Iterable reloadedById = template.findAll(query, SimpleListParent.class); + + assertThat(reloadedById) // + .extracting(e -> e.id, e-> e.name, e -> e.content.size()) // + .containsExactly(tuple(two.id, null, 2)); } @Test // GH-1601 @@ -2283,5 +2302,10 @@ static class JdbcAggregateTemplateIntegrationTests extends AbstractJdbcAggregate static class JdbcAggregateTemplateSingleQueryLoadingIntegrationTests extends AbstractJdbcAggregateTemplateIntegrationTests { + @Disabled + @Override + void findAllByQueryWithColumns() { + super.findAllByQueryWithColumns(); + } } } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java index cc264cbe62..a9eb85c600 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java @@ -364,6 +364,19 @@ void selectByQuery() { ); } + @Test // GH-1803 + void selectByQueryWithColumnLimit() { + + Query query = Query.empty().columns("alpha", "beta", "gamma"); + + String sql = sqlGenerator.selectByQuery(query, new MapSqlParameterSource()); + + assertThat(sql).contains( // + "SELECT dummy_entity.alpha, dummy_entity.beta, dummy_entity.gamma", // + "FROM dummy_entity" // + ); + } + @Test // GH-1919 void selectBySortedQuery() { @@ -381,7 +394,8 @@ void selectBySortedQuery() { "ORDER BY dummy_entity.id1 ASC" // ); assertThat(sql).containsOnlyOnce("LEFT OUTER JOIN referenced_entity ref ON ref.dummy_entity = dummy_entity.id1"); - assertThat(sql).containsOnlyOnce("LEFT OUTER JOIN second_level_referenced_entity ref_further ON ref_further.referenced_entity = ref.x_l1id"); + assertThat(sql).containsOnlyOnce( + "LEFT OUTER JOIN second_level_referenced_entity ref_further ON ref_further.referenced_entity = ref.x_l1id"); } @Test // DATAJDBC-131, DATAJDBC-111 From 217d0630bc0bfd8b3829968dfc486ca7ef79c175 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 16 Jan 2025 14:15:24 +0100 Subject: [PATCH 3/7] Polishing. Extract method. Extend tests. Introduce empty Query object to avoid nullability. --- .../data/jdbc/core/convert/SqlGenerator.java | 52 ++++++++++++------- .../core/convert/SqlGeneratorUnitTests.java | 7 +-- .../data/relational/core/query/Query.java | 3 +- 3 files changed, 38 insertions(+), 24 deletions(-) diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java index f771cdf3c1..d29527b93f 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java @@ -39,6 +39,7 @@ import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; /** * Generates SQL statements to be used by {@link SimpleJdbcRepository} @@ -507,29 +508,40 @@ private String createFindAllSql() { } private SelectBuilder.SelectWhere selectBuilder() { - return selectBuilder(Collections.emptyList(), null); + return selectBuilder(Collections.emptyList(), Query.empty()); } - private SelectBuilder.SelectWhere selectBuilder(Query query) { return selectBuilder(Collections.emptyList(), query); } private SelectBuilder.SelectWhere selectBuilder(Collection keyColumns) { - return selectBuilder(keyColumns, null); + return selectBuilder(keyColumns, Query.empty()); } - private SelectBuilder.SelectWhere selectBuilder(Collection keyColumns, @Nullable Query query) { + private SelectBuilder.SelectWhere selectBuilder(Collection keyColumns, Query query) { Table table = getTable(); - Set columnExpressions = new LinkedHashSet<>(); + Projection projection = getProjection(keyColumns, query, table); + SelectBuilder.SelectAndFrom selectBuilder = StatementBuilder.select(projection.columns()); + SelectBuilder.SelectJoin baseSelect = selectBuilder.from(table); + + for (Join join : projection.joins()) { + baseSelect = baseSelect.leftOuterJoin(join.joinTable).on(join.joinColumn).equals(join.parentId); + } + + return (SelectBuilder.SelectWhere) baseSelect; + } - List joinTables = new ArrayList<>(); + private Projection getProjection(Collection keyColumns, Query query, Table table) { - if (query != null && !query.getColumns().isEmpty()) { + Set columns = new LinkedHashSet<>(); + Set joins = new LinkedHashSet<>(); + + if (!CollectionUtils.isEmpty(query.getColumns())) { for (SqlIdentifier columnName : query.getColumns()) { - columnExpressions.add(Column.create(columnName, table)); + columns.add(Column.create(columnName, table)); } } else { for (PersistentPropertyPath path : mappingContext @@ -540,29 +552,30 @@ private SelectBuilder.SelectWhere selectBuilder(Collection keyCol // add a join if necessary Join join = getJoin(extPath); if (join != null) { - joinTables.add(join); + joins.add(join); } Column column = getColumn(extPath); if (column != null) { - columnExpressions.add(column); + columns.add(column); } } } - for (SqlIdentifier keyColumn : keyColumns) { - columnExpressions.add(table.column(keyColumn).as(keyColumn)); + columns.add(table.column(keyColumn).as(keyColumn)); } - SelectBuilder.SelectAndFrom selectBuilder = StatementBuilder.select(columnExpressions); - SelectBuilder.SelectJoin baseSelect = selectBuilder.from(table); - - for (Join join : joinTables) { - baseSelect = baseSelect.leftOuterJoin(join.joinTable).on(join.joinColumn).equals(join.parentId); - } + return new Projection(columns, joins); + } - return (SelectBuilder.SelectWhere) baseSelect; + /** + * Projection including its source joins. + * + * @param columns + * @param joins + */ + record Projection(Set columns, Set joins) { } private SelectBuilder.SelectOrdered selectBuilder(Collection keyColumns, Sort sort, @@ -901,7 +914,6 @@ public String selectByQuery(Query query, MapSqlParameterSource parameterSource) return render(select); } - /** * Constructs a single sql query that performs select based on the provided query and pagination information. * Additional the bindings for the where clause are stored after execution into the parameterSource diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java index a9eb85c600..773269351e 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java @@ -351,12 +351,13 @@ void findAllPagedAndSorted() { @Test // GH-1919 void selectByQuery() { - Query query = Query.query(Criteria.where("id").is(23L)); + Query query = Query.query(Criteria.where("id").is(23L)).columns(new String[0]); String sql = sqlGenerator.selectByQuery(query, new MapSqlParameterSource()); assertThat(sql).contains( // "SELECT", // + "dummy_entity.id1 AS id1, dummy_entity.x_name AS x_name", // "FROM dummy_entity", // "LEFT OUTER JOIN referenced_entity ref ON ref.dummy_entity = dummy_entity.id1", // "LEFT OUTER JOIN second_level_referenced_entity ref_further ON ref_further.referenced_entity = ref.x_l1id", // @@ -367,12 +368,12 @@ void selectByQuery() { @Test // GH-1803 void selectByQueryWithColumnLimit() { - Query query = Query.empty().columns("alpha", "beta", "gamma"); + Query query = Query.empty().columns("id", "alpha", "beta", "gamma"); String sql = sqlGenerator.selectByQuery(query, new MapSqlParameterSource()); assertThat(sql).contains( // - "SELECT dummy_entity.alpha, dummy_entity.beta, dummy_entity.gamma", // + "SELECT dummy_entity.id1, dummy_entity.alpha, dummy_entity.beta, dummy_entity.gamma", // "FROM dummy_entity" // ); } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Query.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Query.java index 3a8e9d72c6..6d1ed69d4f 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Query.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/query/Query.java @@ -41,6 +41,7 @@ */ public class Query { + private static final Query EMPTY = new Query(null); private static final int NO_LIMIT = -1; private final @Nullable CriteriaDefinition criteria; @@ -84,7 +85,7 @@ private Query(@Nullable CriteriaDefinition criteria, List columns * @return */ public static Query empty() { - return new Query(null); + return EMPTY; } /** From 56b381816c4ab3c1bb23e720cc44c42459611c43 Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Mon, 20 Jan 2025 09:55:57 +0100 Subject: [PATCH 4/7] Consider query columns as property names. Query columns now get checked against property names. If such a property name is found, the property is used. Otherwise the column is considered a literal column and used as is in the SQL generation. Original pull request #1967 See #1803 --- .../data/jdbc/core/convert/SqlGenerator.java | 39 +++++++++++++------ .../core/convert/SqlGeneratorUnitTests.java | 2 +- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java index d29527b93f..6adf386d83 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java @@ -541,24 +541,25 @@ private Projection getProjection(Collection keyColumns, Query que if (!CollectionUtils.isEmpty(query.getColumns())) { for (SqlIdentifier columnName : query.getColumns()) { - columns.add(Column.create(columnName, table)); + + String columnNameString = columnName.getReference(); + RelationalPersistentProperty property = entity.getPersistentProperty(columnNameString); + if (property != null) { + + AggregatePath aggregatePath = mappingContext.getAggregatePath( + mappingContext.getPersistentPropertyPath(columnNameString, entity.getTypeInformation())); + gatherColumn(aggregatePath, joins, columns); + } else { + columns.add(Column.create(columnName, table)); + } } } else { for (PersistentPropertyPath path : mappingContext .findPersistentPropertyPaths(entity.getType(), p -> true)) { - AggregatePath extPath = mappingContext.getAggregatePath(path); + AggregatePath aggregatePath = mappingContext.getAggregatePath(path); - // add a join if necessary - Join join = getJoin(extPath); - if (join != null) { - joins.add(join); - } - - Column column = getColumn(extPath); - if (column != null) { - columns.add(column); - } + gatherColumn(aggregatePath, joins, columns); } } @@ -569,6 +570,20 @@ private Projection getProjection(Collection keyColumns, Query que return new Projection(columns, joins); } + private void gatherColumn(AggregatePath aggregatePath, Set joins, Set columns) { + + // add a join if necessary + Join join = getJoin(aggregatePath); + if (join != null) { + joins.add(join); + } + + Column column = getColumn(aggregatePath); + if (column != null) { + columns.add(column); + } + } + /** * Projection including its source joins. * diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java index 773269351e..7db8c7cf5c 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java @@ -373,7 +373,7 @@ void selectByQueryWithColumnLimit() { String sql = sqlGenerator.selectByQuery(query, new MapSqlParameterSource()); assertThat(sql).contains( // - "SELECT dummy_entity.id1, dummy_entity.alpha, dummy_entity.beta, dummy_entity.gamma", // + "SELECT dummy_entity.id1 AS id1, dummy_entity.alpha, dummy_entity.beta, dummy_entity.gamma", // "FROM dummy_entity" // ); } From c6c9b398d48301cf60e0e5069cec7fad00bb355b Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 11 Jun 2025 15:12:41 +0200 Subject: [PATCH 5/7] Clean up unused profile declarations. --- pom.xml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/pom.xml b/pom.xml index a1f6e1cb38..551841eaf3 100644 --- a/pom.xml +++ b/pom.xml @@ -52,7 +52,6 @@ 1.3.0 - 1.37 0.4.0.BUILD-SNAPSHOT @@ -165,22 +164,14 @@ jmh - - com.github.mp911de.microbenchmark-runner - microbenchmark-runner-junit5 - ${mbr.version} - test - org.openjdk.jmh jmh-core - ${jmh.version} test org.openjdk.jmh jmh-generator-annprocess - ${jmh.version} test From 80c113a24b484caef6b74d8cd6b6d38b65ab418f Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 11 Jun 2025 15:20:20 +0200 Subject: [PATCH 6/7] Polishing. Resolve mapped property paths to aggregate paths. Add tests. --- .../data/jdbc/core/convert/SqlGenerator.java | 37 +++++++++---------- .../core/convert/SqlGeneratorUnitTests.java | 27 ++++++++++++++ 2 files changed, 44 insertions(+), 20 deletions(-) diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java index 6adf386d83..b6db3dc299 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java @@ -23,6 +23,7 @@ import org.springframework.data.domain.Sort; import org.springframework.data.jdbc.repository.support.SimpleJdbcRepository; import org.springframework.data.mapping.PersistentPropertyPath; +import org.springframework.data.mapping.context.InvalidPersistentPropertyPath; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.relational.core.dialect.Dialect; import org.springframework.data.relational.core.dialect.RenderContextFactory; @@ -36,10 +37,10 @@ import org.springframework.data.relational.core.sql.render.RenderContext; import org.springframework.data.relational.core.sql.render.SqlRenderer; import org.springframework.data.util.Lazy; +import org.springframework.data.util.Predicates; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; /** * Generates SQL statements to be used by {@link SimpleJdbcRepository} @@ -539,27 +540,26 @@ private Projection getProjection(Collection keyColumns, Query que Set columns = new LinkedHashSet<>(); Set joins = new LinkedHashSet<>(); - if (!CollectionUtils.isEmpty(query.getColumns())) { - for (SqlIdentifier columnName : query.getColumns()) { + for (SqlIdentifier columnName : query.getColumns()) { - String columnNameString = columnName.getReference(); - RelationalPersistentProperty property = entity.getPersistentProperty(columnNameString); - if (property != null) { + try { + AggregatePath aggregatePath = mappingContext.getAggregatePath( + mappingContext.getPersistentPropertyPath(columnName.getReference(), entity.getTypeInformation())); - AggregatePath aggregatePath = mappingContext.getAggregatePath( - mappingContext.getPersistentPropertyPath(columnNameString, entity.getTypeInformation())); - gatherColumn(aggregatePath, joins, columns); - } else { - columns.add(Column.create(columnName, table)); - } + includeColumnAndJoin(aggregatePath, joins, columns); + } catch (InvalidPersistentPropertyPath e) { + columns.add(Column.create(columnName, table)); } - } else { + } + + if (columns.isEmpty()) { + for (PersistentPropertyPath path : mappingContext - .findPersistentPropertyPaths(entity.getType(), p -> true)) { + .findPersistentPropertyPaths(entity.getType(), Predicates.isTrue())) { AggregatePath aggregatePath = mappingContext.getAggregatePath(path); - gatherColumn(aggregatePath, joins, columns); + includeColumnAndJoin(aggregatePath, joins, columns); } } @@ -570,7 +570,7 @@ private Projection getProjection(Collection keyColumns, Query que return new Projection(columns, joins); } - private void gatherColumn(AggregatePath aggregatePath, Set joins, Set columns) { + private void includeColumnAndJoin(AggregatePath aggregatePath, Set joins, Set columns) { // add a join if necessary Join join = getJoin(aggregatePath); @@ -643,10 +643,7 @@ Column getColumn(AggregatePath path) { // Simple entities without id include there backreference as a synthetic id in order to distinguish null entities // from entities with only null values. - if (path.isQualified() // - || path.isCollectionLike() // - || path.hasIdProperty() // - ) { + if (path.isQualified() || path.isCollectionLike() || path.hasIdProperty()) { return null; } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java index 7db8c7cf5c..01a051ddce 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java @@ -32,6 +32,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; + import org.springframework.data.annotation.Id; import org.springframework.data.annotation.ReadOnlyProperty; import org.springframework.data.annotation.Version; @@ -378,6 +379,32 @@ void selectByQueryWithColumnLimit() { ); } + @Test // GH-1803 + void selectingSetContentSelectsAllColumns() { + + Query query = Query.empty().columns("elements.content"); + + String sql = sqlGenerator.selectByQuery(query, new MapSqlParameterSource()); + + assertThat(sql).contains( // + "SELECT dummy_entity.id1 AS id1, dummy_entity.x_name AS x_name"// + ); + } + + @Test // GH-1803 + void selectByQueryWithMappedColumnPathsRendersCorrectSelection() { + + Query query = Query.empty().columns("ref.content"); + + String sql = sqlGenerator.selectByQuery(query, new MapSqlParameterSource()); + + assertThat(sql).contains( // + "SELECT", // + "ref.id1 AS id1, ref.content AS x_content", // + "FROM dummy_entity", // + "LEFT OUTER JOIN referenced_entity ref ON ref.dummy_entity = dummy_entity.id1"); + } + @Test // GH-1919 void selectBySortedQuery() { From aedafa23066a10e09db1a782268d2f257e051c9c Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Thu, 12 Jun 2025 13:43:31 +0200 Subject: [PATCH 7/7] Include intermediate joins. When fields from referenced entities got requested, ther intermediate necessary joins were not included. --- .../data/jdbc/core/convert/SqlGenerator.java | 21 ++++++++++++++----- .../core/convert/SqlGeneratorUnitTests.java | 2 +- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java index b6db3dc299..4cc3e9aa7b 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java @@ -572,11 +572,7 @@ private Projection getProjection(Collection keyColumns, Query que private void includeColumnAndJoin(AggregatePath aggregatePath, Set joins, Set columns) { - // add a join if necessary - Join join = getJoin(aggregatePath); - if (join != null) { - joins.add(join); - } + joins.addAll(getJoins(aggregatePath)); Column column = getColumn(aggregatePath); if (column != null) { @@ -653,9 +649,24 @@ Column getColumn(AggregatePath path) { return sqlContext.getColumn(path); } + List getJoins(AggregatePath path) { + + List joins = new ArrayList<>(); + while (!path.isRoot()) { + Join join = getJoin(path); + if (join != null) { + joins.add(join); + } + + path = path.getParentPath(); + } + return joins; + } + @Nullable Join getJoin(AggregatePath path) { + // TODO: This doesn't handle paths with length > 1 correctly if (!path.isEntity() || path.isEmbedded() || path.isMultiValued()) { return null; } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java index 01a051ddce..62e95245d7 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java @@ -400,7 +400,7 @@ void selectByQueryWithMappedColumnPathsRendersCorrectSelection() { assertThat(sql).contains( // "SELECT", // - "ref.id1 AS id1, ref.content AS x_content", // + "ref.x_content AS ref_x_content", // "FROM dummy_entity", // "LEFT OUTER JOIN referenced_entity ref ON ref.dummy_entity = dummy_entity.id1"); }