diff --git a/pom.xml b/pom.xml index ebd3103251..551841eaf3 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,13 +46,12 @@ 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 - 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 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-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..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 @@ -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,6 +37,7 @@ 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; @@ -507,45 +509,84 @@ private String createFindAllSql() { } private SelectBuilder.SelectWhere selectBuilder() { - return selectBuilder(Collections.emptyList()); + 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, Query.empty()); + } + + 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); - List joinTables = new ArrayList<>(); - for (PersistentPropertyPath path : mappingContext - .findPersistentPropertyPaths(entity.getType(), p -> true)) { + for (Join join : projection.joins()) { + baseSelect = baseSelect.leftOuterJoin(join.joinTable).on(join.joinColumn).equals(join.parentId); + } - AggregatePath extPath = mappingContext.getAggregatePath(path); + return (SelectBuilder.SelectWhere) baseSelect; + } - // add a join if necessary - Join join = getJoin(extPath); - if (join != null) { - joinTables.add(join); + private Projection getProjection(Collection keyColumns, Query query, Table table) { + + Set columns = new LinkedHashSet<>(); + Set joins = new LinkedHashSet<>(); + + for (SqlIdentifier columnName : query.getColumns()) { + + try { + AggregatePath aggregatePath = mappingContext.getAggregatePath( + mappingContext.getPersistentPropertyPath(columnName.getReference(), entity.getTypeInformation())); + + includeColumnAndJoin(aggregatePath, joins, columns); + } catch (InvalidPersistentPropertyPath e) { + columns.add(Column.create(columnName, table)); } + } + + if (columns.isEmpty()) { - Column column = getColumn(extPath); - if (column != null) { - columnExpressions.add(column); + for (PersistentPropertyPath path : mappingContext + .findPersistentPropertyPaths(entity.getType(), Predicates.isTrue())) { + + AggregatePath aggregatePath = mappingContext.getAggregatePath(path); + + includeColumnAndJoin(aggregatePath, joins, columns); } } 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); + return new Projection(columns, joins); + } - for (Join join : joinTables) { - baseSelect = baseSelect.leftOuterJoin(join.joinTable).on(join.joinColumn).equals(join.parentId); + private void includeColumnAndJoin(AggregatePath aggregatePath, Set joins, Set columns) { + + joins.addAll(getJoins(aggregatePath)); + + Column column = getColumn(aggregatePath); + if (column != null) { + columns.add(column); } + } - 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, @@ -598,10 +639,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; } @@ -611,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; } @@ -876,7 +929,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(); 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..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 @@ -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; @@ -351,12 +352,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", // @@ -364,6 +366,45 @@ void selectByQuery() { ); } + @Test // GH-1803 + void selectByQueryWithColumnLimit() { + + Query query = Query.empty().columns("id", "alpha", "beta", "gamma"); + + String sql = sqlGenerator.selectByQuery(query, new MapSqlParameterSource()); + + assertThat(sql).contains( // + "SELECT dummy_entity.id1 AS id1, dummy_entity.alpha, dummy_entity.beta, dummy_entity.gamma", // + "FROM dummy_entity" // + ); + } + + @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.x_content AS ref_x_content", // + "FROM dummy_entity", // + "LEFT OUTER JOIN referenced_entity ref ON ref.dummy_entity = dummy_entity.id1"); + } + @Test // GH-1919 void selectBySortedQuery() { @@ -381,7 +422,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 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 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; } /**