diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SingleQueryFallbackDataAccessStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SingleQueryFallbackDataAccessStrategy.java index 9628588f..2b03127a 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SingleQueryFallbackDataAccessStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SingleQueryFallbackDataAccessStrategy.java @@ -29,6 +29,7 @@ import org.springframework.util.Assert; * Query Loading. * * @author Mark Paluch + * @author Jens Schauder * @since 3.2 */ class SingleQueryFallbackDataAccessStrategy extends DelegatingDataAccessStrategy { @@ -120,23 +121,25 @@ class SingleQueryFallbackDataAccessStrategy extends DelegatingDataAccessStrategy private boolean entityQualifiesForSingleQueryLoading(Class entityType) { - boolean referenceFound = false; for (PersistentPropertyPath path : converter.getMappingContext() .findPersistentPropertyPaths(entityType, __ -> true)) { RelationalPersistentProperty property = path.getLeafProperty(); if (property.isEntity()) { + // single references are currently not supported + if (!(property.isMap() || property.isCollectionLike())) { + return false; + } + // embedded entities are currently not supported if (property.isEmbedded()) { return false; } - // only a single reference is currently supported - if (referenceFound) { + // nested references are currently not supported + if (path.getLength() > 1) { return false; } - - referenceFound = true; } } return true; 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 858a69a7..20bc9568 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 @@ -22,6 +22,8 @@ import static org.assertj.core.api.SoftAssertions.*; import static org.springframework.data.jdbc.testing.TestConfiguration.*; import static org.springframework.data.jdbc.testing.TestDatabaseFeatures.Feature.*; +import java.sql.ResultSet; +import java.sql.SQLException; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Collections; @@ -37,6 +39,7 @@ import java.util.function.Function; import java.util.stream.IntStream; import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; @@ -44,6 +47,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.dao.DataAccessException; import org.springframework.dao.IncorrectUpdateSemanticsDataAccessException; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.data.annotation.Id; @@ -70,6 +74,7 @@ import org.springframework.data.relational.core.mapping.Table; import org.springframework.data.relational.core.query.Criteria; import org.springframework.data.relational.core.query.CriteriaDefinition; import org.springframework.data.relational.core.query.Query; +import org.springframework.jdbc.core.ResultSetExtractor; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ContextConfiguration; @@ -95,6 +100,7 @@ abstract class AbstractJdbcAggregateTemplateIntegrationTests { @Autowired JdbcAggregateOperations template; @Autowired NamedParameterJdbcOperations jdbcTemplate; @Autowired RelationalMappingContext mappingContext; + @Autowired NamedParameterJdbcOperations jdbc; LegoSet legoSet = createLegoSet("Star Destroyer"); @@ -1202,6 +1208,115 @@ abstract class AbstractJdbcAggregateTemplateIntegrationTests { assertThat(template.findById(entity.id, EnumArrayOwner.class).digits).isEqualTo(new Color[] { Color.BLUE }); } + @Test // GH-1448 + void multipleCollections() { + + MultipleCollections aggregate = new MultipleCollections(); + aggregate.name = "aggregate"; + + aggregate.listElements.add(new ListElement("one")); + aggregate.listElements.add(new ListElement("two")); + aggregate.listElements.add(new ListElement("three")); + + aggregate.setElements.add(new SetElement("one")); + aggregate.setElements.add(new SetElement("two")); + + aggregate.mapElements.put("alpha", new MapElement("one")); + aggregate.mapElements.put("beta", new MapElement("two")); + aggregate.mapElements.put("gamma", new MapElement("three")); + aggregate.mapElements.put("delta", new MapElement("four")); + + template.save(aggregate); + + MultipleCollections reloaded = template.findById(aggregate.id, MultipleCollections.class); + + assertSoftly(softly -> { + + softly.assertThat(reloaded.name).isEqualTo(aggregate.name); + + softly.assertThat(reloaded.listElements).containsExactly(aggregate.listElements.get(0), + aggregate.listElements.get(1), aggregate.listElements.get(2)); + + softly.assertThat(reloaded.setElements) + .containsExactlyInAnyOrder(aggregate.setElements.toArray(new SetElement[0])); + + softly.assertThat(reloaded.mapElements.get("alpha")).isEqualTo(new MapElement("one")); + softly.assertThat(reloaded.mapElements.get("beta")).isEqualTo(new MapElement("two")); + softly.assertThat(reloaded.mapElements.get("gamma")).isEqualTo(new MapElement("three")); + softly.assertThat(reloaded.mapElements.get("delta")).isEqualTo(new MapElement("four")); + }); + } + + @Test // GH-1448 + void multipleCollectionsWithEmptySet() { + + MultipleCollections aggregate = new MultipleCollections(); + aggregate.name = "aggregate"; + + aggregate.listElements.add(new ListElement("one")); + aggregate.listElements.add(new ListElement("two")); + aggregate.listElements.add(new ListElement("three")); + + aggregate.mapElements.put("alpha", new MapElement("one")); + aggregate.mapElements.put("beta", new MapElement("two")); + aggregate.mapElements.put("gamma", new MapElement("three")); + aggregate.mapElements.put("delta", new MapElement("four")); + + template.save(aggregate); + + MultipleCollections reloaded = template.findById(aggregate.id, MultipleCollections.class); + + assertSoftly(softly -> { + + softly.assertThat(reloaded.name).isEqualTo(aggregate.name); + + softly.assertThat(reloaded.listElements).containsExactly(aggregate.listElements.get(0), + aggregate.listElements.get(1), aggregate.listElements.get(2)); + + softly.assertThat(reloaded.setElements) + .containsExactlyInAnyOrder(aggregate.setElements.toArray(new SetElement[0])); + + softly.assertThat(reloaded.mapElements.get("alpha")).isEqualTo(new MapElement("one")); + softly.assertThat(reloaded.mapElements.get("beta")).isEqualTo(new MapElement("two")); + softly.assertThat(reloaded.mapElements.get("gamma")).isEqualTo(new MapElement("three")); + softly.assertThat(reloaded.mapElements.get("delta")).isEqualTo(new MapElement("four")); + }); + } + + @Test // GH-1448 + void multipleCollectionsWithEmptyList() { + + MultipleCollections aggregate = new MultipleCollections(); + aggregate.name = "aggregate"; + + aggregate.setElements.add(new SetElement("one")); + aggregate.setElements.add(new SetElement("two")); + + aggregate.mapElements.put("alpha", new MapElement("one")); + aggregate.mapElements.put("beta", new MapElement("two")); + aggregate.mapElements.put("gamma", new MapElement("three")); + aggregate.mapElements.put("delta", new MapElement("four")); + + template.save(aggregate); + + MultipleCollections reloaded = template.findById(aggregate.id, MultipleCollections.class); + + assertSoftly(softly -> { + + softly.assertThat(reloaded.name).isEqualTo(aggregate.name); + + softly.assertThat(reloaded.listElements).containsExactly(); + + softly.assertThat(reloaded.setElements) + .containsExactlyInAnyOrder(aggregate.setElements.toArray(new SetElement[0])); + + softly.assertThat(reloaded.mapElements.get("alpha")).isEqualTo(new MapElement("one")); + softly.assertThat(reloaded.mapElements.get("beta")).isEqualTo(new MapElement("two")); + softly.assertThat(reloaded.mapElements.get("gamma")).isEqualTo(new MapElement("three")); + softly.assertThat(reloaded.mapElements.get("delta")).isEqualTo(new MapElement("four")); + }); + } + private void saveAndUpdateAggregateWithVersion(VersionedAggregate aggregate, Function toConcreteNumber) { saveAndUpdateAggregateWithVersion(aggregate, toConcreteNumber, 0); @@ -1932,6 +2047,24 @@ abstract class AbstractJdbcAggregateTemplateIntegrationTests { @InsertOnlyProperty String insertOnly; } + @Table + static class MultipleCollections { + @Id Long id; + String name; + List listElements = new ArrayList<>(); + Set setElements = new HashSet<>(); + Map mapElements = new HashMap<>(); + } + + record ListElement(String name) { + } + + record SetElement(String name) { + } + + record MapElement(String name) { + } + @Configuration @Import(TestConfiguration.class) static class Config { diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-db2.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-db2.sql index f086a03b..2abc57c2 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-db2.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-db2.sql @@ -42,6 +42,11 @@ DROP TABLE WITH_ID_ONLY; DROP TABLE WITH_INSERT_ONLY; +DROP TABLE MULTIPLE_COLLECTIONS; +DROP TABLE MAP_ELEMENT; +DROP TABLE LIST_ELEMENT; +DROP TABLE SET_ELEMENT; + CREATE TABLE LEGO_SET ( "id1" BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, @@ -373,4 +378,30 @@ CREATE TABLE WITH_INSERT_ONLY ( ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, INSERT_ONLY VARCHAR(100) -); \ No newline at end of file +); + +CREATE TABLE MULTIPLE_COLLECTIONS +( + ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, + NAME VARCHAR(100) +); + +CREATE TABLE SET_ELEMENT +( + MULTIPLE_COLLECTIONS BIGINT, + NAME VARCHAR(100) +); + +CREATE TABLE LIST_ELEMENT +( + MULTIPLE_COLLECTIONS BIGINT, + MULTIPLE_COLLECTIONS_KEY INT, + NAME VARCHAR(100) +); + +CREATE TABLE MAP_ELEMENT +( + MULTIPLE_COLLECTIONS BIGINT, + MULTIPLE_COLLECTIONS_KEY VARCHAR(10), + NAME VARCHAR(100) +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-h2.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-h2.sql index a6e5eaba..1b1da1a1 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-h2.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-h2.sql @@ -340,4 +340,30 @@ CREATE TABLE WITH_INSERT_ONLY ( ID SERIAL PRIMARY KEY, INSERT_ONLY VARCHAR(100) -); \ No newline at end of file +); + +CREATE TABLE MULTIPLE_COLLECTIONS +( + ID SERIAL PRIMARY KEY, + NAME VARCHAR(100) +); + +CREATE TABLE SET_ELEMENT +( + MULTIPLE_COLLECTIONS BIGINT, + NAME VARCHAR(100) +); + +CREATE TABLE LIST_ELEMENT +( + MULTIPLE_COLLECTIONS BIGINT, + MULTIPLE_COLLECTIONS_KEY INT, + NAME VARCHAR(100) +); + +CREATE TABLE MAP_ELEMENT +( + MULTIPLE_COLLECTIONS BIGINT, + MULTIPLE_COLLECTIONS_KEY VARCHAR(10), + NAME VARCHAR(100) +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-hsql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-hsql.sql index dc738992..b74e716e 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-hsql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-hsql.sql @@ -341,4 +341,30 @@ CREATE TABLE WITH_INSERT_ONLY CREATE TABLE WITH_ID_ONLY ( ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY -) \ No newline at end of file +); + +CREATE TABLE MULTIPLE_COLLECTIONS +( + ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, + NAME VARCHAR(100) +); + +CREATE TABLE SET_ELEMENT +( + MULTIPLE_COLLECTIONS BIGINT, + NAME VARCHAR(100) +); + +CREATE TABLE LIST_ELEMENT +( + MULTIPLE_COLLECTIONS BIGINT, + MULTIPLE_COLLECTIONS_KEY INT, + NAME VARCHAR(100) +); + +CREATE TABLE MAP_ELEMENT +( + MULTIPLE_COLLECTIONS BIGINT, + MULTIPLE_COLLECTIONS_KEY VARCHAR(10), + NAME VARCHAR(100) +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mariadb.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mariadb.sql index 4258e7b4..29f0140d 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mariadb.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mariadb.sql @@ -314,4 +314,30 @@ CREATE TABLE WITH_INSERT_ONLY ( ID BIGINT AUTO_INCREMENT PRIMARY KEY, INSERT_ONLY VARCHAR(100) -); \ No newline at end of file +); + +CREATE TABLE MULTIPLE_COLLECTIONS +( + ID BIGINT AUTO_INCREMENT PRIMARY KEY, + NAME VARCHAR(100) +); + +CREATE TABLE SET_ELEMENT +( + MULTIPLE_COLLECTIONS BIGINT, + NAME VARCHAR(100) +); + +CREATE TABLE LIST_ELEMENT +( + MULTIPLE_COLLECTIONS BIGINT, + MULTIPLE_COLLECTIONS_KEY INT, + NAME VARCHAR(100) +); + +CREATE TABLE MAP_ELEMENT +( + MULTIPLE_COLLECTIONS BIGINT, + MULTIPLE_COLLECTIONS_KEY VARCHAR(10), + NAME VARCHAR(100) +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mssql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mssql.sql index e9a378f4..cacd54ed 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mssql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mssql.sql @@ -347,4 +347,35 @@ CREATE TABLE WITH_INSERT_ONLY ( ID BIGINT IDENTITY PRIMARY KEY, INSERT_ONLY VARCHAR(100) -); \ No newline at end of file +); + +DROP TABLE MULTIPLE_COLLECTIONS; +DROP TABLE MAP_ELEMENT; +DROP TABLE LIST_ELEMENT; +DROP TABLE SET_ELEMENT; + +CREATE TABLE MULTIPLE_COLLECTIONS +( + ID BIGINT IDENTITY PRIMARY KEY, + NAME VARCHAR(100) +); + +CREATE TABLE SET_ELEMENT +( + MULTIPLE_COLLECTIONS BIGINT, + NAME VARCHAR(100) +); + +CREATE TABLE LIST_ELEMENT +( + MULTIPLE_COLLECTIONS BIGINT, + MULTIPLE_COLLECTIONS_KEY INT, + NAME VARCHAR(100) +); + +CREATE TABLE MAP_ELEMENT +( + MULTIPLE_COLLECTIONS BIGINT, + MULTIPLE_COLLECTIONS_KEY VARCHAR(10), + NAME VARCHAR(100) +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mysql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mysql.sql index 40e32f16..3efb07a0 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mysql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-mysql.sql @@ -319,4 +319,30 @@ CREATE TABLE WITH_INSERT_ONLY ( ID BIGINT AUTO_INCREMENT PRIMARY KEY, INSERT_ONLY VARCHAR(100) -); \ No newline at end of file +); + +CREATE TABLE MULTIPLE_COLLECTIONS +( + ID BIGINT AUTO_INCREMENT PRIMARY KEY, + NAME VARCHAR(100) +); + +CREATE TABLE SET_ELEMENT +( + MULTIPLE_COLLECTIONS BIGINT, + NAME VARCHAR(100) +); + +CREATE TABLE LIST_ELEMENT +( + MULTIPLE_COLLECTIONS BIGINT, + MULTIPLE_COLLECTIONS_KEY INT, + NAME VARCHAR(100) +); + +CREATE TABLE MAP_ELEMENT +( + MULTIPLE_COLLECTIONS BIGINT, + MULTIPLE_COLLECTIONS_KEY VARCHAR(10), + NAME VARCHAR(100) +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-oracle.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-oracle.sql index 5a5c5baf..c4255de2 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-oracle.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-oracle.sql @@ -32,6 +32,11 @@ DROP TABLE WITH_LOCAL_DATE_TIME CASCADE CONSTRAINTS PURGE; DROP TABLE WITH_ID_ONLY CASCADE CONSTRAINTS PURGE; DROP TABLE WITH_INSERT_ONLY CASCADE CONSTRAINTS PURGE; +DROP TABLE MULTIPLE_COLLECTIONS CASCADE CONSTRAINTS PURGE; +DROP TABLE MAP_ELEMENT CASCADE CONSTRAINTS PURGE; +DROP TABLE LIST_ELEMENT CASCADE CONSTRAINTS PURGE; +DROP TABLE SET_ELEMENT CASCADE CONSTRAINTS PURGE; + CREATE TABLE LEGO_SET ( "id1" NUMBER GENERATED by default on null as IDENTITY PRIMARY KEY, @@ -354,4 +359,30 @@ CREATE TABLE WITH_INSERT_ONLY ( ID NUMBER GENERATED by default on null as IDENTITY PRIMARY KEY, INSERT_ONLY VARCHAR(100) -); \ No newline at end of file +); + +CREATE TABLE MULTIPLE_COLLECTIONS +( + ID NUMBER GENERATED by default on null as IDENTITY PRIMARY KEY, + NAME VARCHAR(100) +); + +CREATE TABLE SET_ELEMENT +( + MULTIPLE_COLLECTIONS NUMBER, + NAME VARCHAR(100) +); + +CREATE TABLE LIST_ELEMENT +( + MULTIPLE_COLLECTIONS NUMBER, + MULTIPLE_COLLECTIONS_KEY INT, + NAME VARCHAR(100) +); + +CREATE TABLE MAP_ELEMENT +( + MULTIPLE_COLLECTIONS NUMBER, + MULTIPLE_COLLECTIONS_KEY VARCHAR(10), + NAME VARCHAR(100) +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-postgres.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-postgres.sql index d43b5750..e401412e 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-postgres.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-postgres.sql @@ -35,6 +35,11 @@ DROP TABLE WITH_LOCAL_DATE_TIME; DROP TABLE WITH_ID_ONLY; DROP TABLE WITH_INSERT_ONLY; +DROP TABLE MULTIPLE_COLLECTIONS; +DROP TABLE MAP_ELEMENT; +DROP TABLE LIST_ELEMENT; +DROP TABLE SET_ELEMENT; + CREATE TABLE LEGO_SET ( "id1" SERIAL PRIMARY KEY, @@ -376,4 +381,30 @@ CREATE TABLE WITH_INSERT_ONLY ( ID SERIAL PRIMARY KEY, INSERT_ONLY VARCHAR(100) -); \ No newline at end of file +); + +CREATE TABLE MULTIPLE_COLLECTIONS +( + ID SERIAL PRIMARY KEY, + NAME VARCHAR(100) +); + +CREATE TABLE SET_ELEMENT +( + MULTIPLE_COLLECTIONS BIGINT, + NAME VARCHAR(100) +); + +CREATE TABLE LIST_ELEMENT +( + MULTIPLE_COLLECTIONS BIGINT, + MULTIPLE_COLLECTIONS_KEY INT, + NAME VARCHAR(100) +); + +CREATE TABLE MAP_ELEMENT +( + MULTIPLE_COLLECTIONS BIGINT, + MULTIPLE_COLLECTIONS_KEY VARCHAR(10), + NAME VARCHAR(100) +); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sqlgeneration/SingleQuerySqlGenerator.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sqlgeneration/SingleQuerySqlGenerator.java index 9326a55f..949dbc6a 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sqlgeneration/SingleQuerySqlGenerator.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sqlgeneration/SingleQuerySqlGenerator.java @@ -187,16 +187,12 @@ public class SingleQuerySqlGenerator implements SqlGenerator { backReferenceAlias = aliases.getBackReferenceAlias(basePath); columns.add(table.column(basePath.getTableInfo().reverseColumnInfo().name()).as(backReferenceAlias)); - if (basePath.isQualified()) { + keyAlias = aliases.getKeyAlias(basePath); + Expression keyExpression = basePath.isQualified() + ? table.column(basePath.getTableInfo().qualifierColumnInfo().name()).as(keyAlias) + : createRowNumberExpression(basePath, table, keyAlias); + columns.add(keyExpression); - keyAlias = aliases.getKeyAlias(basePath); - columns.add(table.column(basePath.getTableInfo().qualifierColumnInfo().name()).as(keyAlias)); - } else { - - String alias = aliases.getColumnAlias(basePath); - columns.add(new AliasedExpression(just("1"), alias)); - columnAliases.add(just(alias)); - } } String id = null; @@ -215,8 +211,7 @@ public class SingleQuerySqlGenerator implements SqlGenerator { SelectBuilder.BuildSelect buildSelect = condition != null ? select.where(condition) : select; - InlineQuery inlineQuery = InlineQuery.create(buildSelect.build(false), - aliases.getTableAlias(context.getAggregatePath(entity))); + InlineQuery inlineQuery = InlineQuery.create(buildSelect.build(false), aliases.getTableAlias(basePath)); return QueryMeta.of(basePath, inlineQuery, columnAliases, just(id), just(backReferenceAlias), just(keyAlias), just(rowNumberAlias), just(rowCountAlias)); } @@ -252,6 +247,7 @@ public class SingleQuerySqlGenerator implements SqlGenerator { Expressions.just(backReferenceAlias)); select = select.leftOuterJoin(queryMeta.inlineQuery).on(joinCondition); } + return select; } @@ -274,18 +270,54 @@ public class SingleQuerySqlGenerator implements SqlGenerator { private SelectBuilder.SelectOrdered applyWhereCondition(AggregatePath rootPath, List inlineQueries, SelectBuilder.SelectJoin select) { - SelectBuilder.SelectWhereAndOr selectWhere = null; - for (QueryMeta queryMeta : inlineQueries) { + SelectBuilder.SelectWhere selectWhere = (SelectBuilder.SelectWhere) select; - AggregatePath path = queryMeta.basePath; - Expression childRowNumber = just(aliases.getRowNumberAlias(path)); - Condition pseudoJoinCondition = Conditions.isNull(childRowNumber) - .or(Conditions.isEqual(childRowNumber, Expressions.just(aliases.getRowNumberAlias(rootPath)))) - .or(Conditions.isGreater(childRowNumber, Expressions.just(aliases.getRowCountAlias(rootPath)))); + Condition joins = null; - selectWhere = ((SelectBuilder.SelectWhere) select).where(pseudoJoinCondition); + for (int left = 0; left < inlineQueries.size(); left++) { + + QueryMeta leftQueryMeta = inlineQueries.get(left); + AggregatePath leftPath = leftQueryMeta.basePath; + Expression leftRowNumber = just(aliases.getRowNumberAlias(leftPath)); + Expression leftRowCount = just(aliases.getRowCountAlias(leftPath)); + + for (int right = left + 1; right < inlineQueries.size(); right++) { + + QueryMeta rightQueryMeta = inlineQueries.get(right); + AggregatePath rightPath = rightQueryMeta.basePath; + Expression rightRowNumber = just(aliases.getRowNumberAlias(rightPath)); + Expression rightRowCount = just(aliases.getRowCountAlias(rightPath)); + + System.out.println("joining: " + leftPath + " and " + rightPath); + + Condition mutualJoin = Conditions.isEqual(leftRowNumber, rightRowNumber).or(Conditions.isNull(leftRowNumber)) + .or(Conditions.isNull(rightRowNumber)) + .or(Conditions.nest(Conditions.isGreater(leftRowNumber, rightRowCount) + .and(Conditions.isEqual(rightRowNumber, SQL.literalOf(1))))) + .or(Conditions.nest(Conditions.isGreater(rightRowNumber, leftRowCount) + .and(Conditions.isEqual(leftRowNumber, SQL.literalOf(1))))); + + mutualJoin = Conditions.nest(mutualJoin); + + if (joins == null) { + joins = mutualJoin; + } else { + joins = joins.and(mutualJoin); + } + } } + // for (QueryMeta queryMeta : inlineQueries) { + // + // AggregatePath path = queryMeta.basePath; + // Expression childRowNumber = just(aliases.getRowNumberAlias(path)); + // Condition pseudoJoinCondition = Conditions.isNull(childRowNumber) + // .or(Conditions.isEqual(childRowNumber, Expressions.just(aliases.getRowNumberAlias(rootPath)))) + // .or(Conditions.isGreater(childRowNumber, Expressions.just(aliases.getRowCountAlias(rootPath)))); + // + selectWhere = (SelectBuilder.SelectWhere) selectWhere.where(joins); + // } + return selectWhere == null ? (SelectBuilder.SelectOrdered) select : selectWhere; } diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sqlgeneration/AliasFactoryUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sqlgeneration/AliasFactoryUnitTests.java index 5f0f9887..840fa2ff 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sqlgeneration/AliasFactoryUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sqlgeneration/AliasFactoryUnitTests.java @@ -53,7 +53,7 @@ class AliasFactoryUnitTests { } @Test - void nameGetsSanatized() { + void nameGetsSanitized() { String alias = aliasFactory.getColumnAlias( context.getAggregatePath( context.getPersistentPropertyPath("evil", DummyEntity.class))); @@ -136,6 +136,21 @@ class AliasFactoryUnitTests { } } + @Nested + class TableAlias { + @Test // GH-1448 + void tableAliasIsDifferentForDifferentPathsToSameEntity() { + + String alias = aliasFactory.getTableAlias( + context.getAggregatePath(context.getPersistentPropertyPath("dummy", Reference.class))); + + String alias2 = aliasFactory.getTableAlias( + context.getAggregatePath(context.getPersistentPropertyPath("dummy2", Reference.class))); + + assertThat(alias).isNotEqualTo(alias2); + } + } + static class DummyEntity { String name; @@ -144,5 +159,6 @@ class AliasFactoryUnitTests { static class Reference { DummyEntity dummy; + DummyEntity dummy2; } } diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sqlgeneration/SingleQuerySqlGeneratorUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sqlgeneration/SingleQuerySqlGeneratorUnitTests.java index ade6e0da..2f8a8659 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sqlgeneration/SingleQuerySqlGeneratorUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sqlgeneration/SingleQuerySqlGeneratorUnitTests.java @@ -161,11 +161,9 @@ class SingleQuerySqlGeneratorUnitTests { func("coalesce", col(trivialsRowNumber), lit(1))), // col(backref), // col(keyAlias) // - ).extractWhereClause() // - .doesNotContainIgnoringCase("and") // - .containsIgnoringCase(trivialsRowNumber + " is null") // - .containsIgnoringCase(trivialsRowNumber + " = " + rootRowNumber) // - .containsIgnoringCase(trivialsRowNumber + " > " + rootCount); + ) + .extractWhereClause() // + .isEqualTo(""); baseSelect.hasInlineViewSelectingFrom("\"single_reference_aggregate\"") // .hasExactlyColumns( // lit(1).as(rnAlias()), lit(1).as(rootCount), // diff --git a/src/main/antora/modules/ROOT/pages/jdbc/entity-persistence.adoc b/src/main/antora/modules/ROOT/pages/jdbc/entity-persistence.adoc index 4e3449f8..08b0420d 100644 --- a/src/main/antora/modules/ROOT/pages/jdbc/entity-persistence.adoc +++ b/src/main/antora/modules/ROOT/pages/jdbc/entity-persistence.adoc @@ -28,11 +28,11 @@ If the aggregate root references other entities those are loaded with separate s With this an arbitrary number of aggregates can be fully loaded with a single SQL query. This should be significant more efficient, especially for complex aggregates, consisting of many entities. + -Currently, Single Query Loading is restricted to: +Currently, Single Query Loading is restricted in different ways: -1. It only works for aggregates that only reference one entity collection.The plan is to remove this constraint in the future. +1. The aggregate must not have nested collections, this includes `Map`.The plan is to remove this constraint in the future. -2. The aggregate must also not use `AggregateReference` or embedded entities.The plan is to remove this constraint in the future. +2. The aggregate must not use `AggregateReference` or embedded entities.The plan is to remove this constraint in the future. 3. The database dialect must support it.Of the dialects provided by Spring Data JDBC all but H2 and HSQL support this.H2 and HSQL don't support analytic functions (aka windowing functions). @@ -40,6 +40,8 @@ Currently, Single Query Loading is restricted to: 5. Single Query Loading needs to be enabled in the `JdbcMappingContext`, by calling `setSingleQueryLoadingEnabled(true)` +If any condition is not fulfilled Spring Data JDBC falls back to the default approach of loading aggregates. + NOTE: Single Query Loading is to be considered experimental. We appreciate feedback on how it works for you.