From 83236b267cda1894a16291650cc6b893011e7069 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 19 Nov 2024 10:10:09 +0100 Subject: [PATCH 01/21] Prepare next development iteration. See #1943 --- pom.xml | 2 +- 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, 8 insertions(+), 8 deletions(-) diff --git a/pom.xml b/pom.xml index d796b95b6d..14319ee076 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-relational-parent - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT pom Spring Data Relational Parent diff --git a/spring-data-jdbc-distribution/pom.xml b/spring-data-jdbc-distribution/pom.xml index 9c02f50608..b3c39e64c3 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 - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT ../pom.xml diff --git a/spring-data-jdbc/pom.xml b/spring-data-jdbc/pom.xml index 87d4f9704a..e61fd64020 100644 --- a/spring-data-jdbc/pom.xml +++ b/spring-data-jdbc/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-jdbc - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT Spring Data JDBC Spring Data module for JDBC repositories. @@ -15,7 +15,7 @@ org.springframework.data spring-data-relational-parent - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT diff --git a/spring-data-r2dbc/pom.xml b/spring-data-r2dbc/pom.xml index c46dd6bf6b..3ee76fd3c1 100644 --- a/spring-data-r2dbc/pom.xml +++ b/spring-data-r2dbc/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-r2dbc - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT Spring Data R2DBC Spring Data module for R2DBC @@ -15,7 +15,7 @@ org.springframework.data spring-data-relational-parent - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT diff --git a/spring-data-relational/pom.xml b/spring-data-relational/pom.xml index 9760a0f1e7..8fd6d7a6f0 100644 --- a/spring-data-relational/pom.xml +++ b/spring-data-relational/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-relational - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT Spring Data Relational Spring Data Relational support @@ -14,7 +14,7 @@ org.springframework.data spring-data-relational-parent - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT From 17a16944d1f57e479b6fd2436b7671926cafda01 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 19 Nov 2024 10:10:10 +0100 Subject: [PATCH 02/21] After release cleanups. See #1943 --- Jenkinsfile | 2 +- pom.xml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 8919ba10f4..8b0dcdd33c 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -9,7 +9,7 @@ pipeline { triggers { pollSCM 'H/10 * * * *' - upstream(upstreamProjects: "spring-data-commons/main", threshold: hudson.model.Result.SUCCESS) + upstream(upstreamProjects: "spring-data-commons/4.0.x", threshold: hudson.model.Result.SUCCESS) } options { diff --git a/pom.xml b/pom.xml index 14319ee076..0acc4a7020 100644 --- a/pom.xml +++ b/pom.xml @@ -15,12 +15,12 @@ org.springframework.data.build spring-data-parent - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT spring-data-jdbc - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT 4.21.1 reuseReports From d6d51be750ae4487376ff6ef4248e789496effd1 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 19 Nov 2024 15:10:19 +0100 Subject: [PATCH 03/21] Adopt to deprecation removals in Commons. Closes #1944 --- .../query/StringBasedJdbcQuery.java | 65 ------------------- .../JdbcRepositoryIntegrationTests.java | 10 +-- .../JdbcQueryLookupStrategyUnitTests.java | 2 - .../data/jdbc/testing/TestConfiguration.java | 5 +- .../query/R2dbcParameterAccessor.java | 5 +- .../query/StringBasedR2dbcQuery.java | 56 ++-------------- .../support/R2dbcRepositoryFactory.java | 14 ++-- .../support/R2dbcRepositoryFactoryBean.java | 10 --- ...sitoryConfigurationExtensionUnitTests.java | 2 +- .../EmbeddedRelationalPersistentEntity.java | 7 -- 10 files changed, 23 insertions(+), 153 deletions(-) diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java index 7c4ff0d78c..2513bb4b1a 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java @@ -32,9 +32,7 @@ import org.springframework.beans.BeanInstantiationException; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.BeanFactory; -import org.springframework.core.env.StandardEnvironment; import org.springframework.data.expression.ValueEvaluationContext; -import org.springframework.data.expression.ValueExpressionParser; import org.springframework.data.jdbc.core.convert.JdbcColumnTypes; import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.core.mapping.JdbcValue; @@ -42,11 +40,8 @@ import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.repository.query.RelationalParameterAccessor; import org.springframework.data.relational.repository.query.RelationalParametersParameterAccessor; -import org.springframework.data.repository.query.CachingValueExpressionDelegate; import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.Parameters; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; -import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.repository.query.ValueExpressionQueryRewriter; @@ -91,43 +86,6 @@ public class StringBasedJdbcQuery extends AbstractJdbcQuery { private final CachedResultSetExtractorFactory cachedResultSetExtractorFactory; private final ValueExpressionDelegate delegate; - /** - * Creates a new {@link StringBasedJdbcQuery} for the given {@link JdbcQueryMethod}, {@link RelationalMappingContext} - * and {@link RowMapper}. - * - * @param queryMethod must not be {@literal null}. - * @param operations must not be {@literal null}. - * @param defaultRowMapper can be {@literal null} (only in case of a modifying query). - * @deprecated since 3.4, use the constructors accepting {@link ValueExpressionDelegate} instead. - */ - @Deprecated(since = "3.4") - public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOperations operations, - @Nullable RowMapper defaultRowMapper, JdbcConverter converter, - QueryMethodEvaluationContextProvider evaluationContextProvider) { - this(queryMethod.getRequiredQuery(), queryMethod, operations, result -> (RowMapper) defaultRowMapper, - converter, evaluationContextProvider); - } - - /** - * Creates a new {@link StringBasedJdbcQuery} for the given {@link JdbcQueryMethod}, {@link RelationalMappingContext} - * and {@link RowMapperFactory}. - * - * @param queryMethod must not be {@literal null}. - * @param operations must not be {@literal null}. - * @param rowMapperFactory must not be {@literal null}. - * @param converter must not be {@literal null}. - * @param evaluationContextProvider must not be {@literal null}. - * @since 2.3 - * @deprecated use alternative constructor - */ - @Deprecated(since = "3.4") - public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOperations operations, - RowMapperFactory rowMapperFactory, JdbcConverter converter, - QueryMethodEvaluationContextProvider evaluationContextProvider) { - this(queryMethod.getRequiredQuery(), queryMethod, operations, rowMapperFactory, converter, - evaluationContextProvider); - } - /** * Creates a new {@link StringBasedJdbcQuery} for the given {@link JdbcQueryMethod}, {@link RelationalMappingContext} * and {@link RowMapperFactory}. @@ -197,29 +155,6 @@ public StringBasedJdbcQuery(String query, JdbcQueryMethod queryMethod, NamedPara this.delegate = delegate; } - /** - * Creates a new {@link StringBasedJdbcQuery} for the given {@link JdbcQueryMethod}, {@link RelationalMappingContext} - * and {@link RowMapperFactory}. - * - * @param query must not be {@literal null} or empty. - * @param queryMethod must not be {@literal null}. - * @param operations must not be {@literal null}. - * @param rowMapperFactory must not be {@literal null}. - * @param converter must not be {@literal null}. - * @param evaluationContextProvider must not be {@literal null}. - * @since 3.4 - * @deprecated since 3.4, use the constructors accepting {@link ValueExpressionDelegate} instead. - */ - @Deprecated(since = "3.4") - public StringBasedJdbcQuery(String query, JdbcQueryMethod queryMethod, NamedParameterJdbcOperations operations, - RowMapperFactory rowMapperFactory, JdbcConverter converter, - QueryMethodEvaluationContextProvider evaluationContextProvider) { - this(query, queryMethod, operations, rowMapperFactory, converter, new CachingValueExpressionDelegate( - new QueryMethodValueEvaluationContextAccessor(new StandardEnvironment(), rootObject -> evaluationContextProvider - .getEvaluationContext(queryMethod.getParameters(), new Object[] { rootObject })), - ValueExpressionParser.create())); - } - @Override public Object execute(Object[] objects) { 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 7c854b823f..3f54716cec 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 @@ -75,11 +75,11 @@ import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries; import org.springframework.data.repository.core.support.RepositoryFactoryCustomizer; -import org.springframework.data.repository.query.ExtensionAwareQueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.FluentQuery; import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.QueryByExampleExecutor; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.data.spel.EvaluationContextProvider; +import org.springframework.data.spel.ExtensionAwareEvaluationContextProvider; import org.springframework.data.spel.spi.EvaluationContextExtension; import org.springframework.data.support.WindowIterator; import org.springframework.data.util.Streamable; @@ -1573,12 +1573,12 @@ MyEventListener eventListener() { } @Bean - public QueryMethodEvaluationContextProvider extensionAware(List exts) { - return new ExtensionAwareQueryMethodEvaluationContextProvider(exts); + public EvaluationContextProvider extensionAware(List exts) { + return new ExtensionAwareEvaluationContextProvider(exts); } @Bean - RepositoryFactoryCustomizer customizer(QueryMethodEvaluationContextProvider provider) { + RepositoryFactoryCustomizer customizer(EvaluationContextProvider provider) { return repositoryFactory -> repositoryFactory.setEvaluationContextProvider(provider); } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategyUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategyUnitTests.java index 7b70956890..114df1e305 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategyUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategyUnitTests.java @@ -40,7 +40,6 @@ import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.query.QueryLookupStrategy; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.util.TypeInformation; @@ -72,7 +71,6 @@ class JdbcQueryLookupStrategyUnitTests { private RepositoryMetadata metadata; private NamedQueries namedQueries = mock(NamedQueries.class); private NamedParameterJdbcOperations operations = mock(NamedParameterJdbcOperations.class); - QueryMethodEvaluationContextProvider evaluationContextProvider = mock(QueryMethodEvaluationContextProvider.class); @BeforeEach void setup() { diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java index ea3e5482cf..e1b429f34c 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java @@ -50,7 +50,7 @@ import org.springframework.data.relational.core.mapping.NamingStrategy; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.repository.core.NamedQueries; -import org.springframework.data.repository.query.ExtensionAwareQueryMethodEvaluationContextProvider; +import org.springframework.data.spel.ExtensionAwareEvaluationContextProvider; import org.springframework.data.spel.spi.EvaluationContextExtension; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; @@ -99,8 +99,7 @@ JdbcRepositoryFactory jdbcRepositoryFactory( namedQueries.map(it -> it.iterator().next()).ifPresent(factory::setNamedQueries); - factory.setEvaluationContextProvider( - new ExtensionAwareQueryMethodEvaluationContextProvider(evaulationContextExtensions)); + factory.setEvaluationContextProvider(new ExtensionAwareEvaluationContextProvider(evaulationContextExtensions)); return factory; } diff --git a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcParameterAccessor.java b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcParameterAccessor.java index 744140b317..95ecebc615 100644 --- a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcParameterAccessor.java +++ b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcParameterAccessor.java @@ -15,7 +15,6 @@ */ package org.springframework.data.r2dbc.repository.query; -import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -25,9 +24,11 @@ import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import org.reactivestreams.Publisher; + import org.springframework.data.relational.repository.query.RelationalParametersParameterAccessor; import org.springframework.data.repository.util.ReactiveWrapperConverters; -import org.springframework.data.repository.util.ReactiveWrappers; +import org.springframework.data.util.ReactiveWrappers; /** * Reactive {@link org.springframework.data.repository.query.ParametersParameterAccessor} implementation that subscribes diff --git a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java index a72aa6dafa..0519a55634 100644 --- a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java +++ b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java @@ -22,24 +22,18 @@ import java.util.List; import java.util.Map; -import org.springframework.core.env.StandardEnvironment; import org.springframework.data.expression.ReactiveValueEvaluationContextProvider; import org.springframework.data.expression.ValueEvaluationContext; import org.springframework.data.expression.ValueEvaluationContextProvider; -import org.springframework.data.expression.ValueExpressionParser; import org.springframework.data.r2dbc.convert.R2dbcConverter; import org.springframework.data.r2dbc.core.R2dbcEntityOperations; import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy; import org.springframework.data.r2dbc.dialect.BindTargetBinder; import org.springframework.data.r2dbc.repository.Query; import org.springframework.data.relational.repository.query.RelationalParameterAccessor; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; -import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor; -import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.spel.ExpressionDependencies; -import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.r2dbc.core.DatabaseClient; import org.springframework.r2dbc.core.Parameter; @@ -64,46 +58,6 @@ public class StringBasedR2dbcQuery extends AbstractR2dbcQuery { private final ReactiveDataAccessStrategy dataAccessStrategy; private final ReactiveValueEvaluationContextProvider valueContextProvider; - /** - * Creates a new {@link StringBasedR2dbcQuery} for the given {@link StringBasedR2dbcQuery}, {@link DatabaseClient}, - * {@link SpelExpressionParser}, and {@link QueryMethodEvaluationContextProvider}. - * - * @param queryMethod must not be {@literal null}. - * @param entityOperations must not be {@literal null}. - * @param converter must not be {@literal null}. - * @param dataAccessStrategy must not be {@literal null}. - * @param expressionParser must not be {@literal null}. - * @param evaluationContextProvider must not be {@literal null}. - * @deprecated use the constructor version with {@link ValueExpressionDelegate} - */ - @Deprecated(since = "3.4") - public StringBasedR2dbcQuery(R2dbcQueryMethod queryMethod, R2dbcEntityOperations entityOperations, - R2dbcConverter converter, ReactiveDataAccessStrategy dataAccessStrategy, ExpressionParser expressionParser, - ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider) { - this(queryMethod.getRequiredAnnotatedQuery(), queryMethod, entityOperations, converter, dataAccessStrategy, - expressionParser, evaluationContextProvider); - } - - /** - * Create a new {@link StringBasedR2dbcQuery} for the given {@code query}, {@link R2dbcQueryMethod}, - * {@link DatabaseClient}, {@link SpelExpressionParser}, and {@link QueryMethodEvaluationContextProvider}. - * - * @param query must not be {@literal null}. - * @param method must not be {@literal null}. - * @param entityOperations must not be {@literal null}. - * @param converter must not be {@literal null}. - * @param dataAccessStrategy must not be {@literal null}. - * @param expressionParser must not be {@literal null}. - * @param evaluationContextProvider must not be {@literal null}. - * @deprecated use the constructor version with {@link ValueExpressionDelegate} - */ - @Deprecated(since = "3.4") - public StringBasedR2dbcQuery(String query, R2dbcQueryMethod method, R2dbcEntityOperations entityOperations, - R2dbcConverter converter, ReactiveDataAccessStrategy dataAccessStrategy, ExpressionParser expressionParser, - ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider) { - this(query, method, entityOperations, converter, dataAccessStrategy, new ValueExpressionDelegate(new QueryMethodValueEvaluationContextAccessor(new StandardEnvironment(), evaluationContextProvider.getEvaluationContextProvider()), ValueExpressionParser.create(() -> expressionParser))); - } - /** * Create a new {@link StringBasedR2dbcQuery} for the given {@code query}, {@link R2dbcQueryMethod}, * {@link DatabaseClient}, {@link SpelExpressionParser}, and {@link QueryMethodEvaluationContextProvider}. @@ -115,8 +69,10 @@ public StringBasedR2dbcQuery(String query, R2dbcQueryMethod method, R2dbcEntityO * @param valueExpressionDelegate must not be {@literal null}. */ public StringBasedR2dbcQuery(R2dbcQueryMethod method, R2dbcEntityOperations entityOperations, - R2dbcConverter converter, ReactiveDataAccessStrategy dataAccessStrategy, ValueExpressionDelegate valueExpressionDelegate) { - this(method.getRequiredAnnotatedQuery(), method, entityOperations, converter, dataAccessStrategy, valueExpressionDelegate); + R2dbcConverter converter, ReactiveDataAccessStrategy dataAccessStrategy, + ValueExpressionDelegate valueExpressionDelegate) { + this(method.getRequiredAnnotatedQuery(), method, entityOperations, converter, dataAccessStrategy, + valueExpressionDelegate); } /** @@ -130,7 +86,8 @@ public StringBasedR2dbcQuery(R2dbcQueryMethod method, R2dbcEntityOperations enti * @param valueExpressionDelegate must not be {@literal null}. */ public StringBasedR2dbcQuery(String query, R2dbcQueryMethod method, R2dbcEntityOperations entityOperations, - R2dbcConverter converter, ReactiveDataAccessStrategy dataAccessStrategy, ValueExpressionDelegate valueExpressionDelegate) { + R2dbcConverter converter, ReactiveDataAccessStrategy dataAccessStrategy, + ValueExpressionDelegate valueExpressionDelegate) { super(method, entityOperations, converter); @@ -148,7 +105,6 @@ public StringBasedR2dbcQuery(String query, R2dbcQueryMethod method, R2dbcEntityO this.valueContextProvider = (ReactiveValueEvaluationContextProvider) valueContextProvider; this.expressionDependencies = createExpressionDependencies(); - if (method.isSliceQuery()) { throw new UnsupportedOperationException( "Slice queries are not supported using string-based queries; Offending method: " + method); diff --git a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java index 3540cfcd98..ff9110592b 100644 --- a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java +++ b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java @@ -40,7 +40,6 @@ import org.springframework.data.repository.query.CachingValueExpressionDelegate; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy.Key; -import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.lang.Nullable; @@ -78,7 +77,6 @@ public R2dbcRepositoryFactory(DatabaseClient databaseClient, ReactiveDataAccessS this.converter = dataAccessStrategy.getConverter(); this.mappingContext = this.converter.getMappingContext(); this.operations = new R2dbcEntityTemplate(this.databaseClient, this.dataAccessStrategy); - setEvaluationContextProvider(ReactiveQueryMethodEvaluationContextProvider.DEFAULT); } /** @@ -96,7 +94,6 @@ public R2dbcRepositoryFactory(R2dbcEntityOperations operations) { this.converter = dataAccessStrategy.getConverter(); this.mappingContext = this.converter.getMappingContext(); this.operations = operations; - setEvaluationContextProvider(ReactiveQueryMethodEvaluationContextProvider.DEFAULT); } @Override @@ -116,7 +113,8 @@ protected Object getTargetRepository(RepositoryInformation information) { @Override protected Optional getQueryLookupStrategy(@Nullable Key key, ValueExpressionDelegate valueExpressionDelegate) { - return Optional.of(new R2dbcQueryLookupStrategy(operations, new CachingValueExpressionDelegate(valueExpressionDelegate), converter, dataAccessStrategy)); + return Optional.of(new R2dbcQueryLookupStrategy(operations, + new CachingValueExpressionDelegate(valueExpressionDelegate), converter, dataAccessStrategy)); } public RelationalEntityInformation getEntityInformation(Class domainClass) { @@ -145,9 +143,8 @@ private static class R2dbcQueryLookupStrategy extends RelationalQueryLookupStrat private final ValueExpressionDelegate delegate; private final ReactiveDataAccessStrategy dataAccessStrategy; - R2dbcQueryLookupStrategy(R2dbcEntityOperations entityOperations, - ValueExpressionDelegate delegate, R2dbcConverter converter, - ReactiveDataAccessStrategy dataAccessStrategy) { + R2dbcQueryLookupStrategy(R2dbcEntityOperations entityOperations, ValueExpressionDelegate delegate, + R2dbcConverter converter, ReactiveDataAccessStrategy dataAccessStrategy) { super(converter.getMappingContext(), dataAccessStrategy.getDialect()); this.delegate = delegate; @@ -169,7 +166,8 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, : queryMethod.getRequiredAnnotatedQuery(); query = evaluateTableExpressions(metadata, query); - return new StringBasedR2dbcQuery(query, queryMethod, this.entityOperations, this.converter, this.dataAccessStrategy, this.delegate); + return new StringBasedR2dbcQuery(query, queryMethod, this.entityOperations, this.converter, + this.dataAccessStrategy, this.delegate); } else { return new PartTreeR2dbcQuery(queryMethod, this.entityOperations, this.converter, this.dataAccessStrategy); diff --git a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryBean.java b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryBean.java index 827cbc1c49..9f02fba47f 100644 --- a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryBean.java +++ b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryBean.java @@ -16,10 +16,8 @@ package org.springframework.data.r2dbc.repository.support; import java.io.Serializable; -import java.util.Optional; import org.springframework.beans.BeansException; -import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.data.mapping.context.MappingContext; @@ -29,8 +27,6 @@ import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; import org.springframework.data.repository.core.support.RepositoryFactorySupport; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; -import org.springframework.data.repository.query.ReactiveExtensionAwareQueryMethodEvaluationContextProvider; import org.springframework.lang.Nullable; import org.springframework.r2dbc.core.DatabaseClient; import org.springframework.util.Assert; @@ -98,12 +94,6 @@ protected final RepositoryFactorySupport createRepositoryFactory() { : getFactoryInstance(this.client, this.dataAccessStrategy); } - @Override - protected Optional createDefaultQueryMethodEvaluationContextProvider( - ListableBeanFactory beanFactory) { - return Optional.of(new ReactiveExtensionAwareQueryMethodEvaluationContextProvider(beanFactory)); - } - /** * Creates and initializes a {@link RepositoryFactorySupport} instance. * diff --git a/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoryConfigurationExtensionUnitTests.java b/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoryConfigurationExtensionUnitTests.java index 5ae04ba37f..187d90d045 100644 --- a/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoryConfigurationExtensionUnitTests.java +++ b/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoryConfigurationExtensionUnitTests.java @@ -49,7 +49,7 @@ class R2dbcRepositoryConfigurationExtensionUnitTests { private final BeanDefinitionRegistry registry = new DefaultListableBeanFactory(); private final RepositoryConfigurationSource configurationSource = new AnnotationRepositoryConfigurationSource( - metadata, EnableR2dbcRepositories.class, loader, environment, registry); + metadata, EnableR2dbcRepositories.class, loader, environment, registry, null); @Test // gh-13 void isStrictMatchIfDomainTypeIsAnnotatedWithDocument() { diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/EmbeddedRelationalPersistentEntity.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/EmbeddedRelationalPersistentEntity.java index 27359f7592..78bfa01d37 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/EmbeddedRelationalPersistentEntity.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/EmbeddedRelationalPersistentEntity.java @@ -106,13 +106,6 @@ public String getName() { return delegate.getName(); } - @Override - @Deprecated - @Nullable - public PreferredConstructor getPersistenceConstructor() { - return delegate.getPersistenceConstructor(); - } - @Override @Nullable public InstanceCreatorMetadata getInstanceCreatorMetadata() { From d2c45f3d1fa9858749aa3f1fb57ef4597cb88a0b Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 20 Nov 2024 10:43:27 +0100 Subject: [PATCH 04/21] Polishing. Fix invalid Javadoc references. See #1944 --- .../data/r2dbc/repository/query/StringBasedR2dbcQuery.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java index 0519a55634..fe0a6ccc0b 100644 --- a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java +++ b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java @@ -34,7 +34,6 @@ import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.spel.ExpressionDependencies; -import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.r2dbc.core.DatabaseClient; import org.springframework.r2dbc.core.Parameter; import org.springframework.r2dbc.core.PreparedOperation; @@ -60,7 +59,7 @@ public class StringBasedR2dbcQuery extends AbstractR2dbcQuery { /** * Create a new {@link StringBasedR2dbcQuery} for the given {@code query}, {@link R2dbcQueryMethod}, - * {@link DatabaseClient}, {@link SpelExpressionParser}, and {@link QueryMethodEvaluationContextProvider}. + * {@link DatabaseClient}, and {@link ValueExpressionDelegate}. * * @param method must not be {@literal null}. * @param entityOperations must not be {@literal null}. @@ -77,7 +76,7 @@ public StringBasedR2dbcQuery(R2dbcQueryMethod method, R2dbcEntityOperations enti /** * Create a new {@link StringBasedR2dbcQuery} for the given {@code query}, {@link R2dbcQueryMethod}, - * {@link DatabaseClient}, {@link SpelExpressionParser}, and {@link QueryMethodEvaluationContextProvider}. + * {@link DatabaseClient}, and {@link ValueExpressionDelegate}. * * @param method must not be {@literal null}. * @param entityOperations must not be {@literal null}. From 5d913c8be2e3aceced818f87d4d2b62700f25bf2 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 25 Nov 2024 08:43:18 +0100 Subject: [PATCH 05/21] Adopt to deprecation removals in Commons. See #1944 --- .../data/r2dbc/repository/query/R2dbcQueryMethod.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethod.java b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethod.java index f210ed90c7..f4d1401970 100644 --- a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethod.java +++ b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethod.java @@ -15,8 +15,6 @@ */ package org.springframework.data.r2dbc.repository.query; -import static org.springframework.data.repository.util.ClassUtils.*; - import java.lang.reflect.Method; import java.util.Optional; @@ -89,7 +87,7 @@ public R2dbcQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFa this.mappingContext = mappingContext; - if (hasParameterOfType(method, Pageable.class)) { + if (ReflectionUtils.hasParameterOfType(method, Pageable.class)) { TypeInformation returnType = TypeInformation.fromReturnTypeOf(method); @@ -110,7 +108,7 @@ public R2dbcQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFa method.toString())); } - if (hasParameterOfType(method, Sort.class)) { + if (ReflectionUtils.hasParameterOfType(method, Sort.class)) { throw new IllegalStateException(String.format("Method must not have Pageable *and* Sort parameter; " + "Use sorting capabilities on Pageable instead; Offending method: %s", method.toString())); } From 38edf0db892c4a223850592feb05f4f475351d9a Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 24 Jan 2025 10:48:00 +0100 Subject: [PATCH 06/21] Prepare 4.0 M1 (2025.1.0). See #1943 --- pom.xml | 20 ++++---------------- src/main/resources/notice.txt | 2 +- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/pom.xml b/pom.xml index 0acc4a7020..8c01884e43 100644 --- a/pom.xml +++ b/pom.xml @@ -15,12 +15,12 @@ org.springframework.data.build spring-data-parent - 4.0.0-SNAPSHOT + 4.0.0-M1 spring-data-jdbc - 4.0.0-SNAPSHOT + 4.0.0-M1 4.21.1 reuseReports @@ -310,20 +310,8 @@ - - spring-snapshot - https://repo.spring.io/snapshot - - true - - - false - - - - spring-milestone - https://repo.spring.io/milestone - + + diff --git a/src/main/resources/notice.txt b/src/main/resources/notice.txt index 2d1d063052..5cdfd0a640 100644 --- a/src/main/resources/notice.txt +++ b/src/main/resources/notice.txt @@ -1,4 +1,4 @@ -Spring Data Relational 3.5 RC1 (2025.0.0) +Spring Data Relational 4.0 M1 (2025.1.0) Copyright (c) [2017-2019] Pivotal Software, Inc. This product is licensed to you under the Apache License, Version 2.0 (the "License"). From 0803dc4fc84bdab9172ecb3f86743be3addebdad Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 24 Jan 2025 10:48:56 +0100 Subject: [PATCH 07/21] Release version 4.0 M1 (2025.1.0). See #1943 --- pom.xml | 2 +- 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, 8 insertions(+), 8 deletions(-) diff --git a/pom.xml b/pom.xml index 8c01884e43..54fb91f67b 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-M1 pom Spring Data Relational Parent diff --git a/spring-data-jdbc-distribution/pom.xml b/spring-data-jdbc-distribution/pom.xml index b3c39e64c3..11a76143d4 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-M1 ../pom.xml diff --git a/spring-data-jdbc/pom.xml b/spring-data-jdbc/pom.xml index e61fd64020..4628783551 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-M1 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-M1 diff --git a/spring-data-r2dbc/pom.xml b/spring-data-r2dbc/pom.xml index 3ee76fd3c1..387ad3b30e 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-M1 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-M1 diff --git a/spring-data-relational/pom.xml b/spring-data-relational/pom.xml index 8fd6d7a6f0..d3ecbf9fc6 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-M1 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-M1 From 988ad420df6ccad78e174f7d70008ed6954a37fd Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 24 Jan 2025 10:53:10 +0100 Subject: [PATCH 08/21] Prepare next development iteration. See #1943 --- pom.xml | 2 +- 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, 8 insertions(+), 8 deletions(-) diff --git a/pom.xml b/pom.xml index 54fb91f67b..8c01884e43 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-relational-parent - 4.0.0-M1 + 4.0.0-SNAPSHOT pom Spring Data Relational Parent diff --git a/spring-data-jdbc-distribution/pom.xml b/spring-data-jdbc-distribution/pom.xml index 11a76143d4..b3c39e64c3 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-M1 + 4.0.0-SNAPSHOT ../pom.xml diff --git a/spring-data-jdbc/pom.xml b/spring-data-jdbc/pom.xml index 4628783551..e61fd64020 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-M1 + 4.0.0-SNAPSHOT Spring Data JDBC Spring Data module for JDBC repositories. @@ -15,7 +15,7 @@ org.springframework.data spring-data-relational-parent - 4.0.0-M1 + 4.0.0-SNAPSHOT diff --git a/spring-data-r2dbc/pom.xml b/spring-data-r2dbc/pom.xml index 387ad3b30e..3ee76fd3c1 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-M1 + 4.0.0-SNAPSHOT Spring Data R2DBC Spring Data module for R2DBC @@ -15,7 +15,7 @@ org.springframework.data spring-data-relational-parent - 4.0.0-M1 + 4.0.0-SNAPSHOT diff --git a/spring-data-relational/pom.xml b/spring-data-relational/pom.xml index d3ecbf9fc6..8fd6d7a6f0 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-M1 + 4.0.0-SNAPSHOT Spring Data Relational Spring Data Relational support @@ -14,7 +14,7 @@ org.springframework.data spring-data-relational-parent - 4.0.0-M1 + 4.0.0-SNAPSHOT From fd3353aaa44e8cb72bcb863034abdf87f9535f9c Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 24 Jan 2025 10:53:12 +0100 Subject: [PATCH 09/21] After release cleanups. See #1943 --- pom.xml | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 8c01884e43..0acc4a7020 100644 --- a/pom.xml +++ b/pom.xml @@ -15,12 +15,12 @@ org.springframework.data.build spring-data-parent - 4.0.0-M1 + 4.0.0-SNAPSHOT spring-data-jdbc - 4.0.0-M1 + 4.0.0-SNAPSHOT 4.21.1 reuseReports @@ -310,8 +310,20 @@ - - + + spring-snapshot + https://repo.spring.io/snapshot + + true + + + false + + + + spring-milestone + https://repo.spring.io/milestone + From 3640cbed576e70fe7894ba241ff8189538daabf1 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 10 Feb 2025 14:15:46 +0100 Subject: [PATCH 10/21] Refine generic boundaries. Kotlin reacts to JSpecify with increased nullability requirements. See #1980 --- .../jdbc/core/JdbcAggregateOperationsExtensions.kt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/spring-data-jdbc/src/main/kotlin/org/springframework/data/jdbc/core/JdbcAggregateOperationsExtensions.kt b/spring-data-jdbc/src/main/kotlin/org/springframework/data/jdbc/core/JdbcAggregateOperationsExtensions.kt index b1b7fcd26d..c0a46a20ea 100644 --- a/spring-data-jdbc/src/main/kotlin/org/springframework/data/jdbc/core/JdbcAggregateOperationsExtensions.kt +++ b/spring-data-jdbc/src/main/kotlin/org/springframework/data/jdbc/core/JdbcAggregateOperationsExtensions.kt @@ -20,7 +20,7 @@ import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable import org.springframework.data.domain.Sort import org.springframework.data.relational.core.query.Query -import java.util.Optional +import java.util.* /** * Kotlin extensions for [JdbcAggregateOperations]. @@ -80,7 +80,7 @@ inline fun JdbcAggregateOperations.findAll(sort: Sort): List = /** * Extension for [JdbcAggregateOperations.findAll] with pagination. */ -inline fun JdbcAggregateOperations.findAll(pageable: Pageable): Page = +inline fun JdbcAggregateOperations.findAll(pageable: Pageable): Page = findAll(T::class.java, pageable) /** @@ -98,7 +98,10 @@ inline fun JdbcAggregateOperations.findAll(query: Query): List = /** * Extension for [JdbcAggregateOperations.findAll] with query and pagination. */ -inline fun JdbcAggregateOperations.findAll(query: Query, pageable: Pageable): Page = +inline fun JdbcAggregateOperations.findAll( + query: Query, + pageable: Pageable +): Page = findAll(query, T::class.java, pageable) /** @@ -117,4 +120,4 @@ inline fun JdbcAggregateOperations.deleteAllById(ids: Iterable<*>): * Extension for [JdbcAggregateOperations.deleteAll]. */ inline fun JdbcAggregateOperations.deleteAll(): Unit = - deleteAll(T::class.java) \ No newline at end of file + deleteAll(T::class.java) From 05270bf0a9172b89b1f1e81aea9a5ee49d397ea6 Mon Sep 17 00:00:00 2001 From: mipo256 Date: Wed, 11 Dec 2024 13:23:26 +0300 Subject: [PATCH 11/21] Remove DbActionExecutionException. We now raise the exceptions from `NamedParameterJdbcTemplate` directly. If you used to extract the `cause` of a `DbActionExecutionException` you should now catch that Exception directly. Original pull request #1956 Closes #831 Signed-off-by: mipo256 --- .../jdbc/core/AggregateChangeExecutor.java | 72 +++++++---------- ...JdbcAggregateTemplateIntegrationTests.java | 7 +- ...RepositoryConcurrencyIntegrationTests.java | 5 +- .../JdbcRepositoryIntegrationTests.java | 81 +++++++++++++++++-- .../JdbcRepositoryIntegrationTests-db2.sql | 7 +- .../JdbcRepositoryIntegrationTests-h2.sql | 8 +- .../JdbcRepositoryIntegrationTests-hsql.sql | 8 +- ...JdbcRepositoryIntegrationTests-mariadb.sql | 8 +- .../JdbcRepositoryIntegrationTests-mssql.sql | 9 ++- .../JdbcRepositoryIntegrationTests-mysql.sql | 8 +- .../JdbcRepositoryIntegrationTests-oracle.sql | 9 ++- ...dbcRepositoryIntegrationTests-postgres.sql | 9 ++- .../DbActionExecutionExceptionUnitTests.java | 36 --------- 13 files changed, 170 insertions(+), 97 deletions(-) delete mode 100644 spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/DbActionExecutionExceptionUnitTests.java diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/AggregateChangeExecutor.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/AggregateChangeExecutor.java index 1de697ad09..e5f88f6837 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/AggregateChangeExecutor.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/AggregateChangeExecutor.java @@ -15,22 +15,21 @@ */ package org.springframework.data.jdbc.core; -import org.springframework.dao.OptimisticLockingFailureException; +import java.util.List; + import org.springframework.data.jdbc.core.convert.DataAccessStrategy; import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.relational.core.conversion.AggregateChange; import org.springframework.data.relational.core.conversion.DbAction; -import org.springframework.data.relational.core.conversion.DbActionExecutionException; import org.springframework.data.relational.core.conversion.MutableAggregateChange; -import java.util.List; - /** * Executes an {@link MutableAggregateChange}. * * @author Jens Schauder * @author Myeonghyeon Lee * @author Chirag Tailor + * @author Mikhail Polivakha * @since 2.0 */ class AggregateChangeExecutor { @@ -79,43 +78,34 @@ void executeDelete(AggregateChange aggregateChange) { } private void execute(DbAction action, JdbcAggregateChangeExecutionContext executionContext) { - - try { - if (action instanceof DbAction.InsertRoot insertRoot) { - executionContext.executeInsertRoot(insertRoot); - } else if (action instanceof DbAction.BatchInsertRoot batchInsertRoot) { - executionContext.executeBatchInsertRoot(batchInsertRoot); - } else if (action instanceof DbAction.Insert insert) { - executionContext.executeInsert(insert); - } else if (action instanceof DbAction.BatchInsert batchInsert) { - executionContext.executeBatchInsert(batchInsert); - } else if (action instanceof DbAction.UpdateRoot updateRoot) { - executionContext.executeUpdateRoot(updateRoot); - } else if (action instanceof DbAction.Delete delete) { - executionContext.executeDelete(delete); - } else if (action instanceof DbAction.BatchDelete batchDelete) { - executionContext.executeBatchDelete(batchDelete); - } else if (action instanceof DbAction.DeleteAll deleteAll) { - executionContext.executeDeleteAll(deleteAll); - } else if (action instanceof DbAction.DeleteRoot deleteRoot) { - executionContext.executeDeleteRoot(deleteRoot); - } else if (action instanceof DbAction.BatchDeleteRoot batchDeleteRoot) { - executionContext.executeBatchDeleteRoot(batchDeleteRoot); - } else if (action instanceof DbAction.DeleteAllRoot deleteAllRoot) { - executionContext.executeDeleteAllRoot(deleteAllRoot); - } else if (action instanceof DbAction.AcquireLockRoot acquireLockRoot) { - executionContext.executeAcquireLock(acquireLockRoot); - } else if (action instanceof DbAction.AcquireLockAllRoot acquireLockAllRoot) { - executionContext.executeAcquireLockAllRoot(acquireLockAllRoot); - } else { - throw new RuntimeException("unexpected action"); - } - } catch (Exception e) { - - if (e instanceof OptimisticLockingFailureException) { - throw e; - } - throw new DbActionExecutionException(action, e); + if (action instanceof DbAction.InsertRoot insertRoot) { + executionContext.executeInsertRoot(insertRoot); + } else if (action instanceof DbAction.BatchInsertRoot batchInsertRoot) { + executionContext.executeBatchInsertRoot(batchInsertRoot); + } else if (action instanceof DbAction.Insert insert) { + executionContext.executeInsert(insert); + } else if (action instanceof DbAction.BatchInsert batchInsert) { + executionContext.executeBatchInsert(batchInsert); + } else if (action instanceof DbAction.UpdateRoot updateRoot) { + executionContext.executeUpdateRoot(updateRoot); + } else if (action instanceof DbAction.Delete delete) { + executionContext.executeDelete(delete); + } else if (action instanceof DbAction.BatchDelete batchDelete) { + executionContext.executeBatchDelete(batchDelete); + } else if (action instanceof DbAction.DeleteAll deleteAll) { + executionContext.executeDeleteAll(deleteAll); + } else if (action instanceof DbAction.DeleteRoot deleteRoot) { + executionContext.executeDeleteRoot(deleteRoot); + } else if (action instanceof DbAction.BatchDeleteRoot batchDeleteRoot) { + executionContext.executeBatchDeleteRoot(batchDeleteRoot); + } else if (action instanceof DbAction.DeleteAllRoot deleteAllRoot) { + executionContext.executeDeleteAllRoot(deleteAllRoot); + } else if (action instanceof DbAction.AcquireLockRoot acquireLockRoot) { + executionContext.executeAcquireLock(acquireLockRoot); + } else if (action instanceof DbAction.AcquireLockAllRoot acquireLockAllRoot) { + executionContext.executeAcquireLockAllRoot(acquireLockAllRoot); + } else { + throw new RuntimeException("unexpected action"); } } } 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 4f047f8406..467fae1fde 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 @@ -563,9 +563,8 @@ void updateFailedRootDoesNotExist() { LegoSet entity = new LegoSet(); entity.id = 100L; // does not exist in the database - assertThatExceptionOfType(DbActionExecutionException.class) // - .isThrownBy(() -> template.save(entity)) // - .withCauseInstanceOf(IncorrectUpdateSemanticsDataAccessException.class); + assertThatExceptionOfType(IncorrectUpdateSemanticsDataAccessException.class) // + .isThrownBy(() -> template.save(entity)); } @Test // DATAJDBC-112 @@ -1165,7 +1164,7 @@ void saveAndUpdateAggregateWithIdAndNullVersion() { aggregate.setVersion(null); aggregate.setId(23L); - assertThatThrownBy(() -> template.save(aggregate)).isInstanceOf(DbActionExecutionException.class); + assertThatThrownBy(() -> template.save(aggregate)).isInstanceOf(IncorrectUpdateSemanticsDataAccessException.class); } @Test // DATAJDBC-462 diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryConcurrencyIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryConcurrencyIntegrationTests.java index 470c7fc88d..4d4d04d000 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryConcurrencyIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryConcurrencyIntegrationTests.java @@ -53,6 +53,7 @@ * * @author Myeonghyeon Lee * @author Jens Schauder + * @author Mikhail Polivakha */ @ExtendWith(SpringExtension.class) public class JdbcRepositoryConcurrencyIntegrationTests { @@ -159,7 +160,7 @@ public void concurrentUpdateAndDelete() throws Exception { } catch (Exception ex) { // When the delete execution is complete, the Update execution throws an // IncorrectUpdateSemanticsDataAccessException. - if (ex.getCause() instanceof IncorrectUpdateSemanticsDataAccessException) { + if (ex instanceof IncorrectUpdateSemanticsDataAccessException) { return null; } throw ex; @@ -193,7 +194,7 @@ public void concurrentUpdateAndDeleteAll() throws Exception { } catch (Exception ex) { // When the delete execution is complete, the Update execution throws an // IncorrectUpdateSemanticsDataAccessException. - if (ex.getCause() instanceof IncorrectUpdateSemanticsDataAccessException) { + if (ex instanceof IncorrectUpdateSemanticsDataAccessException) { return null; } throw ex; 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 3f54716cec..bd83c5d0d4 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 @@ -15,10 +15,12 @@ */ package org.springframework.data.jdbc.repository; -import static java.util.Arrays.*; -import static java.util.Collections.*; -import static org.assertj.core.api.Assertions.*; -import static org.assertj.core.api.SoftAssertions.*; +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.SoftAssertions.assertSoftly; import java.io.IOException; import java.sql.ResultSet; @@ -37,6 +39,7 @@ import java.util.function.Consumer; 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.junit.jupiter.params.ParameterizedTest; @@ -49,9 +52,21 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.core.io.ClassPathResource; +import org.springframework.dao.DuplicateKeyException; import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.data.annotation.Id; -import org.springframework.data.domain.*; +import org.springframework.data.annotation.Transient; +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.Persistable; +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.jdbc.core.mapping.AggregateReference; import org.springframework.data.jdbc.repository.query.Modifying; import org.springframework.data.jdbc.repository.query.Query; @@ -64,8 +79,8 @@ import org.springframework.data.jdbc.testing.TestDatabaseFeatures; import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.MappedCollection; -import org.springframework.data.relational.core.mapping.Table; import org.springframework.data.relational.core.mapping.Sequence; +import org.springframework.data.relational.core.mapping.Table; import org.springframework.data.relational.core.mapping.event.AbstractRelationalEvent; import org.springframework.data.relational.core.mapping.event.AfterConvertEvent; import org.springframework.data.relational.core.sql.LockMode; @@ -104,6 +119,8 @@ public class JdbcRepositoryIntegrationTests { @Autowired NamedParameterJdbcTemplate template; @Autowired DummyEntityRepository repository; + + @Autowired ProvidedIdEntityRepository providedIdEntityRepository; @Autowired MyEventListener eventListener; @Autowired RootRepository rootRepository; @Autowired WithDelimitedColumnRepository withDelimitedColumnRepository; @@ -208,6 +225,18 @@ public void findAllFindsAllSpecifiedEntities() { .containsExactlyInAnyOrder(entity.getIdProp(), other.getIdProp()); } + @Test // DATAJDBC-611 + public void testDuplicateKeyExceptionIsThrownInCaseOfUniqueKeyViolation() { + + // given. + ProvidedIdEntity first = ProvidedIdEntity.newInstance(1L, "name"); + ProvidedIdEntity second = ProvidedIdEntity.newInstance(1L, "other"); + + // when/then + Assertions.assertThatCode(() -> providedIdEntityRepository.save(first)).doesNotThrowAnyException(); + Assertions.assertThatThrownBy(() -> providedIdEntityRepository.save(second)).isInstanceOf(DuplicateKeyException.class); + } + @Test // DATAJDBC-97 public void countsEntities() { @@ -1436,6 +1465,10 @@ interface DummyProjectExample { String getName(); } + interface ProvidedIdEntityRepository extends CrudRepository { + + } + interface DummyEntityRepository extends CrudRepository, QueryByExampleExecutor { @Lock(LockMode.PESSIMISTIC_WRITE) @@ -1543,6 +1576,11 @@ DummyEntityRepository dummyEntityRepository() { return factory.getRepository(DummyEntityRepository.class); } + @Bean + ProvidedIdEntityRepository providedIdEntityRepository() { + return factory.getRepository(ProvidedIdEntityRepository.class); + } + @Bean RootRepository rootRepository() { return factory.getRepository(RootRepository.class); @@ -1886,6 +1924,37 @@ public String getName() { } } + static class ProvidedIdEntity implements Persistable { + + @Id + private final Long id; + + private String name; + + @Transient + private boolean isNew; + + private ProvidedIdEntity(Long id, String name, boolean isNew) { + this.id = id; + this.name = name; + this.isNew = isNew; + } + + private static ProvidedIdEntity newInstance(Long id, String name) { + return new ProvidedIdEntity(id, name, true); + } + + @Override + public Long getId() { + return id; + } + + @Override + public boolean isNew() { + return isNew; + } + } + static class DummyEntity { String name; diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql index 1c00e779a6..9b0f80ea2e 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql @@ -5,6 +5,7 @@ DROP TABLE LEAF; DROP TABLE WITH_DELIMITED_COLUMN; DROP TABLE ENTITY_WITH_SEQUENCE; DROP SEQUENCE ENTITY_SEQUENCE; +DROP TABLE PROVIDED_ID_ENTITY; CREATE TABLE dummy_entity ( @@ -55,4 +56,8 @@ CREATE TABLE ENTITY_WITH_SEQUENCE NAME VARCHAR(100) ); -CREATE SEQUENCE ENTITY_SEQUENCE START WITH 1 INCREMENT BY 1 NO MAXVALUE; \ No newline at end of file +CREATE TABLE PROVIDED_ID_ENTITY +( + ID BIGINT PRIMARY KEY, + NAME VARCHAR(30) +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-h2.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-h2.sql index 6f9087b69d..208ed0649e 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-h2.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-h2.sql @@ -47,4 +47,10 @@ CREATE TABLE ENTITY_WITH_SEQUENCE NAME VARCHAR(100) ); -CREATE SEQUENCE ENTITY_SEQUENCE START WITH 1 INCREMENT BY 1 NO MAXVALUE; \ No newline at end of file +CREATE SEQUENCE ENTITY_SEQUENCE START WITH 1 INCREMENT BY 1 NO MAXVALUE; + +CREATE TABLE PROVIDED_ID_ENTITY +( + ID BIGINT PRIMARY KEY, + NAME VARCHAR(30) +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql index 6f9087b69d..208ed0649e 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql @@ -47,4 +47,10 @@ CREATE TABLE ENTITY_WITH_SEQUENCE NAME VARCHAR(100) ); -CREATE SEQUENCE ENTITY_SEQUENCE START WITH 1 INCREMENT BY 1 NO MAXVALUE; \ No newline at end of file +CREATE SEQUENCE ENTITY_SEQUENCE START WITH 1 INCREMENT BY 1 NO MAXVALUE; + +CREATE TABLE PROVIDED_ID_ENTITY +( + ID BIGINT PRIMARY KEY, + NAME VARCHAR(30) +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql index 23d3ad7221..8f68cb5891 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql @@ -47,4 +47,10 @@ CREATE TABLE ENTITY_WITH_SEQUENCE NAME VARCHAR(100) ); -CREATE SEQUENCE `ENTITY_SEQUENCE` START WITH 1 INCREMENT BY 1 NO MAXVALUE; \ No newline at end of file +CREATE SEQUENCE `ENTITY_SEQUENCE` START WITH 1 INCREMENT BY 1 NO MAXVALUE; + +CREATE TABLE PROVIDED_ID_ENTITY +( + ID BIGINT PRIMARY KEY, + NAME VARCHAR(30) +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql index 69f191f65d..8922308b28 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql @@ -5,6 +5,7 @@ DROP TABLE IF EXISTS LEAF; DROP TABLE IF EXISTS WITH_DELIMITED_COLUMN; DROP TABLE IF EXISTS ENTITY_WITH_SEQUENCE; DROP SEQUENCE IF EXISTS ENTITY_SEQUENCE; +DROP TABLE IF EXISTS PROVIDED_ID_ENTITY; CREATE TABLE dummy_entity ( @@ -55,4 +56,10 @@ CREATE TABLE ENTITY_WITH_SEQUENCE NAME VARCHAR(100) ); -CREATE SEQUENCE ENTITY_SEQUENCE START WITH 1 INCREMENT BY 1 NO MAXVALUE; \ No newline at end of file +CREATE SEQUENCE ENTITY_SEQUENCE START WITH 1 INCREMENT BY 1 NO MAXVALUE; + +CREATE TABLE PROVIDED_ID_ENTITY +( + ID BIGINT PRIMARY KEY, + NAME VARCHAR(30) +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql index 0d3e16587f..a1a6054e4f 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql @@ -42,4 +42,10 @@ CREATE TABLE WITH_DELIMITED_COLUMN ID BIGINT AUTO_INCREMENT PRIMARY KEY, `ORG.XTUNIT.IDENTIFIER` VARCHAR(100), STYPE VARCHAR(100) -); \ No newline at end of file +); + +CREATE TABLE PROVIDED_ID_ENTITY +( + ID BIGINT PRIMARY KEY, + NAME VARCHAR(30) +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql index 428ff48f3f..797976c00b 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql @@ -5,6 +5,7 @@ DROP TABLE LEAF CASCADE CONSTRAINTS PURGE; DROP TABLE WITH_DELIMITED_COLUMN CASCADE CONSTRAINTS PURGE; DROP TABLE ENTITY_WITH_SEQUENCE CASCADE CONSTRAINTS PURGE; DROP SEQUENCE ENTITY_SEQUENCE; +DROP TABLE PROVIDED_ID_ENTITY CASCADE CONSTRAINTS PURGE; CREATE TABLE DUMMY_ENTITY ( @@ -55,4 +56,10 @@ CREATE TABLE ENTITY_WITH_SEQUENCE NAME VARCHAR(100) ); -CREATE SEQUENCE ENTITY_SEQUENCE START WITH 1 INCREMENT BY 1; \ No newline at end of file +CREATE SEQUENCE ENTITY_SEQUENCE START WITH 1 INCREMENT BY 1; + +CREATE TABLE PROVIDED_ID_ENTITY +( + ID BIGINT PRIMARY KEY, + NAME VARCHAR(30) +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql index 42e69437a7..bd1497a2e2 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql @@ -5,6 +5,7 @@ DROP TABLE LEAF; DROP TABLE WITH_DELIMITED_COLUMN; DROP TABLE ENTITY_WITH_SEQUENCE; DROP SEQUENCE ENTITY_SEQUENCE; +DROP TABLE PROVIDED_ID_ENTITY; CREATE TABLE dummy_entity ( @@ -55,4 +56,10 @@ CREATE TABLE ENTITY_WITH_SEQUENCE NAME VARCHAR(100) ); -CREATE SEQUENCE "ENTITY_SEQUENCE" START WITH 1 INCREMENT BY 1 NO MAXVALUE; \ No newline at end of file +CREATE SEQUENCE "ENTITY_SEQUENCE" START WITH 1 INCREMENT BY 1 NO MAXVALUE; + +CREATE TABLE PROVIDED_ID_ENTITY +( + ID BIGINT PRIMARY KEY, + NAME VARCHAR(30) +); diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/DbActionExecutionExceptionUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/DbActionExecutionExceptionUnitTests.java deleted file mode 100644 index c81c7dd20a..0000000000 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/DbActionExecutionExceptionUnitTests.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2018-2025 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.conversion; - -import org.junit.jupiter.api.Test; - -import static org.mockito.Mockito.*; - -/** - * Unit test for {@link DbActionExecutionException}. - * - * @author Jens Schauder - */ -public class DbActionExecutionExceptionUnitTests { - - @Test // DATAJDBC-162 - public void constructorWorksWithNullPropertyPath() { - - DbAction action = mock(DbAction.class); - new DbActionExecutionException(action, null); - } - -} From 7340cdf1df7e70843684869536dc07f70b5c10f0 Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Mon, 17 Mar 2025 14:42:28 +0100 Subject: [PATCH 12/21] Polishing. Formatting. Fixing SQL scripts. Original pull request #1956 See #831 --- .../jdbc/core/AggregateChangeExecutor.java | 1 + ...JdbcAggregateTemplateIntegrationTests.java | 9 +- ...RepositoryConcurrencyIntegrationTests.java | 20 +- .../JdbcRepositoryIntegrationTests.java | 200 ++++++++---------- .../JdbcRepositoryIntegrationTests-db2.sql | 8 +- .../JdbcRepositoryIntegrationTests-h2.sql | 6 +- .../JdbcRepositoryIntegrationTests-hsql.sql | 6 +- ...JdbcRepositoryIntegrationTests-mariadb.sql | 6 +- .../JdbcRepositoryIntegrationTests-mssql.sql | 6 +- .../JdbcRepositoryIntegrationTests-mysql.sql | 6 +- .../JdbcRepositoryIntegrationTests-oracle.sql | 8 +- ...dbcRepositoryIntegrationTests-postgres.sql | 6 +- 12 files changed, 131 insertions(+), 151 deletions(-) diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/AggregateChangeExecutor.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/AggregateChangeExecutor.java index e5f88f6837..45b139b7ab 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/AggregateChangeExecutor.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/AggregateChangeExecutor.java @@ -78,6 +78,7 @@ void executeDelete(AggregateChange aggregateChange) { } private void execute(DbAction action, JdbcAggregateChangeExecutionContext executionContext) { + if (action instanceof DbAction.InsertRoot insertRoot) { executionContext.executeInsertRoot(insertRoot); } else if (action instanceof DbAction.BatchInsertRoot batchInsertRoot) { 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 467fae1fde..00ce1278c7 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 @@ -53,7 +53,6 @@ import org.springframework.data.jdbc.testing.TestConfiguration; import org.springframework.data.jdbc.testing.TestDatabaseFeatures; import org.springframework.data.mapping.context.InvalidPersistentPropertyPath; -import org.springframework.data.relational.core.conversion.DbActionExecutionException; import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.Embedded; import org.springframework.data.relational.core.mapping.InsertOnlyProperty; @@ -317,7 +316,7 @@ void saveAndLoadManeEntitiesWithReferenceEntityLikeStream() { Stream streamable = template.streamAll(LegoSet.class); - assertThat(streamable) + assertThat(streamable) // .extracting("id", "manual.id", "manual.content") // .containsExactly(tuple(legoSet.id, legoSet.manual.id, legoSet.manual.content)); } @@ -730,7 +729,7 @@ void saveAndLoadAnEntityWithArray() { void saveAndLoadAnEntityWithEmptyArray() { ArrayOwner arrayOwner = new ArrayOwner(); - arrayOwner.digits = new String[] { }; + arrayOwner.digits = new String[] {}; ArrayOwner saved = template.save(arrayOwner); @@ -854,7 +853,7 @@ void saveAndLoadAnEntityWithSet() { assertThat(reloaded.digits).isEqualTo(new HashSet<>(asList("one", "two", "three"))); } - @Test //GH-1737 + @Test // GH-1737 @EnabledOnFeature(SUPPORTS_ARRAYS) void saveAndLoadEmbeddedArray() { @@ -869,7 +868,7 @@ void saveAndLoadEmbeddedArray() { assertThat(reloaded.embeddedStringList.digits).containsExactly("one", "two", "three"); } - @Test //GH-1737 + @Test // GH-1737 @EnabledOnFeature(SUPPORTS_ARRAYS) void saveAndLoadEmptyEmbeddedArray() { diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryConcurrencyIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryConcurrencyIntegrationTests.java index 4d4d04d000..a1ed2ab243 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryConcurrencyIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryConcurrencyIntegrationTests.java @@ -73,12 +73,9 @@ DummyEntityRepository dummyEntityRepository(JdbcRepositoryFactory factory) { } } - @Autowired - NamedParameterJdbcTemplate template; - @Autowired - DummyEntityRepository repository; - @Autowired - PlatformTransactionManager transactionManager; + @Autowired NamedParameterJdbcTemplate template; + @Autowired DummyEntityRepository repository; + @Autowired PlatformTransactionManager transactionManager; List concurrencyEntities; DummyEntity entity; @@ -216,7 +213,7 @@ public void concurrentUpdateAndDeleteAll() throws Exception { } private void executeInParallel(CountDownLatch startLatch, CountDownLatch doneLatch, - UnaryOperator deleteAction, DummyEntity entity) { + UnaryOperator deleteAction, DummyEntity entity) { // delete new Thread(() -> { try { @@ -253,13 +250,11 @@ private static DummyEntity createDummyEntity() { return new DummyEntity(null, "Entity Name", new ArrayList<>()); } - interface DummyEntityRepository extends CrudRepository { - } + interface DummyEntityRepository extends CrudRepository {} static class DummyEntity { - @Id - private Long id; + @Id private Long id; String name; final List content; @@ -292,8 +287,7 @@ public DummyEntity withContent(List content) { static class Element { - @Id - private Long id; + @Id private Long id; final Long content; public Element(Long id, Long content) { 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 bd83c5d0d4..50fe9d03a9 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 @@ -15,12 +15,10 @@ */ package org.springframework.data.jdbc.repository; -import static java.util.Arrays.asList; -import static java.util.Collections.emptyList; -import static java.util.Collections.singletonList; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static java.util.Arrays.*; +import static java.util.Collections.*; +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.SoftAssertions.*; import java.io.IOException; import java.sql.ResultSet; @@ -39,7 +37,6 @@ import java.util.function.Consumer; 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.junit.jupiter.params.ParameterizedTest; @@ -56,17 +53,7 @@ import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Transient; -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.Persistable; -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; @@ -126,6 +113,37 @@ public class JdbcRepositoryIntegrationTests { @Autowired WithDelimitedColumnRepository withDelimitedColumnRepository; @Autowired EntityWithSequenceRepository entityWithSequenceRepository; + public static Stream findAllByExamplePageableSource() { + + return Stream.of( // + Arguments.of(PageRequest.of(0, 3), 3, 34, Arrays.asList("3", "4", "100")), // + Arguments.of(PageRequest.of(1, 10), 10, 10, Arrays.asList("9", "20", "30")), // + Arguments.of(PageRequest.of(2, 10), 10, 10, Arrays.asList("1", "2", "3")), // + Arguments.of(PageRequest.of(33, 3), 1, 34, Collections.emptyList()), // + Arguments.of(PageRequest.of(36, 3), 0, 34, Collections.emptyList()), // + Arguments.of(PageRequest.of(0, 10000), 100, 1, Collections.emptyList()), // + Arguments.of(PageRequest.of(100, 10000), 0, 1, Collections.emptyList()) // + ); + } + + private static DummyEntity createEntity() { + return createEntity("Entity Name"); + } + + private static DummyEntity createEntity(String entityName) { + return createEntity(entityName, it -> {}); + } + + private static DummyEntity createEntity(String entityName, Consumer customizer) { + + DummyEntity entity = new DummyEntity(); + entity.setName(entityName); + + customizer.accept(entity); + + return entity; + } + @BeforeEach public void before() { @@ -225,16 +243,14 @@ public void findAllFindsAllSpecifiedEntities() { .containsExactlyInAnyOrder(entity.getIdProp(), other.getIdProp()); } - @Test // DATAJDBC-611 - public void testDuplicateKeyExceptionIsThrownInCaseOfUniqueKeyViolation() { + @Test // GH-831 + public void duplicateKeyExceptionIsThrownInCaseOfUniqueKeyViolation() { - // given. ProvidedIdEntity first = ProvidedIdEntity.newInstance(1L, "name"); ProvidedIdEntity second = ProvidedIdEntity.newInstance(1L, "other"); - // when/then - Assertions.assertThatCode(() -> providedIdEntityRepository.save(first)).doesNotThrowAnyException(); - Assertions.assertThatThrownBy(() -> providedIdEntityRepository.save(second)).isInstanceOf(DuplicateKeyException.class); + assertThatCode(() -> providedIdEntityRepository.save(first)).doesNotThrowAnyException(); + assertThatThrownBy(() -> providedIdEntityRepository.save(second)).isInstanceOf(DuplicateKeyException.class); } @Test // DATAJDBC-97 @@ -967,18 +983,6 @@ void findAllByExamplePageable(Pageable pageRequest, int size, int totalPages, Li } } - public static Stream findAllByExamplePageableSource() { - return Stream.of( // - Arguments.of(PageRequest.of(0, 3), 3, 34, Arrays.asList("3", "4", "100")), // - Arguments.of(PageRequest.of(1, 10), 10, 10, Arrays.asList("9", "20", "30")), // - Arguments.of(PageRequest.of(2, 10), 10, 10, Arrays.asList("1", "2", "3")), // - Arguments.of(PageRequest.of(33, 3), 1, 34, Collections.emptyList()), // - Arguments.of(PageRequest.of(36, 3), 0, 34, Collections.emptyList()), // - Arguments.of(PageRequest.of(0, 10000), 100, 1, Collections.emptyList()), // - Arguments.of(PageRequest.of(100, 10000), 0, 1, Collections.emptyList()) // - ); - } - @Test // GH-1192 void existsByExampleShouldGetOne() { @@ -1461,6 +1465,10 @@ private Instant createDummyBeforeAndAfterNow() { return now; } + enum Direction { + LEFT, CENTER, RIGHT + } + interface DummyProjectExample { String getName(); } @@ -1565,6 +1573,10 @@ interface WithDelimitedColumnRepository extends CrudRepository {} + interface DummyProjection { + String getName(); + } + @Configuration @Import(TestConfiguration.class) static class Config { @@ -1709,22 +1721,22 @@ public Long getId() { return this.id; } - public String getIdentifier() { - return this.identifier; - } - - public String getType() { - return this.type; - } - public void setId(Long id) { this.id = id; } + public String getIdentifier() { + return this.identifier; + } + public void setIdentifier(String identifier) { this.identifier = identifier; } + public String getType() { + return this.type; + } + public void setType(String type) { this.type = type; } @@ -1866,6 +1878,11 @@ public String getExtensionId() { return "myext"; } + @Override + public Object getRootObject() { + return new ExtensionRoot(); + } + public static class ExtensionRoot { // just public for testing purposes public static Long ID = 1L; @@ -1874,29 +1891,6 @@ public Long getId() { return ID; } } - - @Override - public Object getRootObject() { - return new ExtensionRoot(); - } - } - - private static DummyEntity createEntity() { - return createEntity("Entity Name"); - } - - private static DummyEntity createEntity(String entityName) { - return createEntity(entityName, it -> {}); - } - - private static DummyEntity createEntity(String entityName, Consumer customizer) { - - DummyEntity entity = new DummyEntity(); - entity.setName(entityName); - - customizer.accept(entity); - - return entity; } static class EntityWithSequence { @@ -1926,13 +1920,11 @@ public String getName() { static class ProvidedIdEntity implements Persistable { - @Id - private final Long id; + @Id private final Long id; private String name; - @Transient - private boolean isNew; + @Transient private boolean isNew; private ProvidedIdEntity(Long id, String name, boolean isNew) { this.id = id; @@ -1960,11 +1952,11 @@ static class DummyEntity { String name; Instant pointInTime; OffsetDateTime offsetDateTime; - @Id private Long idProp; boolean flag; AggregateReference ref; Direction direction; byte[] bytes = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 }; + @Id private Long idProp; public DummyEntity(String name) { this.name = name; @@ -1976,54 +1968,54 @@ public String getName() { return this.name; } - public Instant getPointInTime() { - return this.pointInTime; - } - - public OffsetDateTime getOffsetDateTime() { - return this.offsetDateTime; - } - - public Long getIdProp() { - return this.idProp; - } - - public boolean isFlag() { - return this.flag; - } - - public AggregateReference getRef() { - return this.ref; - } - - public Direction getDirection() { - return this.direction; - } - public void setName(String name) { this.name = name; } + public Instant getPointInTime() { + return this.pointInTime; + } + public void setPointInTime(Instant pointInTime) { this.pointInTime = pointInTime; } + public OffsetDateTime getOffsetDateTime() { + return this.offsetDateTime; + } + public void setOffsetDateTime(OffsetDateTime offsetDateTime) { this.offsetDateTime = offsetDateTime; } + public Long getIdProp() { + return this.idProp; + } + public void setIdProp(Long idProp) { this.idProp = idProp; } + public boolean isFlag() { + return this.flag; + } + public void setFlag(boolean flag) { this.flag = flag; } + public AggregateReference getRef() { + return this.ref; + } + public void setRef(AggregateReference ref) { this.ref = ref; } + public Direction getDirection() { + return this.direction; + } + public void setDirection(Direction direction) { this.direction = direction; } @@ -2045,24 +2037,20 @@ public int hashCode() { return Objects.hash(name, pointInTime, offsetDateTime, idProp, flag, ref, direction); } - public void setBytes(byte[] bytes) { - this.bytes = bytes; - } - public byte[] getBytes() { return bytes; } + public void setBytes(byte[] bytes) { + this.bytes = bytes; + } + @Override public String toString() { return "DummyEntity{" + "name='" + name + '\'' + ", idProp=" + idProp + '}'; } } - enum Direction { - LEFT, CENTER, RIGHT - } - static class DummyDto { @Id Long idProp; String name; @@ -2099,10 +2087,6 @@ public AggregateReference getRef() { } } - interface DummyProjection { - String getName(); - } - static final class DtoProjection { private final String name; diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql index 9b0f80ea2e..38269a87db 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql @@ -52,12 +52,14 @@ CREATE TABLE WITH_DELIMITED_COLUMN CREATE TABLE ENTITY_WITH_SEQUENCE ( - ID BIGINT, + ID BIGINT, NAME VARCHAR(100) ); +CREATE SEQUENCE ENTITY_SEQUENCE START WITH 1 INCREMENT BY 1 NO MAXVALUE; + CREATE TABLE PROVIDED_ID_ENTITY ( - ID BIGINT PRIMARY KEY, - NAME VARCHAR(30) + ID BIGINT NOT NULL PRIMARY KEY, + NAME VARCHAR(30) ); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-h2.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-h2.sql index 208ed0649e..c24060be24 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-h2.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-h2.sql @@ -43,7 +43,7 @@ CREATE TABLE WITH_DELIMITED_COLUMN CREATE TABLE ENTITY_WITH_SEQUENCE ( - ID BIGINT, + ID BIGINT, NAME VARCHAR(100) ); @@ -51,6 +51,6 @@ CREATE SEQUENCE ENTITY_SEQUENCE START WITH 1 INCREMENT BY 1 NO MAXVALUE; CREATE TABLE PROVIDED_ID_ENTITY ( - ID BIGINT PRIMARY KEY, - NAME VARCHAR(30) + ID BIGINT PRIMARY KEY, + NAME VARCHAR(30) ); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql index 208ed0649e..c24060be24 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql @@ -43,7 +43,7 @@ CREATE TABLE WITH_DELIMITED_COLUMN CREATE TABLE ENTITY_WITH_SEQUENCE ( - ID BIGINT, + ID BIGINT, NAME VARCHAR(100) ); @@ -51,6 +51,6 @@ CREATE SEQUENCE ENTITY_SEQUENCE START WITH 1 INCREMENT BY 1 NO MAXVALUE; CREATE TABLE PROVIDED_ID_ENTITY ( - ID BIGINT PRIMARY KEY, - NAME VARCHAR(30) + ID BIGINT PRIMARY KEY, + NAME VARCHAR(30) ); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql index 8f68cb5891..6291f2b934 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql @@ -43,7 +43,7 @@ CREATE TABLE WITH_DELIMITED_COLUMN CREATE TABLE ENTITY_WITH_SEQUENCE ( - ID BIGINT, + ID BIGINT, NAME VARCHAR(100) ); @@ -51,6 +51,6 @@ CREATE SEQUENCE `ENTITY_SEQUENCE` START WITH 1 INCREMENT BY 1 NO MAXVALUE; CREATE TABLE PROVIDED_ID_ENTITY ( - ID BIGINT PRIMARY KEY, - NAME VARCHAR(30) + ID BIGINT PRIMARY KEY, + NAME VARCHAR(30) ); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql index 8922308b28..6c70d68a58 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql @@ -52,7 +52,7 @@ CREATE TABLE WITH_DELIMITED_COLUMN CREATE TABLE ENTITY_WITH_SEQUENCE ( - ID BIGINT, + ID BIGINT, NAME VARCHAR(100) ); @@ -60,6 +60,6 @@ CREATE SEQUENCE ENTITY_SEQUENCE START WITH 1 INCREMENT BY 1 NO MAXVALUE; CREATE TABLE PROVIDED_ID_ENTITY ( - ID BIGINT PRIMARY KEY, - NAME VARCHAR(30) + ID BIGINT PRIMARY KEY, + NAME VARCHAR(30) ); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql index a1a6054e4f..035e52d4b5 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql @@ -1,5 +1,5 @@ SET -SQL_MODE='ALLOW_INVALID_DATES'; + SQL_MODE = 'ALLOW_INVALID_DATES'; CREATE TABLE DUMMY_ENTITY ( @@ -46,6 +46,6 @@ CREATE TABLE WITH_DELIMITED_COLUMN CREATE TABLE PROVIDED_ID_ENTITY ( - ID BIGINT PRIMARY KEY, - NAME VARCHAR(30) + ID BIGINT PRIMARY KEY, + NAME VARCHAR(30) ); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql index 797976c00b..1f122d15b9 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql @@ -52,14 +52,14 @@ CREATE TABLE WITH_DELIMITED_COLUMN CREATE TABLE ENTITY_WITH_SEQUENCE ( - ID NUMBER, - NAME VARCHAR(100) + ID NUMBER, + NAME VARCHAR2(100) ); CREATE SEQUENCE ENTITY_SEQUENCE START WITH 1 INCREMENT BY 1; CREATE TABLE PROVIDED_ID_ENTITY ( - ID BIGINT PRIMARY KEY, - NAME VARCHAR(30) + ID NUMBER PRIMARY KEY, + NAME VARCHAR2(30) ); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql index bd1497a2e2..05aea26e12 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql @@ -52,7 +52,7 @@ CREATE TABLE "WITH_DELIMITED_COLUMN" CREATE TABLE ENTITY_WITH_SEQUENCE ( - ID BIGINT, + ID BIGINT, NAME VARCHAR(100) ); @@ -60,6 +60,6 @@ CREATE SEQUENCE "ENTITY_SEQUENCE" START WITH 1 INCREMENT BY 1 NO MAXVALUE; CREATE TABLE PROVIDED_ID_ENTITY ( - ID BIGINT PRIMARY KEY, - NAME VARCHAR(30) + ID BIGINT PRIMARY KEY, + NAME VARCHAR(30) ); From 3b124ca00c17dfad6cbdfe463fc277cdf1357f36 Mon Sep 17 00:00:00 2001 From: mipo256 Date: Tue, 1 Apr 2025 08:48:03 +0300 Subject: [PATCH 13/21] Fail on annotated query methods with `@Lock`. Query methods that are not derived methods now cause an exception, when they are annotated with `@Lock`, since this combination is not supported. Before they just logged a warning. Comment edited by Jens Schauder Signed-off-by: mipo256 --- .../data/jdbc/repository/query/StringBasedJdbcQuery.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java index 2513bb4b1a..3219917d6c 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java @@ -15,7 +15,7 @@ */ package org.springframework.data.jdbc.repository.query; -import static org.springframework.data.jdbc.repository.query.JdbcQueryExecution.*; +import static org.springframework.data.jdbc.repository.query.JdbcQueryExecution.ResultProcessingConverter; import java.lang.reflect.Array; import java.lang.reflect.Constructor; @@ -27,8 +27,6 @@ import java.util.function.Function; import java.util.function.Supplier; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import org.springframework.beans.BeanInstantiationException; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.BeanFactory; @@ -75,8 +73,7 @@ public class StringBasedJdbcQuery extends AbstractJdbcQuery { private static final String PARAMETER_NEEDS_TO_BE_NAMED = "For queries with named parameters you need to provide names for method parameters; Use @Param for query method parameters, or use the javac flag -parameters"; - private final static String LOCKING_IS_NOT_SUPPORTED = "Currently, @Lock is supported only on derived queries. In other words, for queries created with @Query, the locking condition specified with @Lock does nothing"; - private static final Log LOG = LogFactory.getLog(StringBasedJdbcQuery.class); + private final static String LOCKING_IS_NOT_SUPPORTED = "Currently, @Lock is supported only on derived queries. In other words, for queries created with @Query, the locking condition specified with @Lock does nothing. Offending method: "; private final JdbcConverter converter; private final RowMapperFactory rowMapperFactory; private final ValueExpressionQueryRewriter.ParsedQuery parsedQuery; @@ -149,7 +146,7 @@ public StringBasedJdbcQuery(String query, JdbcQueryMethod queryMethod, NamedPara this.query = query; if (queryMethod.hasLockMode()) { - LOG.warn(LOCKING_IS_NOT_SUPPORTED); + throw new UnsupportedOperationException(LOCKING_IS_NOT_SUPPORTED + queryMethod); } this.parsedQuery = rewriter.parse(this.query); this.delegate = delegate; From 09b15d90cc91a06e4d004f9ca2b65344c1773e30 Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Thu, 10 Apr 2025 13:22:51 +0200 Subject: [PATCH 14/21] Polishing. Formatting. Added a test. Original pull request #2023 See #2008 --- .../query/StringBasedJdbcQuery.java | 3 +- .../query/StringBasedJdbcQueryUnitTests.java | 31 +++++++++++++++---- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java index 3219917d6c..6f57149d7e 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java @@ -73,7 +73,7 @@ public class StringBasedJdbcQuery extends AbstractJdbcQuery { private static final String PARAMETER_NEEDS_TO_BE_NAMED = "For queries with named parameters you need to provide names for method parameters; Use @Param for query method parameters, or use the javac flag -parameters"; - private final static String LOCKING_IS_NOT_SUPPORTED = "Currently, @Lock is supported only on derived queries. In other words, for queries created with @Query, the locking condition specified with @Lock does nothing. Offending method: "; + private static final String LOCKING_IS_NOT_SUPPORTED = "Currently, @Lock is supported only on derived queries. In other words, for queries created with @Query, the locking condition specified with @Lock does nothing. Offending method: "; private final JdbcConverter converter; private final RowMapperFactory rowMapperFactory; private final ValueExpressionQueryRewriter.ParsedQuery parsedQuery; @@ -457,6 +457,7 @@ public boolean requiresRowMapper() { @Nullable static Constructor findPrimaryConstructor(Class clazz) { + try { return clazz.getDeclaredConstructor(); } catch (NoSuchMethodException ex) { 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 c0d9cd5bf2..9d222e653a 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 @@ -53,6 +53,8 @@ import org.springframework.data.jdbc.support.JdbcUtil; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.relational.core.mapping.RelationalMappingContext; +import org.springframework.data.relational.core.sql.LockMode; +import org.springframework.data.relational.repository.Lock; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries; @@ -252,7 +254,7 @@ void sliceQueryNotSupported() { JdbcQueryMethod queryMethod = createMethod("sliceAll", Pageable.class); assertThatThrownBy( - () -> new StringBasedJdbcQuery(queryMethod, operations, result -> defaultRowMapper, converter, delegate)) + () -> new StringBasedJdbcQuery(queryMethod, operations, result -> defaultRowMapper, converter, delegate)) .isInstanceOf(UnsupportedOperationException.class) .hasMessageContaining("Slice queries are not supported using string-based queries"); } @@ -278,6 +280,16 @@ void limitNotSupported() { .isInstanceOf(UnsupportedOperationException.class); } + @Test // GH-2023 + void lockNotSupported() { + + JdbcQueryMethod queryMethod = createMethod("unsupportedWithLock", Long.class); + + assertThatThrownBy( + () -> new StringBasedJdbcQuery(queryMethod, operations, result -> defaultRowMapper, converter, delegate)) + .isInstanceOf(UnsupportedOperationException.class); + } + @Test // GH-1212 void convertsEnumCollectionParameterIntoStringCollectionParameter() { @@ -355,10 +367,12 @@ void spelCanBeUsedInsideQueries() { List list = new ArrayList<>(); list.add(new MyEvaluationContextProvider()); - QueryMethodValueEvaluationContextAccessor accessor = new QueryMethodValueEvaluationContextAccessor(new StandardEnvironment(), list); + QueryMethodValueEvaluationContextAccessor accessor = new QueryMethodValueEvaluationContextAccessor( + new StandardEnvironment(), list); this.delegate = new ValueExpressionDelegate(accessor, ValueExpressionParser.create()); - StringBasedJdbcQuery sut = new StringBasedJdbcQuery(queryMethod, operations, result -> defaultRowMapper, converter, delegate); + StringBasedJdbcQuery sut = new StringBasedJdbcQuery(queryMethod, operations, result -> defaultRowMapper, converter, + delegate); ArgumentCaptor paramSource = ArgumentCaptor.forClass(SqlParameterSource.class); ArgumentCaptor query = ArgumentCaptor.forClass(String.class); @@ -401,8 +415,8 @@ public SqlParameterSource extractParameterSource() { mock(RelationResolver.class)) : this.converter; - StringBasedJdbcQuery query = new StringBasedJdbcQuery(method.getDeclaredQuery(), method, operations, result -> mock(RowMapper.class), - converter, delegate); + StringBasedJdbcQuery query = new StringBasedJdbcQuery(method.getDeclaredQuery(), method, operations, + result -> mock(RowMapper.class), converter, delegate); query.execute(arguments); @@ -438,7 +452,8 @@ private StringBasedJdbcQuery createQuery(JdbcQueryMethod queryMethod) { } private StringBasedJdbcQuery createQuery(JdbcQueryMethod queryMethod, String preparedReference, Object value) { - return new StringBasedJdbcQuery(queryMethod, operations, new StubRowMapperFactory(preparedReference, value), converter, delegate); + return new StringBasedJdbcQuery(queryMethod, operations, new StubRowMapperFactory(preparedReference, value), + converter, delegate); } interface MyRepository extends Repository { @@ -505,6 +520,10 @@ interface MyRepository extends Repository { @Query("select count(1) from person where (firstname, lastname) in (:tuples)") Object findByListOfTuples(@Param("tuples") List tuples); + + @Lock(value = LockMode.PESSIMISTIC_READ) + @Query("SELECT * FROM person WHERE id = :id") + DummyEntity unsupportedWithLock(Long id); } private static class CustomRowMapper implements RowMapper { From 83650d9bfcc93387510e059681534d3940c894db Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Mon, 10 Jun 2024 11:22:07 +0200 Subject: [PATCH 15/21] 574-composite-id - Prepare branch --- pom.xml | 2 +- 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, 8 insertions(+), 8 deletions(-) diff --git a/pom.xml b/pom.xml index 0acc4a7020..68b15c7442 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-1737-nullable-embedded-with-collection-574-composite-id-SNAPSHOT pom Spring Data Relational Parent diff --git a/spring-data-jdbc-distribution/pom.xml b/spring-data-jdbc-distribution/pom.xml index b3c39e64c3..8d5696607e 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-1737-nullable-embedded-with-collection-574-composite-id-SNAPSHOT ../pom.xml diff --git a/spring-data-jdbc/pom.xml b/spring-data-jdbc/pom.xml index e61fd64020..9f7d6c308e 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-1737-nullable-embedded-with-collection-574-composite-id-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-1737-nullable-embedded-with-collection-574-composite-id-SNAPSHOT diff --git a/spring-data-r2dbc/pom.xml b/spring-data-r2dbc/pom.xml index 3ee76fd3c1..d2b28e8abc 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-1737-nullable-embedded-with-collection-574-composite-id-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-1737-nullable-embedded-with-collection-574-composite-id-SNAPSHOT diff --git a/spring-data-relational/pom.xml b/spring-data-relational/pom.xml index 8fd6d7a6f0..14ea2ad98c 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-1737-nullable-embedded-with-collection-574-composite-id-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-1737-nullable-embedded-with-collection-574-composite-id-SNAPSHOT From c9815c07600064027ed9ee145e04ce694884ce4c Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Mon, 10 Jun 2024 15:18:19 +0200 Subject: [PATCH 16/21] Add support for composite ids. Entities may be annotated with `@Id` and `@Embedded`, resulting in a composite id on the database side. The full embedded entity is considered the id, and therefore the check for determining if an aggregate is considered a new aggregate requiring an insert or an existing one, asking for an update is based on that entity, not its elements. Most use cases will require a custom `BeforeConvertCallback` to set the id for new aggregate. For an entity with `@Embedded` id, the back reference used in tables for referenced entities consists of multiple columns, each named by a concatenation of + `_` + . E.g. the back reference to a `Person` entity, with a composite id with the properties `firstName` and `lastName` will consist of the two columns `PERSON_FIRST_NAME` and `PERSON_LAST_NAME`. This holds for directly referenced entities as well as `List`, `Set` and `Map`. Closes #574 Original pull request #1957 --- .../convert/DefaultDataAccessStrategy.java | 14 +- ...dbcBackReferencePropertyValueProvider.java | 54 --- .../core/convert/JdbcIdentifierBuilder.java | 41 +- .../core/convert/MappingJdbcConverter.java | 68 +-- .../data/jdbc/core/convert/SqlContext.java | 17 +- .../data/jdbc/core/convert/SqlGenerator.java | 296 +++++++----- .../core/convert/SqlParametersFactory.java | 92 ++-- .../query/JdbcDeleteQueryCreator.java | 41 +- .../repository/query/JdbcQueryCreator.java | 84 ++-- .../jdbc/repository/query/SqlContext.java | 44 +- ...AggregateTemplateHsqlIntegrationTests.java | 258 +++++++++++ .../JdbcIdentifierBuilderUnitTests.java | 182 +++++--- ...orContextBasedNamingStrategyUnitTests.java | 4 +- .../SqlGeneratorEmbeddedUnitTests.java | 253 ++++++++++- ...GeneratorFixedNamingStrategyUnitTests.java | 9 +- .../core/convert/SqlGeneratorUnitTests.java | 71 +-- ...ava => SqlParametersFactoryUnitTests.java} | 52 ++- ...mbeddedWithCollectionIntegrationTests.java | 3 +- .../query/PartTreeJdbcQueryUnitTests.java | 13 +- ...egateTemplateHsqlIntegrationTests-hsql.sql | 46 ++ ...AggregateTemplateIntegrationTests-hsql.sql | 428 +++++++++--------- .../core/mapping/AggregatePath.java | 251 ++++++++-- .../BasicRelationalPersistentEntity.java | 1 + .../core/mapping/DefaultAggregatePath.java | 62 ++- .../EmbeddedRelationalPersistentEntity.java | 1 + .../mapping/RelationalMappingContext.java | 8 +- .../mapping/RelationalPersistentEntity.java | 2 + .../relational/core/sql/AnalyticFunction.java | 51 ++- .../data/relational/core/sql/Conditions.java | 2 +- .../relational/core/sql/TupleExpression.java | 54 +++ .../core/sql/render/ExpressionVisitor.java | 9 +- .../core/sql/render/TupleVisitor.java | 72 +++ .../SingleQuerySqlGenerator.java | 17 +- .../core/mapping/ColumnInfosUnitTests.java | 102 +++++ .../DefaultAggregatePathUnitTests.java | 161 ++++--- .../RelationalMappingContextUnitTests.java | 17 +- .../core/sql/TupleExpressionUnitTests.java | 48 ++ .../sql/render/DeleteRendererUnitTests.java | 8 +- .../sql/render/SelectRendererUnitTests.java | 425 +++++++++-------- .../sqlgeneration/AliasFactoryUnitTests.java | 55 +-- .../modules/ROOT/pages/jdbc/mapping.adoc | 3 + .../antora/modules/ROOT/partials/mapping.adoc | 7 + 42 files changed, 2393 insertions(+), 1033 deletions(-) delete mode 100644 spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcBackReferencePropertyValueProvider.java create mode 100644 spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/CompositeIdAggregateTemplateHsqlIntegrationTests.java rename spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/{SqlParametersFactoryTest.java => SqlParametersFactoryUnitTests.java} (86%) create mode 100644 spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/CompositeIdAggregateTemplateHsqlIntegrationTests-hsql.sql create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/TupleExpression.java create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/TupleVisitor.java create mode 100644 spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/ColumnInfosUnitTests.java create mode 100644 spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/TupleExpressionUnitTests.java diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java index c638a3e763..ac8cf92838 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java @@ -117,6 +117,7 @@ public Object insert(T instance, Class domainType, Identifier identifier, public Object[] insert(List> insertSubjects, Class domainType, IdValueSource idValueSource) { Assert.notEmpty(insertSubjects, "Batch insert must contain at least one InsertSubject"); + SqlIdentifierParameterSource[] sqlParameterSources = insertSubjects.stream() .map(insertSubject -> sqlParametersFactory.forInsert( // insertSubject.getInstance(), // @@ -167,7 +168,7 @@ public boolean updateWithVersion(S instance, Class domainType, Number pre public void delete(Object id, Class domainType) { String deleteByIdSql = sql(domainType).getDeleteById(); - SqlParameterSource parameter = sqlParametersFactory.forQueryById(id, domainType, ID_SQL_PARAMETER); + SqlParameterSource parameter = sqlParametersFactory.forQueryById(id, domainType); operations.update(deleteByIdSql, parameter); } @@ -188,7 +189,7 @@ public void deleteWithVersion(Object id, Class domainType, Number previou RelationalPersistentEntity persistentEntity = getRequiredPersistentEntity(domainType); - SqlIdentifierParameterSource parameterSource = sqlParametersFactory.forQueryById(id, domainType, ID_SQL_PARAMETER); + SqlIdentifierParameterSource parameterSource = sqlParametersFactory.forQueryById(id, domainType); parameterSource.addValue(VERSION_SQL_PARAMETER, previousVersion); int affectedRows = operations.update(sql(domainType).getDeleteByIdAndVersion(), parameterSource); @@ -208,8 +209,7 @@ public void delete(Object rootId, PersistentPropertyPath prope public void acquireLockById(Object id, LockMode lockMode, Class domainType) { String acquireLockByIdSql = sql(domainType).getAcquireLockById(lockMode); - SqlIdentifierParameterSource parameter = sqlParametersFactory.forQueryById(id, domainType, ID_SQL_PARAMETER); + SqlIdentifierParameterSource parameter = sqlParametersFactory.forQueryById(id, domainType); operations.query(acquireLockByIdSql, parameter, ResultSet::next); } @@ -269,7 +269,7 @@ public long count(Class domainType) { public T findById(Object id, Class domainType) { String findOneSql = sql(domainType).getFindOne(); - SqlIdentifierParameterSource parameter = sqlParametersFactory.forQueryById(id, domainType, ID_SQL_PARAMETER); + SqlIdentifierParameterSource parameter = sqlParametersFactory.forQueryById(id, domainType); try { return operations.queryForObject(findOneSql, parameter, getEntityRowMapper(domainType)); @@ -355,7 +355,7 @@ public Object mapRow(ResultSet rs, int rowNum) throws SQLException { public boolean existsById(Object id, Class domainType) { String existsSql = sql(domainType).getExists(); - SqlParameterSource parameter = sqlParametersFactory.forQueryById(id, domainType, ID_SQL_PARAMETER); + SqlParameterSource parameter = sqlParametersFactory.forQueryById(id, domainType); Boolean result = operations.queryForObject(existsSql, parameter, Boolean.class); Assert.state(result != null, "The result of an exists query must not be null"); diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcBackReferencePropertyValueProvider.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcBackReferencePropertyValueProvider.java deleted file mode 100644 index 34f9e88de5..0000000000 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcBackReferencePropertyValueProvider.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2020-2025 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.jdbc.core.convert; - -import org.springframework.data.mapping.model.PropertyValueProvider; -import org.springframework.data.relational.core.mapping.AggregatePath; -import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; - -/** - * {@link PropertyValueProvider} obtaining values from a {@link ResultSetAccessor}. For a given id property it provides - * the value in the resultset under which other entities refer back to it. - * - * @author Jens Schauder - * @author Kurt Niemi - * @author Mikhail Polivakha - * @since 2.0 - */ -class JdbcBackReferencePropertyValueProvider implements PropertyValueProvider { - - private final AggregatePath basePath; - private final ResultSetAccessor resultSet; - - /** - * @param basePath path from the aggregate root relative to which all properties get resolved. - * @param resultSet the {@link ResultSetAccessor} from which to obtain the actual values. - */ - JdbcBackReferencePropertyValueProvider(AggregatePath basePath, ResultSetAccessor resultSet) { - - this.resultSet = resultSet; - this.basePath = basePath; - } - - @Override - public T getPropertyValue(RelationalPersistentProperty property) { - return (T) resultSet.getObject(basePath.append(property).getTableInfo().reverseColumnInfo().alias().getReference()); - } - - public JdbcBackReferencePropertyValueProvider extendBy(RelationalPersistentProperty property) { - return new JdbcBackReferencePropertyValueProvider(basePath.append(property), resultSet); - } -} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilder.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilder.java index 22944aaad2..41e6b2c488 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilder.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilder.java @@ -15,7 +15,13 @@ */ package org.springframework.data.jdbc.core.convert; +import java.util.function.Function; + +import org.springframework.data.mapping.PersistentPropertyPathAccessor; import org.springframework.data.relational.core.mapping.AggregatePath; +import org.springframework.data.relational.core.mapping.RelationalMappingContext; +import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; +import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.util.Assert; /** @@ -41,13 +47,31 @@ public static JdbcIdentifierBuilder empty() { */ public static JdbcIdentifierBuilder forBackReferences(JdbcConverter converter, AggregatePath path, Object value) { - Identifier identifier = Identifier.of( // - path.getTableInfo().reverseColumnInfo().name(), // - value, // - converter.getColumnType(path.getIdDefiningParentPath().getRequiredIdProperty()) // - ); + RelationalPersistentProperty idProperty = path.getIdDefiningParentPath().getRequiredIdProperty(); + AggregatePath.ColumnInfos reverseColumnInfos = path.getTableInfo().reverseColumnInfos(); + + // create property accessor + RelationalMappingContext mappingContext = converter.getMappingContext(); + RelationalPersistentEntity persistentEntity = mappingContext.getPersistentEntity(idProperty.getType()); + + Function valueProvider; + if (persistentEntity == null) { + valueProvider = ap -> value; + } else { + PersistentPropertyPathAccessor propertyPathAccessor = persistentEntity.getPropertyPathAccessor(value); + valueProvider = ap -> propertyPathAccessor.getProperty(ap.getRequiredPersistentPropertyPath()); + } + + final Identifier[] identifierHolder = new Identifier[] { Identifier.empty() }; + + reverseColumnInfos.forEach((ap, ci) -> { + + RelationalPersistentProperty property = ap.getRequiredLeafProperty(); + identifierHolder[0] = identifierHolder[0].withPart(ci.name(), valueProvider.apply(ap), + converter.getColumnType(property)); + }); - return new JdbcIdentifierBuilder(identifier); + return new JdbcIdentifierBuilder(identifierHolder[0]); } /** @@ -62,8 +86,9 @@ public JdbcIdentifierBuilder withQualifier(AggregatePath path, Object value) { Assert.notNull(path, "Path must not be null"); Assert.notNull(value, "Value must not be null"); - identifier = identifier.withPart(path.getTableInfo().qualifierColumnInfo().name(), value, - path.getTableInfo().qualifierColumnType()); + AggregatePath.TableInfo tableInfo = path.getTableInfo(); + identifier = identifier.withPart(tableInfo.qualifierColumnInfo().name(), value, + tableInfo.qualifierColumnType()); return this; } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverter.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverter.java index 7460931dab..80e1975de2 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverter.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverter.java @@ -80,7 +80,7 @@ public class MappingJdbcConverter extends MappingRelationalConverter implements * {@link #MappingJdbcConverter(RelationalMappingContext, RelationResolver, CustomConversions, JdbcTypeFactory)} * (MappingContext, RelationResolver, JdbcTypeFactory)} to convert arrays and large objects into JDBC-specific types. * - * @param context must not be {@literal null}. + * @param context must not be {@literal null}. * @param relationResolver used to fetch additional relations from the database. Must not be {@literal null}. */ public MappingJdbcConverter(RelationalMappingContext context, RelationResolver relationResolver) { @@ -98,12 +98,12 @@ public MappingJdbcConverter(RelationalMappingContext context, RelationResolver r /** * Creates a new {@link MappingJdbcConverter} given {@link MappingContext}. * - * @param context must not be {@literal null}. + * @param context must not be {@literal null}. * @param relationResolver used to fetch additional relations from the database. Must not be {@literal null}. - * @param typeFactory must not be {@literal null} + * @param typeFactory must not be {@literal null} */ public MappingJdbcConverter(RelationalMappingContext context, RelationResolver relationResolver, - CustomConversions conversions, JdbcTypeFactory typeFactory) { + CustomConversions conversions, JdbcTypeFactory typeFactory) { super(context, conversions); @@ -220,7 +220,7 @@ private boolean canWriteAsJdbcValue(@Nullable Object value) { return true; } - if (value instanceof AggregateReference aggregateReference) { + if (value instanceof AggregateReference aggregateReference) { return canWriteAsJdbcValue(aggregateReference.getId()); } @@ -285,7 +285,7 @@ public R readAndResolve(TypeInformation type, RowDocument source, Identif @Override protected RelationalPropertyValueProvider newValueProvider(RowDocumentAccessor documentAccessor, - ValueExpressionEvaluator evaluator, ConversionContext context) { + ValueExpressionEvaluator evaluator, ConversionContext context) { if (context instanceof ResolvingConversionContext rcc) { @@ -314,7 +314,7 @@ class ResolvingRelationalPropertyValueProvider implements RelationalPropertyValu private final Identifier identifier; private ResolvingRelationalPropertyValueProvider(AggregatePathValueProvider delegate, RowDocumentAccessor accessor, - ResolvingConversionContext context, Identifier identifier) { + ResolvingConversionContext context, Identifier identifier) { AggregatePath path = context.aggregatePath(); @@ -323,7 +323,7 @@ private ResolvingRelationalPropertyValueProvider(AggregatePathValueProvider dele this.context = context; this.identifier = path.isEntity() ? potentiallyAppendIdentifier(identifier, path.getRequiredLeafEntity(), - property -> delegate.getValue(path.append(property))) + property -> delegate.getValue(path.append(property))) : identifier; } @@ -331,7 +331,7 @@ private ResolvingRelationalPropertyValueProvider(AggregatePathValueProvider dele * Conditionally append the identifier if the entity has an identifier property. */ static Identifier potentiallyAppendIdentifier(Identifier base, RelationalPersistentEntity entity, - Function getter) { + Function getter) { if (entity.hasIdProperty()) { @@ -361,24 +361,9 @@ public T getPropertyValue(RelationalPersistentProperty property) { if (property.isCollectionLike() || property.isMap()) { - Identifier identifierToUse = this.identifier; - AggregatePath idDefiningParentPath = aggregatePath.getIdDefiningParentPath(); + Identifier identifier = constructIdentifier(aggregatePath); - // note that the idDefiningParentPath might not itself have an id property, but have a combination of back - // references and possibly keys, that form an id - if (idDefiningParentPath.hasIdProperty()) { - - RelationalPersistentProperty identifier = idDefiningParentPath.getRequiredIdProperty(); - AggregatePath idPath = idDefiningParentPath.append(identifier); - Object value = delegate.getValue(idPath); - - Assert.state(value != null, "Identifier value must not be null at this point"); - - identifierToUse = Identifier.of(aggregatePath.getTableInfo().reverseColumnInfo().name(), value, - identifier.getActualType()); - } - - Iterable allByPath = relationResolver.findAllByPath(identifierToUse, + Iterable allByPath = relationResolver.findAllByPath(identifier, aggregatePath.getRequiredPersistentPropertyPath()); if (property.isCollectionLike()) { @@ -403,6 +388,29 @@ public T getPropertyValue(RelationalPersistentProperty property) { return (T) delegate.getValue(aggregatePath); } + private Identifier constructIdentifier(AggregatePath aggregatePath) { + + Identifier identifierToUse = this.identifier; + AggregatePath idDefiningParentPath = aggregatePath.getIdDefiningParentPath(); + + // note that the idDefiningParentPath might not itself have an id property, but have a combination of back + // references and possibly keys, that form an id + if (idDefiningParentPath.hasIdProperty()) { + + RelationalPersistentProperty idProperty = idDefiningParentPath.getRequiredIdProperty(); + AggregatePath idPath = idProperty.isEntity() ? idDefiningParentPath.append(idProperty) : idDefiningParentPath; + Identifier[] buildingIdentifier = new Identifier[] { Identifier.empty() }; + aggregatePath.getTableInfo().reverseColumnInfos().forEach((ap, ci) -> { + + Object value = delegate.getValue(idPath.append(ap)); + buildingIdentifier[0] = buildingIdentifier[0].withPart(ci.name(), value, + ap.getRequiredLeafProperty().getActualType()); + }); + identifierToUse = buildingIdentifier[0]; + } + return identifierToUse; + } + @Override public boolean hasValue(RelationalPersistentProperty property) { @@ -423,7 +431,7 @@ public boolean hasValue(RelationalPersistentProperty property) { return delegate.hasValue(toUse); } - return delegate.hasValue(aggregatePath.getTableInfo().reverseColumnInfo().alias()); + return delegate.hasValue(aggregatePath.getTableInfo().reverseColumnInfos().any().alias()); } return delegate.hasValue(aggregatePath); @@ -449,7 +457,7 @@ public boolean hasNonEmptyValue(RelationalPersistentProperty property) { return delegate.hasValue(toUse); } - return delegate.hasValue(aggregatePath.getTableInfo().reverseColumnInfo().alias()); + return delegate.hasValue(aggregatePath.getTableInfo().reverseColumnInfos().any().alias()); } return delegate.hasNonEmptyValue(aggregatePath); @@ -460,7 +468,7 @@ public RelationalPropertyValueProvider withContext(ConversionContext context) { return context == this.context ? this : new ResolvingRelationalPropertyValueProvider(delegate.withContext(context), accessor, - (ResolvingConversionContext) context, identifier); + (ResolvingConversionContext) context, identifier); } } @@ -472,7 +480,7 @@ public RelationalPropertyValueProvider withContext(ConversionContext context) { * @param identifier */ private record ResolvingConversionContext(ConversionContext delegate, AggregatePath aggregatePath, - Identifier identifier) implements ConversionContext { + Identifier identifier) implements ConversionContext { @Override public S convert(Object source, TypeInformation typeHint) { diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlContext.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlContext.java index 7663e6cd4f..b1b4dfdc2d 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlContext.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlContext.java @@ -40,10 +40,6 @@ class SqlContext { this.table = Table.create(entity.getQualifiedTableName()); } - Column getIdColumn() { - return table.column(entity.getIdColumn()); - } - Column getVersionColumn() { return table.column(entity.getRequiredVersionProperty().getColumnName()); } @@ -60,11 +56,20 @@ Table getTable(AggregatePath path) { } Column getColumn(AggregatePath path) { + AggregatePath.ColumnInfo columnInfo = path.getColumnInfo(); return getTable(path).column(columnInfo.name()).as(columnInfo.alias()); } - Column getReverseColumn(AggregatePath path) { - return getTable(path).column(path.getTableInfo().reverseColumnInfo().name()).as(path.getTableInfo().reverseColumnInfo().alias()); + /** + * A token reverse column, used in selects to identify, if an entity is present or {@literal null}. + * + * @param path must not be null. + * @return a {@literal Column} that is part of the effective primary key for the given path. + */ + Column getAnyReverseColumn(AggregatePath path) { + + AggregatePath.ColumnInfo columnInfo = path.getTableInfo().reverseColumnInfos().any(); + return getTable(path).column(columnInfo.name()).as(columnInfo.alias()); } } 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..e1a61709e5 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 @@ -36,6 +36,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.Pair; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -62,9 +63,7 @@ class SqlGenerator { static final SqlIdentifier VERSION_SQL_PARAMETER = SqlIdentifier.unquoted("___oldOptimisticLockingVersion"); - static final SqlIdentifier ID_SQL_PARAMETER = SqlIdentifier.unquoted("id"); static final SqlIdentifier IDS_SQL_PARAMETER = SqlIdentifier.unquoted("ids"); - static final SqlIdentifier ROOT_ID_PARAMETER = SqlIdentifier.unquoted("rootId"); /** * Length of an aggregate path that is one longer then the root path. @@ -73,7 +72,6 @@ class SqlGenerator { private final RelationalPersistentEntity entity; private final RelationalMappingContext mappingContext; - private final RenderContext renderContext; private final SqlContext sqlContext; private final SqlRenderer sqlRenderer; @@ -96,6 +94,10 @@ class SqlGenerator { private final QueryMapper queryMapper; private final Dialect dialect; + private final Function, Condition> inCondition; + private final Function, Condition> equalityCondition; + private final Function, Condition> notNullCondition; + /** * Create a new {@link SqlGenerator} given {@link RelationalMappingContext} and {@link RelationalPersistentEntity}. * @@ -110,11 +112,15 @@ class SqlGenerator { this.mappingContext = mappingContext; this.entity = entity; this.sqlContext = new SqlContext(entity); - this.renderContext = new RenderContextFactory(dialect).createRenderContext(); - this.sqlRenderer = SqlRenderer.create(renderContext); + this.sqlRenderer = SqlRenderer.create(new RenderContextFactory(dialect).createRenderContext()); this.columns = new Columns(entity, mappingContext, converter); this.queryMapper = new QueryMapper(converter); this.dialect = dialect; + + inCondition = inCondition(); + equalityCondition = equalityCondition(); + notNullCondition = isNotNullCondition(); + } /** @@ -156,44 +162,53 @@ private static boolean isDeeplyNested(AggregatePath path) { * given {@literal path} to those that reference the root entities specified by the {@literal rootCondition}. * * @param path specifies the table and id to select - * @param rootCondition the condition on the root of the path determining what to select - * @param filterColumn the column to apply the IN-condition to. + * @param conditionFunction a function for construction a where clause + * @param columns map making all columns available as a map from {@link AggregatePath} * @return the IN condition */ - private Condition getSubselectCondition(AggregatePath path, Function rootCondition, - Column filterColumn) { + private Condition getSubselectCondition(AggregatePath path, + Function, Condition> conditionFunction, Map columns) { AggregatePath parentPath = path.getParentPath(); if (!parentPath.hasIdProperty()) { if (isDeeplyNested(parentPath)) { - return getSubselectCondition(parentPath, rootCondition, filterColumn); + return getSubselectCondition(parentPath, conditionFunction, columns); } - return rootCondition.apply(filterColumn); + return conditionFunction.apply(columns); } - Table subSelectTable = Table.create(parentPath.getTableInfo().qualifiedTableName()); - Column idColumn = subSelectTable.column(parentPath.getTableInfo().idColumnName()); - Column selectFilterColumn = subSelectTable.column(parentPath.getTableInfo().effectiveIdColumnName()); + AggregatePath.TableInfo parentPathTableInfo = parentPath.getTableInfo(); + Table subSelectTable = Table.create(parentPathTableInfo.qualifiedTableName()); + + Map selectFilterColumns = new TreeMap<>(); + parentPathTableInfo.effectiveIdColumnInfos().forEach( // + (ap, ci) -> // + selectFilterColumns.put(ap, subSelectTable.column(ci.name())) // + ); Condition innerCondition; if (isFirstNonRoot(parentPath)) { // if the parent is the root of the path - // apply the rootCondition - innerCondition = rootCondition.apply(selectFilterColumn); + innerCondition = conditionFunction.apply(selectFilterColumns); } else { - // otherwise, we need another layer of subselect - innerCondition = getSubselectCondition(parentPath, rootCondition, selectFilterColumn); + innerCondition = getSubselectCondition(parentPath, conditionFunction, selectFilterColumns); } + List idColumns = parentPathTableInfo.idColumnInfos().toList(ci -> subSelectTable.column(ci.name())); + Select select = Select.builder() // - .select(idColumn) // + .select(idColumns) // .from(subSelectTable) // .where(innerCondition).build(); - return filterColumn.in(select); + return Conditions.in(toExpression(columns), select); + } + + private Expression toExpression(Map columnsMap) { + return TupleExpression.maybeWrap(new ArrayList<>(columnsMap.values())); } private BindMarker getBindMarker(SqlIdentifier columnName) { @@ -439,7 +454,7 @@ String createDeleteAllSql(@Nullable PersistentPropertyPath path) { - return createDeleteByPathAndCriteria(mappingContext.getAggregatePath(path), - filterColumn -> filterColumn.isEqualTo(getBindMarker(ROOT_ID_PARAMETER))); + return createDeleteByPathAndCriteria(mappingContext.getAggregatePath(path), equalityCondition); } /** @@ -462,17 +476,82 @@ String createDeleteByPath(PersistentPropertyPath p * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. */ String createDeleteInByPath(PersistentPropertyPath path) { + return createDeleteByPathAndCriteria(mappingContext.getAggregatePath(path), inCondition); + } + + /** + * Constructs a function for constructing a where condition. The where condition will be of the form + * {@literal IN :bind-marker} + */ + private Function, Condition> inCondition() { + + return columnMap -> { + + List columns = List.copyOf(columnMap.values()); + + if (columns.size() == 1) { + return Conditions.in(columns.get(0), getBindMarker(IDS_SQL_PARAMETER)); + } + return Conditions.in(TupleExpression.create(columns), getBindMarker(IDS_SQL_PARAMETER)); + }; + } + + /** + * Constructs a function for constructing a where. The where condition will be of the form + * {@literal = :bind-marker-a AND = :bind-marker-b ...} + */ + private Function, Condition> equalityCondition() { - return createDeleteByPathAndCriteria(mappingContext.getAggregatePath(path), - filterColumn -> filterColumn.in(getBindMarker(IDS_SQL_PARAMETER))); + AggregatePath.ColumnInfos idColumnInfos = mappingContext.getAggregatePath(entity).getTableInfo().idColumnInfos(); + + return columnMap -> { + + Condition result = null; + for (Map.Entry entry : columnMap.entrySet()) { + BindMarker bindMarker = getBindMarker(idColumnInfos.get(entry.getKey()).name()); + Comparison singleCondition = entry.getValue().isEqualTo(bindMarker); + + result = result == null ? singleCondition : result.and(singleCondition); + } + return result; + }; + } + + /** + * Constructs a function for constructing where a condition. The where condition will be of the form + * {@literal IS NOT NULL AND IS NOT NULL ... } + */ + private Function, Condition> isNotNullCondition() { + + return columnMap -> { + + Condition result = null; + for (Column column : columnMap.values()) { + Condition singleCondition = column.isNotNull(); + + result = result == null ? singleCondition : result.and(singleCondition); + } + return result; + }; } private String createFindOneSql() { - Select select = selectBuilder().where(getIdColumn().isEqualTo(getBindMarker(ID_SQL_PARAMETER))) // - .build(); + return render(selectBuilder().where(equalityIdWhereCondition()).build()); + } - return render(select); + private Condition equalityIdWhereCondition() { + + Condition aggregate = null; + for (Column column : getIdColumns()) { + + Comparison condition = column.isEqualTo(getBindMarker(column.getName())); + aggregate = aggregate == null ? condition : aggregate.and(condition); + } + + Assert.state(aggregate != null, "We need at least one id column"); + + return aggregate; } private String createAcquireLockById(LockMode lockMode) { @@ -480,9 +559,9 @@ private String createAcquireLockById(LockMode lockMode) { Table table = this.getTable(); Select select = StatementBuilder // - .select(getIdColumn()) // + .select(getSingleNonNullColumn()) // .from(table) // - .where(getIdColumn().isEqualTo(getBindMarker(ID_SQL_PARAMETER))) // + .where(equalityIdWhereCondition()) // .lock(lockMode) // .build(); @@ -494,7 +573,7 @@ private String createAcquireLockAll(LockMode lockMode) { Table table = this.getTable(); Select select = StatementBuilder // - .select(getIdColumn()) // + .select(getSingleNonNullColumn()) // .from(table) // .lock(lockMode) // .build(); @@ -538,14 +617,24 @@ private SelectBuilder.SelectWhere selectBuilder(Collection keyCol columnExpressions.add(table.column(keyColumn).as(keyColumn)); } - SelectBuilder.SelectAndFrom selectBuilder = StatementBuilder.select(columnExpressions); - SelectBuilder.SelectJoin baseSelect = selectBuilder.from(table); + SelectBuilder.SelectJoin baseSelect = StatementBuilder.select(columnExpressions).from(table); + + return (SelectBuilder.SelectWhere) addJoins(baseSelect, joinTables); + } + + private static SelectBuilder.SelectJoin addJoins(SelectBuilder.SelectJoin baseSelect, List joinTables) { for (Join join : joinTables) { - baseSelect = baseSelect.leftOuterJoin(join.joinTable).on(join.joinColumn).equals(join.parentId); - } - return (SelectBuilder.SelectWhere) baseSelect; + Condition condition = null; + for (Pair columnPair : join.columns) { + Comparison elementalCondition = columnPair.getFirst().isEqualTo(columnPair.getSecond()); + condition = condition == null ? elementalCondition : condition.and(elementalCondition); + } + + baseSelect = baseSelect.leftOuterJoin(join.joinTable).on(Objects.requireNonNull(condition)); + } + return baseSelect; } private SelectBuilder.SelectOrdered selectBuilder(Collection keyColumns, Sort sort, @@ -605,7 +694,7 @@ Column getColumn(AggregatePath path) { return null; } - return sqlContext.getReverseColumn(path); + return sqlContext.getAnyReverseColumn(path); } return sqlContext.getColumn(path); @@ -619,32 +708,46 @@ Join getJoin(AggregatePath path) { } Table currentTable = sqlContext.getTable(path); + AggregatePath.ColumnInfos backRefColumnInfos = path.getTableInfo().reverseColumnInfos(); AggregatePath idDefiningParentPath = path.getIdDefiningParentPath(); Table parentTable = sqlContext.getTable(idDefiningParentPath); + AggregatePath.ColumnInfos idColumnInfos = idDefiningParentPath.getTableInfo().idColumnInfos(); + + List> joinConditions = new ArrayList<>(); + backRefColumnInfos.forEach((ap, ci) -> { + joinConditions.add(Pair.of(currentTable.column(ci.name()), parentTable.column(idColumnInfos.get(ap).name()))); + }); return new Join( // currentTable, // - currentTable.column(path.getTableInfo().reverseColumnInfo().name()), // - parentTable.column(idDefiningParentPath.getTableInfo().idColumnName()) // + joinConditions // ); } private String createFindAllInListSql() { - Select select = selectBuilder().where(getIdColumn().in(getBindMarker(IDS_SQL_PARAMETER))).build(); + In condition = idInWhereClause(); + Select select = selectBuilder().where(condition).build(); return render(select); } - private String createExistsSql() { + private In idInWhereClause() { + + List idColumns = getIdColumns(); + Expression expression = idColumns.size() == 1 ? idColumns.get(0) : TupleExpression.create(idColumns); + + return Conditions.in(expression, getBindMarker(IDS_SQL_PARAMETER)); + } + private String createExistsSql() { Table table = getTable(); Select select = StatementBuilder // - .select(Functions.count(getIdColumn())) // + .select(Functions.count(getSingleNonNullColumn())) // .from(table) // - .where(getIdColumn().isEqualTo(getBindMarker(ID_SQL_PARAMETER))) // + .where(equalityIdWhereCondition()) // .build(); return render(select); @@ -715,7 +818,7 @@ private UpdateBuilder.UpdateWhereAndOr createBaseUpdate() { return Update.builder() // .table(table) // .set(assignments) // - .where(getIdColumn().isEqualTo(getBindMarker(entity.getIdColumn()))); + .where(equalityIdWhereCondition()); } private String createDeleteByIdSql() { @@ -738,16 +841,17 @@ private String createDeleteByIdAndVersionSql() { private DeleteBuilder.DeleteWhereAndOr createBaseDeleteById(Table table) { return Delete.builder().from(table) // - .where(getIdColumn().isEqualTo(getBindMarker(ID_SQL_PARAMETER))); + .where(equalityIdWhereCondition()); } private DeleteBuilder.DeleteWhereAndOr createBaseDeleteByIdIn(Table table) { return Delete.builder().from(table) // - .where(getIdColumn().in(getBindMarker(IDS_SQL_PARAMETER))); + .where(idInWhereClause()); } - private String createDeleteByPathAndCriteria(AggregatePath path, Function rootCondition) { + private String createDeleteByPathAndCriteria(AggregatePath path, + Function, Condition> multiIdCondition) { Table table = Table.create(path.getTableInfo().qualifiedTableName()); @@ -755,16 +859,18 @@ private String createDeleteByPathAndCriteria(AggregatePath path, Function columns = new TreeMap<>(); + AggregatePath.ColumnInfos columnInfos = path.getTableInfo().reverseColumnInfos(); + columnInfos.forEach((ag, ci) -> columns.put(ag, table.column(ci.name()))); if (isFirstNonRoot(path)) { delete = builder // - .where(rootCondition.apply(filterColumn)) // + .where(multiIdCondition.apply(columns)) // .build(); } else { - Condition condition = getSubselectCondition(path, rootCondition, filterColumn); + Condition condition = getSubselectCondition(path, multiIdCondition, columns); delete = builder.where(condition).build(); } @@ -777,7 +883,7 @@ private String createDeleteByListSql() { Delete delete = Delete.builder() // .from(table) // - .where(getIdColumn().in(getBindMarker(IDS_SQL_PARAMETER))) // + .where(idInWhereClause()) // .build(); return render(delete); @@ -803,8 +909,22 @@ private Table getTable() { return sqlContext.getTable(); } - private Column getIdColumn() { - return sqlContext.getIdColumn(); + /** + * @return a single column of the primary key to be used in places where one need something not null to be selected. + */ + private Column getSingleNonNullColumn() { + + AggregatePath.ColumnInfos columnInfos = mappingContext.getAggregatePath(entity).getTableInfo().idColumnInfos(); + return columnInfos.any((ap, ci) -> sqlContext.getTable(columnInfos.fullPath(ap)).column(ci.name()).as(ci.alias())); + } + + private List getIdColumns() { + + AggregatePath.ColumnInfos columnInfos = mappingContext.getAggregatePath(entity).getTableInfo().idColumnInfos(); + List result = new ArrayList<>(columnInfos.size()); + columnInfos.forEach((ap, ci) -> result.add(sqlContext.getColumn(columnInfos.fullPath(ap)))); + + return result; } private Column getVersionColumn() { @@ -961,7 +1081,8 @@ private SelectBuilder.SelectJoin getExistsSelect() { .select(dialect.getExistsFunction()) // .from(table); - // add possible joins + // collect joins + List joins = new ArrayList<>(); for (PersistentPropertyPath path : mappingContext .findPersistentPropertyPaths(entity.getType(), p -> true)) { @@ -970,10 +1091,11 @@ private SelectBuilder.SelectJoin getExistsSelect() { // add a join if necessary Join join = getJoin(aggregatePath); if (join != null) { - baseSelect = baseSelect.leftOuterJoin(join.joinTable).on(join.joinColumn).equals(join.parentId); + joins.add(join); } } - return baseSelect; + + return addJoins(baseSelect, joins); } /** @@ -995,6 +1117,7 @@ private SelectBuilder.SelectJoin getSelectCountWithExpression(Expression... coun .select(Functions.count(countExpressions)) // .from(table); + List joins = new ArrayList<>(); // add possible joins for (PersistentPropertyPath path : mappingContext .findPersistentPropertyPaths(entity.getType(), p -> true)) { @@ -1004,10 +1127,10 @@ private SelectBuilder.SelectJoin getSelectCountWithExpression(Expression... coun // add a join if necessary Join join = getJoin(extPath); if (join != null) { - baseSelect = baseSelect.leftOuterJoin(join.joinTable).on(join.joinColumn).equals(join.parentId); + joins.add(join); } } - return baseSelect; + return addJoins(baseSelect, joins); } private SelectBuilder.SelectOrdered applyQueryOnSelect(Query query, MapSqlParameterSource parameterSource, @@ -1048,62 +1171,7 @@ SelectBuilder.SelectOrdered applyCriteria(@Nullable CriteriaDefinition criteria, /** * Value object representing a {@code JOIN} association. */ - static final class Join { - - private final Table joinTable; - private final Column joinColumn; - private final Column parentId; - - Join(Table joinTable, Column joinColumn, Column parentId) { - - Assert.notNull(joinTable, "JoinTable must not be null"); - Assert.notNull(joinColumn, "JoinColumn must not be null"); - Assert.notNull(parentId, "ParentId must not be null"); - - this.joinTable = joinTable; - this.joinColumn = joinColumn; - this.parentId = parentId; - } - - Table getJoinTable() { - return this.joinTable; - } - - Column getJoinColumn() { - return this.joinColumn; - } - - Column getParentId() { - return this.parentId; - } - - @Override - public boolean equals(@Nullable Object o) { - - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Join join = (Join) o; - return joinTable.equals(join.joinTable) && joinColumn.equals(join.joinColumn) && parentId.equals(join.parentId); - } - - @Override - public int hashCode() { - return Objects.hash(joinTable, joinColumn, parentId); - } - - @Override - public String toString() { - - return "Join{" + // - "joinTable=" + joinTable + // - ", joinColumn=" + joinColumn + // - ", parentId=" + parentId + // - '}'; - } + record Join(Table joinTable, List> columns) { } /** diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlParametersFactory.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlParametersFactory.java index 8bf9bb869f..4e9ee941ed 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlParametersFactory.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlParametersFactory.java @@ -25,7 +25,9 @@ import org.springframework.data.jdbc.support.JdbcUtil; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.PersistentPropertyPathAccessor; import org.springframework.data.relational.core.conversion.IdValueSource; +import org.springframework.data.relational.core.mapping.AggregatePath; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; @@ -78,9 +80,15 @@ SqlIdentifierParameterSource forInsert(T instance, Class domainType, Iden if (IdValueSource.PROVIDED.equals(idValueSource)) { - RelationalPersistentProperty idProperty = persistentEntity.getRequiredIdProperty(); - Object idValue = persistentEntity.getIdentifierAccessor(instance).getRequiredIdentifier(); - addConvertedPropertyValue(parameterSource, idProperty, idValue, idProperty.getColumnName()); + PersistentPropertyPathAccessor propertyPathAccessor = persistentEntity.getPropertyPathAccessor(instance); + + AggregatePath.ColumnInfos columnInfos = context.getAggregatePath(persistentEntity).getTableInfo().idColumnInfos(); + columnInfos.forEach((ap, __) -> { + Object idValue = propertyPathAccessor.getProperty(columnInfos.fullPath(ap).getRequiredPersistentPropertyPath()); + RelationalPersistentProperty idProperty = ap.getRequiredLeafProperty(); + addConvertedPropertyValue(parameterSource, idProperty, idValue, idProperty.getColumnName()); + }); + } return parameterSource; } @@ -104,20 +112,40 @@ SqlIdentifierParameterSource forUpdate(T instance, Class domainType) { * * @param id the entity id. Must not be {@code null}. * @param domainType the type of the instance. Must not be {@code null}. - * @param name the name to be used for the id parameter. * @return the {@link SqlIdentifierParameterSource} for the query. Guaranteed to not be {@code null}. * @since 2.4 */ - SqlIdentifierParameterSource forQueryById(Object id, Class domainType, SqlIdentifier name) { + SqlIdentifierParameterSource forQueryById(Object id, Class domainType) { SqlIdentifierParameterSource parameterSource = new SqlIdentifierParameterSource(); - addConvertedPropertyValue( // - parameterSource, // - getRequiredPersistentEntity(domainType).getRequiredIdProperty(), // - id, // - name // - ); + RelationalPersistentEntity entity = getRequiredPersistentEntity(domainType); + RelationalPersistentProperty singleIdProperty = entity.getRequiredIdProperty(); + + if (singleIdProperty.isEntity()) { + + RelationalPersistentEntity complexId = context.getPersistentEntity(singleIdProperty); + PersistentPropertyPathAccessor accessor = complexId.getPropertyPathAccessor(id); + + context.getAggregatePath(entity).getTableInfo().idColumnInfos().forEach((ap, ci) -> { + Object idValue = accessor.getProperty(ap.getRequiredPersistentPropertyPath()); + + addConvertedPropertyValue( // + parameterSource, // + ap.getRequiredLeafProperty(), // + idValue, // + ci.name() // + ); + }); + } else { + + addConvertedPropertyValue( // + parameterSource, // + singleIdProperty, // + id, // + singleIdProperty.getColumnName() // + ); + } return parameterSource; } @@ -133,9 +161,32 @@ SqlIdentifierParameterSource forQueryByIds(Iterable ids, Class domainT SqlIdentifierParameterSource parameterSource = new SqlIdentifierParameterSource(); - addConvertedPropertyValuesAsList(parameterSource, getRequiredPersistentEntity(domainType).getRequiredIdProperty(), - ids); + RelationalPersistentEntity entity = context.getPersistentEntity(domainType); + RelationalPersistentProperty singleIdProperty = entity.getRequiredIdProperty(); + + if (singleIdProperty.isEntity()) { + + RelationalPersistentEntity complexId = context.getPersistentEntity(singleIdProperty); + AggregatePath.ColumnInfos idColumnInfos = context.getAggregatePath(entity).getTableInfo().idColumnInfos(); + + List parameterValues = new ArrayList<>(); + for (Object id : ids) { + + PersistentPropertyPathAccessor accessor = complexId.getPropertyPathAccessor(id); + + List tupleList = new ArrayList<>(); + idColumnInfos.forEach((ap, ci) -> { + tupleList.add(accessor.getProperty(ap.getRequiredPersistentPropertyPath())); + }); + parameterValues.add(tupleList.toArray(new Object[0])); + } + + parameterSource.addValue(SqlGenerator.IDS_SQL_PARAMETER, parameterValues); + } else { + addConvertedPropertyValuesAsList(parameterSource, getRequiredPersistentEntity(domainType).getRequiredIdProperty(), + ids); + } return parameterSource; } @@ -156,21 +207,6 @@ SqlIdentifierParameterSource forQueryByIdentifier(Identifier identifier) { return parameterSource; } - /** - * Utility to create {@link Predicate}s. - */ - static class Predicates { - - /** - * Include all {@link Predicate} returning {@literal false} to never skip a property. - * - * @return the include all {@link Predicate}. - */ - static Predicate includeAll() { - return it -> false; - } - } - private void addConvertedPropertyValue(SqlIdentifierParameterSource parameterSource, RelationalPersistentProperty property, @Nullable Object value, SqlIdentifier name) { diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcDeleteQueryCreator.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcDeleteQueryCreator.java index a7d187b441..a02c681b66 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcDeleteQueryCreator.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcDeleteQueryCreator.java @@ -17,12 +17,10 @@ import java.util.ArrayList; import java.util.List; -import java.util.stream.Stream; import org.springframework.data.domain.Sort; import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.core.convert.QueryMapper; -import org.springframework.data.mapping.Parameter; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.relational.core.dialect.Dialect; import org.springframework.data.relational.core.dialect.RenderContextFactory; @@ -31,14 +29,9 @@ import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.core.query.Criteria; -import org.springframework.data.relational.core.sql.Condition; -import org.springframework.data.relational.core.sql.Conditions; -import org.springframework.data.relational.core.sql.Delete; +import org.springframework.data.relational.core.sql.*; import org.springframework.data.relational.core.sql.DeleteBuilder.DeleteWhere; -import org.springframework.data.relational.core.sql.Select; import org.springframework.data.relational.core.sql.SelectBuilder.SelectWhere; -import org.springframework.data.relational.core.sql.StatementBuilder; -import org.springframework.data.relational.core.sql.Table; import org.springframework.data.relational.core.sql.render.SqlRenderer; import org.springframework.data.relational.repository.query.RelationalEntityMetadata; import org.springframework.data.relational.repository.query.RelationalParameterAccessor; @@ -49,8 +42,8 @@ import org.springframework.util.Assert; /** - * Implementation of {@link RelationalQueryCreator} that creates {@link List} of deletion {@link ParametrizedQuery} - * from a {@link PartTree}. + * Implementation of {@link RelationalQueryCreator} that creates {@link List} of deletion {@link ParametrizedQuery} from + * a {@link PartTree}. * * @author Yunyoung LEE * @author Nikita Konev @@ -96,13 +89,16 @@ protected List complete(@Nullable Criteria criteria, Sort sor Table table = Table.create(entityMetadata.getTableName()); MapSqlParameterSource parameterSource = new MapSqlParameterSource(); - SqlContext sqlContext = new SqlContext(entity); + SqlContext sqlContext = new SqlContext(); Condition condition = criteria == null ? null : queryMapper.getMappedObject(parameterSource, criteria, table, entity); + List idColumns = context.getAggregatePath(entity).getTableInfo().idColumnInfos() + .toList(ci -> table.column(ci.name())); + // create select criteria query for subselect - SelectWhere selectBuilder = StatementBuilder.select(sqlContext.getIdColumn()).from(table); + SelectWhere selectBuilder = StatementBuilder.select(idColumns).from(table); Select select = condition == null ? selectBuilder.build() : selectBuilder.where(condition).build(); // create delete relation queries @@ -139,19 +135,28 @@ private void deleteRelations(List deleteChain, RelationalPersistentEntit if (aggregatePath.isEntity() && !aggregatePath.isEmbedded()) { - SqlContext sqlContext = new SqlContext(aggregatePath.getLeafEntity()); + SqlContext sqlContext = new SqlContext(); + + // MariaDB prior to 11.6 does not support aliases for delete statements + Table table = sqlContext.getUnaliasedTable(aggregatePath); + + List reverseColumns = aggregatePath.getTableInfo().reverseColumnInfos() + .toList(ci -> table.column(ci.name())); + Expression expression = TupleExpression.maybeWrap(reverseColumns); + + Condition inCondition = Conditions.in(expression, parentSelect); - Condition inCondition = Conditions - .in(sqlContext.getTable().column(aggregatePath.getTableInfo().reverseColumnInfo().name()), parentSelect); + List parentIdColumns = aggregatePath.getIdDefiningParentPath().getTableInfo().idColumnInfos() + .toList(ci -> table.column(ci.name())); Select select = StatementBuilder.select( // - sqlContext.getTable().column(aggregatePath.getIdDefiningParentPath().getTableInfo().idColumnName()) // - ).from(sqlContext.getTable()) // + parentIdColumns // + ).from(table) // .where(inCondition) // .build(); deleteRelations(deleteChain, aggregatePath.getLeafEntity(), select); - deleteChain.add(StatementBuilder.delete(sqlContext.getTable()).where(inCondition).build()); + deleteChain.add(StatementBuilder.delete(table).where(inCondition).build()); } } } 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 cc28ff2f18..7d22cd2fdc 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 @@ -17,7 +17,6 @@ import java.util.ArrayList; import java.util.List; -import java.util.Objects; import java.util.Optional; import org.springframework.data.domain.Pageable; @@ -32,14 +31,7 @@ import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; 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.Expression; -import org.springframework.data.relational.core.sql.Expressions; -import org.springframework.data.relational.core.sql.Functions; -import org.springframework.data.relational.core.sql.Select; -import org.springframework.data.relational.core.sql.SelectBuilder; -import org.springframework.data.relational.core.sql.StatementBuilder; -import org.springframework.data.relational.core.sql.Table; +import org.springframework.data.relational.core.sql.*; import org.springframework.data.relational.core.sql.render.SqlRenderer; import org.springframework.data.relational.repository.Lock; import org.springframework.data.relational.repository.query.RelationalEntityMetadata; @@ -222,7 +214,8 @@ SelectBuilder.SelectLimitOffset createSelectClause(RelationalPersistentEntity SelectBuilder.SelectJoin builder; if (tree.isExistsProjection()) { - Column idColumn = table.column(entity.getIdColumn()); + AggregatePath.ColumnInfo anyIdColumnInfo = context.getAggregatePath(entity).getTableInfo().idColumnInfos().any(); + Column idColumn = table.column(anyIdColumnInfo.name()); builder = Select.builder().select(idColumn).from(table); } else if (tree.isCountProjection()) { builder = Select.builder().select(Functions.count(Expressions.asterisk())).from(table); @@ -237,7 +230,7 @@ private SelectBuilder.SelectJoin selectBuilder(Table table) { List columnExpressions = new ArrayList<>(); RelationalPersistentEntity entity = entityMetadata.getTableEntity(); - SqlContext sqlContext = new SqlContext(entity); + SqlContext sqlContext = new SqlContext(); List joinTables = new ArrayList<>(); for (PersistentPropertyPath path : context @@ -267,7 +260,19 @@ private SelectBuilder.SelectJoin selectBuilder(Table table) { SelectBuilder.SelectJoin baseSelect = selectBuilder.from(table); for (Join join : joinTables) { - baseSelect = baseSelect.leftOuterJoin(join.joinTable).on(join.joinColumn).equals(join.parentId); + + Condition condition = null; + + for (int i = 0; i < join.joinColumns.size(); i++) { + Column parentColumn = join.parentId.get(i); + Column joinColumn = join.joinColumns.get(i); + Comparison singleCondition = joinColumn.isEqualTo(parentColumn); + condition = condition == null ? singleCondition : condition.and(singleCondition); + } + + Assert.state(condition != null, "No condition found"); + + baseSelect = baseSelect.leftOuterJoin(join.joinTable).on(condition); } return baseSelect; @@ -276,7 +281,7 @@ private SelectBuilder.SelectJoin selectBuilder(Table table) { /** * Create a {@link Column} for {@link AggregatePath}. * - * @param sqlContext + * @param sqlContext for generating SQL constructs. * @param path the path to the column in question. * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. */ @@ -293,9 +298,6 @@ private Column getColumn(SqlContext sqlContext, AggregatePath path) { if (path.isEntity()) { - // Simple entities without id include there backreference as an synthetic id in order to distinguish null entities - // from entities with only null values. - if (path.isQualified() // || path.isCollectionLike() // || path.hasIdProperty() // @@ -303,7 +305,9 @@ private Column getColumn(SqlContext sqlContext, AggregatePath path) { return null; } - return sqlContext.getReverseColumn(path); + // Simple entities without id include there backreference as an synthetic id in order to distinguish null entities + // from entities with only null values. + return sqlContext.getAnyReverseColumn(path); } return sqlContext.getColumn(path); @@ -321,53 +325,25 @@ Join getJoin(SqlContext sqlContext, AggregatePath path) { AggregatePath idDefiningParentPath = path.getIdDefiningParentPath(); Table parentTable = sqlContext.getTable(idDefiningParentPath); + List reverseColumns = path.getTableInfo().reverseColumnInfos().toList(ci -> currentTable.column(ci.name())); + List idColumns = idDefiningParentPath.getTableInfo().idColumnInfos() + .toList(ci -> parentTable.column(ci.name())); return new Join( // currentTable, // - currentTable.column(path.getTableInfo().reverseColumnInfo().name()), // - parentTable.column(idDefiningParentPath.getTableInfo().idColumnName()) // + reverseColumns, // + idColumns // ); } /** * Value object representing a {@code JOIN} association. */ - static private final class Join { + private record Join(Table joinTable, List joinColumns, List parentId) { - private final Table joinTable; - private final Column joinColumn; - private final Column parentId; - - Join(Table joinTable, Column joinColumn, Column parentId) { - - Assert.notNull(joinTable, "JoinTable must not be null"); - Assert.notNull(joinColumn, "JoinColumn must not be null"); - Assert.notNull(parentId, "ParentId must not be null"); - - this.joinTable = joinTable; - this.joinColumn = joinColumn; - this.parentId = parentId; - } - - @Override - public boolean equals(@Nullable Object o) { - - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - Join join = (Join) o; - return joinTable.equals(join.joinTable) && joinColumn.equals(join.joinColumn) && parentId.equals(join.parentId); - } - - @Override - public int hashCode() { - return Objects.hash(joinTable, joinColumn, parentId); + Join { + Assert.isTrue(joinColumns.size() == parentId.size(), + "Both sides of a join condition must have the same number of columns"); } - @Override - public String toString() { - - return "Join{" + "joinTable=" + joinTable + ", joinColumn=" + joinColumn + ", parentId=" + parentId + '}'; - } } } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/SqlContext.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/SqlContext.java index 4d34666631..ab6db1ab51 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/SqlContext.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/SqlContext.java @@ -16,13 +16,12 @@ package org.springframework.data.jdbc.repository.query; import org.springframework.data.relational.core.mapping.AggregatePath; -import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.sql.Column; import org.springframework.data.relational.core.sql.SqlIdentifier; import org.springframework.data.relational.core.sql.Table; /** - * Utility to get from path to SQL DSL elements. This is a temporary class and duplicates + * Utility to get from path to SQL DSL elements. This is a temporary class and duplicates parts of * {@link org.springframework.data.jdbc.core.convert.SqlContext}. * * @author Jens Schauder @@ -32,42 +31,29 @@ */ class SqlContext { - private final RelationalPersistentEntity entity; - private final Table table; - - SqlContext(RelationalPersistentEntity entity) { - - this.entity = entity; - this.table = Table.create(entity.getQualifiedTableName()); - } + Table getTable(AggregatePath path) { - Column getIdColumn() { - return table.column(entity.getIdColumn()); + Table table = getUnaliasedTable(path); + AggregatePath.TableInfo tableInfo = path.getTableInfo(); + SqlIdentifier tableAlias = tableInfo.tableAlias(); + return tableAlias == null ? table : table.as(tableAlias); } - Column getVersionColumn() { - return table.column(entity.getRequiredVersionProperty().getColumnName()); - } + Column getColumn(AggregatePath path) { - Table getTable() { - return table; + AggregatePath.ColumnInfo columnInfo = path.getColumnInfo(); + return getTable(path).column(columnInfo.name()).as(columnInfo.alias()); } - Table getTable(AggregatePath path) { + Column getAnyReverseColumn(AggregatePath path) { - SqlIdentifier tableAlias = path.getTableInfo().tableAlias(); - Table table = Table.create(path.getTableInfo().qualifiedTableName()); - return tableAlias == null ? table : table.as(tableAlias); + AggregatePath.ColumnInfo anyReverseColumnInfo = path.getTableInfo().reverseColumnInfos().any(); + return getTable(path).column(anyReverseColumnInfo.name()).as(anyReverseColumnInfo.alias()); } - Column getColumn(AggregatePath path) { - AggregatePath.ColumnInfo columnInfo = path.getColumnInfo(); - AggregatePath.ColumnInfo columnInfo1 = path.getColumnInfo(); - return getTable(path).column(columnInfo1.name()).as(columnInfo.alias()); - } + public Table getUnaliasedTable(AggregatePath path) { - Column getReverseColumn(AggregatePath path) { - return getTable(path).column(path.getTableInfo().reverseColumnInfo().name()) - .as(path.getTableInfo().reverseColumnInfo().alias()); + AggregatePath.TableInfo tableInfo = path.getTableInfo(); + return Table.create(tableInfo.qualifiedTableName()); } } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/CompositeIdAggregateTemplateHsqlIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/CompositeIdAggregateTemplateHsqlIntegrationTests.java new file mode 100644 index 0000000000..dc10524878 --- /dev/null +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/CompositeIdAggregateTemplateHsqlIntegrationTests.java @@ -0,0 +1,258 @@ +/* + * Copyright 2017-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.jdbc.core; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.data.annotation.Id; +import org.springframework.data.jdbc.core.convert.DataAccessStrategy; +import org.springframework.data.jdbc.core.convert.JdbcConverter; +import org.springframework.data.jdbc.testing.DatabaseType; +import org.springframework.data.jdbc.testing.EnabledOnDatabase; +import org.springframework.data.jdbc.testing.IntegrationTest; +import org.springframework.data.jdbc.testing.TestConfiguration; +import org.springframework.data.relational.core.mapping.Embedded; +import org.springframework.data.relational.core.mapping.RelationalMappingContext; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; + +/** + * Integration tests for {@link JdbcAggregateTemplate} and it's handling of entities with embedded entities as keys. + * + * @author Jens Schauder + */ +@IntegrationTest +@EnabledOnDatabase(DatabaseType.HSQL) +public class CompositeIdAggregateTemplateHsqlIntegrationTests { + + @Autowired JdbcAggregateOperations template; + @Autowired private NamedParameterJdbcOperations namedParameterJdbcTemplate; + + @Test // GH-574 + void saveAndLoadSimpleEntity() { + + SimpleEntity entity = template.insert(new SimpleEntity(new WrappedPk(23L), "alpha")); + + assertThat(entity.wrappedPk).isNotNull() // + .extracting(WrappedPk::id).isNotNull(); + + SimpleEntity reloaded = template.findById(entity.wrappedPk, SimpleEntity.class); + + assertThat(reloaded).isEqualTo(entity); + } + + @Test // GH-574 + void saveAndLoadEntityWithList() { + + WithList entity = template + .insert(new WithList(new WrappedPk(23L), "alpha", List.of(new Child("Romulus"), new Child("Remus")))); + + assertThat(entity.wrappedPk).isNotNull() // + .extracting(WrappedPk::id).isNotNull(); + + WithList reloaded = template.findById(entity.wrappedPk, WithList.class); + + assertThat(reloaded).isEqualTo(entity); + } + + @Test // GH-574 + void saveAndLoadSimpleEntityWithEmbeddedPk() { + + SimpleEntityWithEmbeddedPk entity = template + .insert(new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "x"), "alpha")); + + SimpleEntityWithEmbeddedPk reloaded = template.findById(entity.embeddedPk, SimpleEntityWithEmbeddedPk.class); + + assertThat(reloaded).isEqualTo(entity); + } + + @Test // GH-574 + void saveAndLoadSimpleEntitiesWithEmbeddedPk() { + + List entities = (List) template + .insertAll(List.of(new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "x"), "alpha"), + new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "y"), "beta"), + new SimpleEntityWithEmbeddedPk(new EmbeddedPk(24L, "y"), "gamma"))); + + List firstTwoPks = entities.stream().limit(2).map(SimpleEntityWithEmbeddedPk::embeddedPk).toList(); + Iterable reloaded = template.findAllById(firstTwoPks, SimpleEntityWithEmbeddedPk.class); + + assertThat(reloaded).containsExactlyInAnyOrder(entities.get(0), entities.get(1)); + } + + @Test // GH-574 + void deleteSingleSimpleEntityWithEmbeddedPk() { + + List entities = (List) template + .insertAll(List.of(new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "x"), "alpha"), + new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "y"), "beta"), + new SimpleEntityWithEmbeddedPk(new EmbeddedPk(24L, "y"), "gamma"))); + + template.delete(entities.get(1)); + + Iterable reloaded = template.findAll(SimpleEntityWithEmbeddedPk.class); + + assertThat(reloaded).containsExactlyInAnyOrder(entities.get(0), entities.get(2)); + } + + @Test // GH-574 + void deleteMultipleSimpleEntityWithEmbeddedPk() { + + List entities = (List) template + .insertAll(List.of(new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "x"), "alpha"), + new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "y"), "beta"), + new SimpleEntityWithEmbeddedPk(new EmbeddedPk(24L, "y"), "gamma"))); + + template.deleteAll(List.of(entities.get(1), entities.get(0))); + + Iterable reloaded = template.findAll(SimpleEntityWithEmbeddedPk.class); + + assertThat(reloaded).containsExactly(entities.get(2)); + } + + @Test // GH-574 + void existsSingleSimpleEntityWithEmbeddedPk() { + + List entities = (List) template + .insertAll(List.of(new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "x"), "alpha"), + new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "y"), "beta"), + new SimpleEntityWithEmbeddedPk(new EmbeddedPk(24L, "y"), "gamma"))); + + assertThat(template.existsById(entities.get(1).embeddedPk, SimpleEntityWithEmbeddedPk.class)).isTrue(); + assertThat(template.existsById(new EmbeddedPk(24L, "x"), SimpleEntityWithEmbeddedPk.class)).isFalse(); + + } + + @Test // GH-574 + void updateSingleSimpleEntityWithEmbeddedPk() { + + List entities = (List) template + .insertAll(List.of(new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "x"), "alpha"), + new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "y"), "beta"), + new SimpleEntityWithEmbeddedPk(new EmbeddedPk(24L, "y"), "gamma"))); + + SimpleEntityWithEmbeddedPk updated = new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "x"), "ALPHA"); + template.save(updated); + + Iterable reloaded = template.findAll(SimpleEntityWithEmbeddedPk.class); + + assertThat(reloaded).containsExactlyInAnyOrder(updated, entities.get(1), entities.get(2)); + } + + @Test // GH-574 + void saveAndLoadSingleReferenceAggregate() { + + SingleReference entity = template.insert(new SingleReference(new EmbeddedPk(23L, "x"), "alpha", new Child("Alf"))); + + SingleReference reloaded = template.findById(entity.embeddedPk, SingleReference.class); + + assertThat(reloaded).isEqualTo(entity); + } + + @Test // GH-574 + void updateSingleReferenceAggregate() { + + EmbeddedPk id = new EmbeddedPk(23L, "x"); + template.insert(new SingleReference(id, "alpha", new Child("Alf"))); + + SingleReference updated = new SingleReference(id, "beta", new Child("Barny")); + template.save(updated); + + List all = template.findAll(SingleReference.class); + + assertThat(all).containsExactly(updated); + } + + @Test // GH-574 + void saveAndLoadWithListAndCompositeId() { + + WithListAndCompositeId entity = template.insert( // + new WithListAndCompositeId( // + new EmbeddedPk(23L, "x"), "alpha", // + List.of( // + new Child("Alf"), // + new Child("Bob"), // + new Child("Flo") // + ) // + ) // + ); + + WithListAndCompositeId reloaded = template.findById(entity.embeddedPk, WithListAndCompositeId.class); + + assertThat(reloaded).isEqualTo(entity); + } + + private record WrappedPk(Long id) { + } + + private record SimpleEntity( // + @Id @Embedded(onEmpty = Embedded.OnEmpty.USE_NULL) WrappedPk wrappedPk, // + String name // + ) { + } + + private record Child(String name) { + } + + private record WithList( // + @Id @Embedded(onEmpty = Embedded.OnEmpty.USE_NULL) WrappedPk wrappedPk, // + String name, List children) { + } + + private record EmbeddedPk(Long one, String two) { + } + + private record SimpleEntityWithEmbeddedPk( // + @Id @Embedded(onEmpty = Embedded.OnEmpty.USE_NULL) EmbeddedPk embeddedPk, // + String name // + ) { + } + + private record SingleReference( // + @Id @Embedded(onEmpty = Embedded.OnEmpty.USE_NULL) EmbeddedPk embeddedPk, // + String name, // + Child child) { + } + + private record WithListAndCompositeId( // + @Id @Embedded(onEmpty = Embedded.OnEmpty.USE_NULL) EmbeddedPk embeddedPk, // + String name, // + List child) { + } + + @Configuration + @Import(TestConfiguration.class) + static class Config { + + @Bean + Class testClass() { + return CompositeIdAggregateTemplateHsqlIntegrationTests.class; + } + + @Bean + JdbcAggregateOperations operations(ApplicationEventPublisher publisher, RelationalMappingContext context, + DataAccessStrategy dataAccessStrategy, JdbcConverter converter) { + return new JdbcAggregateTemplate(publisher, context, converter, dataAccessStrategy); + } + } +} diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilderUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilderUnitTests.java index 5873ce23a1..b5ec5ece9f 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilderUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilderUnitTests.java @@ -22,11 +22,13 @@ import java.util.Map; import java.util.UUID; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.core.PersistentPropertyPathTestUtils; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.relational.core.mapping.AggregatePath; +import org.springframework.data.relational.core.mapping.Embedded; /** * Unit tests for the {@link JdbcIdentifierBuilder}. @@ -40,90 +42,144 @@ public class JdbcIdentifierBuilderUnitTests { throw new UnsupportedOperationException(); }); - @Test // DATAJDBC-326 - public void parametersWithPropertyKeysUseTheParentPropertyJdbcType() { - - Identifier identifier = JdbcIdentifierBuilder.forBackReferences(converter, getPath("child"), "eins").build(); - - assertThat(identifier.getParts()) // - .extracting("name", "value", "targetType") // - .containsExactly( // - tuple(quoted("DUMMY_ENTITY"), "eins", UUID.class) // - ); + @Nested + class WithSimpleId { + @Test // DATAJDBC-326 + void parametersWithPropertyKeysUseTheParentPropertyJdbcType() { + + Identifier identifier = JdbcIdentifierBuilder.forBackReferences(converter, getPath("child"), "eins").build(); + + assertThat(identifier.getParts()) // + .extracting("name", "value", "targetType") // + .containsExactly( // + tuple(quoted("DUMMY_ENTITY"), "eins", UUID.class) // + ); + } + + @Test // DATAJDBC-326 + void qualifiersForMaps() { + + AggregatePath path = getPath("children"); + + Identifier identifier = JdbcIdentifierBuilder // + .forBackReferences(converter, path, "parent-eins") // + .withQualifier(path, "map-key-eins") // + .build(); + + assertThat(identifier.getParts()) // + .extracting("name", "value", "targetType") // + .containsExactlyInAnyOrder( // + tuple(quoted("DUMMY_ENTITY"), "parent-eins", UUID.class), // + tuple(quoted("DUMMY_ENTITY_KEY"), "map-key-eins", String.class) // + ); + } + + @Test // DATAJDBC-326 + void qualifiersForLists() { + + AggregatePath path = getPath("moreChildren"); + + Identifier identifier = JdbcIdentifierBuilder // + .forBackReferences(converter, path, "parent-eins") // + .withQualifier(path, "list-index-eins") // + .build(); + + assertThat(identifier.getParts()) // + .extracting("name", "value", "targetType") // + .containsExactlyInAnyOrder( // + tuple(quoted("DUMMY_ENTITY"), "parent-eins", UUID.class), // + tuple(quoted("DUMMY_ENTITY_KEY"), "list-index-eins", Integer.class) // + ); + } + + @Test // DATAJDBC-326 + void backreferenceAcrossEmbeddable() { + + Identifier identifier = JdbcIdentifierBuilder // + .forBackReferences(converter, getPath("embeddable.child"), "parent-eins") // + .build(); + + assertThat(identifier.getParts()) // + .extracting("name", "value", "targetType") // + .containsExactly( // + tuple(quoted("DUMMY_ENTITY"), "parent-eins", UUID.class) // + ); + } + + @Test // DATAJDBC-326 + void backreferenceAcrossNoId() { + + Identifier identifier = JdbcIdentifierBuilder // + .forBackReferences(converter, getPath("noId.child"), "parent-eins") // + .build(); + + assertThat(identifier.getParts()) // + .extracting("name", "value", "targetType") // + .containsExactly( // + tuple(quoted("DUMMY_ENTITY"), "parent-eins", UUID.class) // + ); + } + + private AggregatePath getPath(String dotPath) { + return JdbcIdentifierBuilderUnitTests.this.getPath(dotPath, DummyEntity.class); + } } - @Test // DATAJDBC-326 - public void qualifiersForMaps() { + @Nested + class WithCompositeId { - AggregatePath path = getPath("children"); + CompositeId exampleId = new CompositeId("parent-eins", 23); - Identifier identifier = JdbcIdentifierBuilder // - .forBackReferences(converter, path, "parent-eins") // - .withQualifier(path, "map-key-eins") // - .build(); + @Test // GH-574 + void forBackReferences() { - assertThat(identifier.getParts()) // - .extracting("name", "value", "targetType") // - .containsExactlyInAnyOrder( // - tuple(quoted("DUMMY_ENTITY"), "parent-eins", UUID.class), // - tuple(quoted("DUMMY_ENTITY_KEY"), "map-key-eins", String.class) // - ); - } + AggregatePath path = getPath("children"); - @Test // DATAJDBC-326 - public void qualifiersForLists() { + Identifier identifier = JdbcIdentifierBuilder // + .forBackReferences(converter, path, exampleId) // + .build(); - AggregatePath path = getPath("moreChildren"); + assertThat(identifier.getParts()) // + .extracting("name", "value", "targetType") // + .containsExactlyInAnyOrder( // + tuple(quoted("DUMMY_ENTITY_WITH_COMPOSITE_ID_ONE"), exampleId.one, String.class), // + tuple(quoted("DUMMY_ENTITY_WITH_COMPOSITE_ID_TWO"), exampleId.two, Integer.class) // + ); + } - Identifier identifier = JdbcIdentifierBuilder // - .forBackReferences(converter, path, "parent-eins") // - .withQualifier(path, "list-index-eins") // - .build(); + private AggregatePath getPath(String dotPath) { + return JdbcIdentifierBuilderUnitTests.this.getPath(dotPath, DummyEntityWithCompositeId.class); + } + } - assertThat(identifier.getParts()) // - .extracting("name", "value", "targetType") // - .containsExactlyInAnyOrder( // - tuple(quoted("DUMMY_ENTITY"), "parent-eins", UUID.class), // - tuple(quoted("DUMMY_ENTITY_KEY"), "list-index-eins", Integer.class) // - ); + private AggregatePath getPath(String dotPath, Class entityType) { + return context.getAggregatePath(PersistentPropertyPathTestUtils.getPath(dotPath, entityType, context)); } - @Test // DATAJDBC-326 - public void backreferenceAcrossEmbeddable() { + @SuppressWarnings("unused") + static class DummyEntity { - Identifier identifier = JdbcIdentifierBuilder // - .forBackReferences(converter, getPath("embeddable.child"), "parent-eins") // - .build(); + @Id UUID id; + String one; + Long two; + Child child; - assertThat(identifier.getParts()) // - .extracting("name", "value", "targetType") // - .containsExactly( // - tuple(quoted("DUMMY_ENTITY"), "parent-eins", UUID.class) // - ); - } + Map children; - @Test // DATAJDBC-326 - public void backreferenceAcrossNoId() { + List moreChildren; - Identifier identifier = JdbcIdentifierBuilder // - .forBackReferences(converter, getPath("noId.child"), "parent-eins") // - .build(); + Embeddable embeddable; - assertThat(identifier.getParts()) // - .extracting("name", "value", "targetType") // - .containsExactly( // - tuple(quoted("DUMMY_ENTITY"), "parent-eins", UUID.class) // - ); + NoId noId; } - private AggregatePath getPath(String dotPath) { - return context.getAggregatePath(PersistentPropertyPathTestUtils.getPath(dotPath, DummyEntity.class, context)); + record CompositeId(String one, Integer two) { } - @SuppressWarnings("unused") - static class DummyEntity { + static class DummyEntityWithCompositeId { - @Id UUID id; + @Embedded(onEmpty = Embedded.OnEmpty.USE_NULL) + @Id CompositeId id; String one; Long two; Child child; diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorContextBasedNamingStrategyUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorContextBasedNamingStrategyUnitTests.java index 745698211b..f25f421c30 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorContextBasedNamingStrategyUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorContextBasedNamingStrategyUnitTests.java @@ -89,7 +89,7 @@ public void cascadingDeleteFirstLevel() { assertThat(sql).isEqualTo( // "DELETE FROM " // + user + ".referenced_entity WHERE " // - + user + ".referenced_entity.dummy_entity = :rootId" // + + user + ".referenced_entity.dummy_entity = :id" // ); }); } @@ -107,7 +107,7 @@ public void cascadingDeleteAllSecondLevel() { "DELETE FROM " + user + ".second_level_referenced_entity " // + "WHERE " + user + ".second_level_referenced_entity.referenced_entity IN " // + "(SELECT " + user + ".referenced_entity.l1id FROM " + user + ".referenced_entity " // - + "WHERE " + user + ".referenced_entity.dummy_entity = :rootId)"); + + "WHERE " + user + ".referenced_entity.dummy_entity = :id)"); }); } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorEmbeddedUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorEmbeddedUnitTests.java index 7c510617b2..a6a7c63bae 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorEmbeddedUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorEmbeddedUnitTests.java @@ -20,17 +20,18 @@ import static org.assertj.core.api.SoftAssertions.*; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.core.PersistentPropertyPathTestUtils; import org.springframework.data.jdbc.core.mapping.AggregateReference; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; +import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.Embedded; import org.springframework.data.relational.core.mapping.Embedded.OnEmpty; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; +import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.core.mapping.Table; import org.springframework.data.relational.core.sql.Aliased; import org.springframework.data.relational.core.sql.SqlIdentifier; @@ -41,6 +42,7 @@ * * @author Bastian Wilhelm * @author Mark Paluch + * @author Jens Schauder */ class SqlGeneratorEmbeddedUnitTests { @@ -84,6 +86,139 @@ void findOne() { }); } + @Test // GH-574 + void findOneWrappedId() { + + SqlGenerator sqlGenerator = createSqlGenerator(DummyEntityWithWrappedId.class); + + String sql = sqlGenerator.getFindOne(); + + assertSoftly(softly -> { + + softly.assertThat(sql).startsWith("SELECT") // + .contains("dummy_entity_with_wrapped_id.name AS name") // + .contains("dummy_entity_with_wrapped_id.id") // + .contains("WHERE dummy_entity_with_wrapped_id.id = :id"); + }); + } + + @Test // GH-574 + void findOneEmbeddedId() { + + SqlGenerator sqlGenerator = createSqlGenerator(DummyEntityWithEmbeddedId.class); + + String sql = sqlGenerator.getFindOne(); + + assertSoftly(softly -> { + + softly.assertThat(sql).startsWith("SELECT") // + .contains("dummy_entity_with_embedded_id.name AS name") // + .contains("dummy_entity_with_embedded_id.one") // + .contains("dummy_entity_with_embedded_id.two") // + .contains(" WHERE ") // + .contains("dummy_entity_with_embedded_id.one = :one") // + .contains("dummy_entity_with_embedded_id.two = :two"); + }); + } + + @Test // GH-574 + void deleteByIdEmbeddedId() { + + SqlGenerator sqlGenerator = createSqlGenerator(DummyEntityWithEmbeddedId.class); + + String sql = sqlGenerator.getDeleteById(); + + assertSoftly(softly -> { + + softly.assertThat(sql).startsWith("DELETE") // + .contains(" WHERE ") // + .contains("dummy_entity_with_embedded_id.one = :one") // + .contains("dummy_entity_with_embedded_id.two = :two"); + }); + } + + @Test // GH-574 + void deleteByIdInEmbeddedId() { + + SqlGenerator sqlGenerator = createSqlGenerator(DummyEntityWithEmbeddedId.class); + + String sql = sqlGenerator.getDeleteByIdIn(); + + assertSoftly(softly -> { + + softly.assertThat(sql).startsWith("DELETE") // + .contains(" WHERE ") // + .contains("(dummy_entity_with_embedded_id.one, dummy_entity_with_embedded_id.two) IN (:ids)"); + }); + } + + @Test // GH-574 + void deleteByPathEmbeddedId() { + + SqlGenerator sqlGenerator = createSqlGenerator(DummyEntityWithEmbeddedId.class); + PersistentPropertyPath path = PersistentPropertyPathTestUtils.getPath("other", + DummyEntityWithEmbeddedIdAndReference.class, context); + + String sql = sqlGenerator.createDeleteByPath(path); + + assertSoftly(softly -> { + + softly.assertThat(sql).startsWith("DELETE FROM other_entity WHERE") // + .contains("other_entity.dummy_entity_with_embedded_id_and_reference_one = :one") // + .contains("other_entity.dummy_entity_with_embedded_id_and_reference_two = :two"); + }); + } + + @Test // GH-574 + void deleteInByPathEmbeddedId() { + + SqlGenerator sqlGenerator = createSqlGenerator(DummyEntityWithEmbeddedId.class); + PersistentPropertyPath path = PersistentPropertyPathTestUtils.getPath("other", + DummyEntityWithEmbeddedIdAndReference.class, context); + + String sql = sqlGenerator.createDeleteInByPath(path); + + assertSoftly(softly -> { + + softly.assertThat(sql).startsWith("DELETE FROM other_entity WHERE") // + .contains(" WHERE ") // + .contains( + "(other_entity.dummy_entity_with_embedded_id_and_reference_one, other_entity.dummy_entity_with_embedded_id_and_reference_two) IN (:ids)"); + }); + } + + @Test // GH-574 + void updateWithEmbeddedId() { + + SqlGenerator sqlGenerator = createSqlGenerator(DummyEntityWithEmbeddedId.class); + + String sql = sqlGenerator.getUpdate(); + + assertSoftly(softly -> { + + softly.assertThat(sql).startsWith("UPDATE") // + .contains(" WHERE ") // + .contains("dummy_entity_with_embedded_id.one = :one") // + .contains("dummy_entity_with_embedded_id.two = :two"); + }); + } + + @Test // GH-574 + void existsByIdEmbeddedId() { + + SqlGenerator sqlGenerator = createSqlGenerator(DummyEntityWithEmbeddedId.class); + + String sql = sqlGenerator.getExists(); + + assertSoftly(softly -> { + + softly.assertThat(sql).startsWith("SELECT COUNT") // + .contains(" WHERE ") // + .contains("dummy_entity_with_embedded_id.one = :one") // + .contains("dummy_entity_with_embedded_id.two = :two"); + }); + } + @Test // DATAJDBC-111 void findAll() { final String sql = sqlGenerator.getFindAll(); @@ -109,7 +244,8 @@ void findAll() { @Test // DATAJDBC-111 void findAllInList() { - final String sql = sqlGenerator.getFindAllInList(); + + String sql = sqlGenerator.getFindAllInList(); assertSoftly(softly -> { @@ -130,6 +266,45 @@ void findAllInList() { }); } + @Test // GH-574 + void findAllInListEmbeddedId() { + + SqlGenerator sqlGenerator = createSqlGenerator(DummyEntityWithEmbeddedId.class); + + String sql = sqlGenerator.getFindAllInList(); + + assertSoftly(softly -> { + + softly.assertThat(sql).startsWith("SELECT") // + .contains("dummy_entity_with_embedded_id.name AS name") // + .contains("dummy_entity_with_embedded_id.one") // + .contains("dummy_entity_with_embedded_id.two") // + .contains(" WHERE (dummy_entity_with_embedded_id.one, dummy_entity_with_embedded_id.two) IN (:ids)"); + }); + } + + @Test // GH-574 + void findOneWithReference() { + + SqlGenerator sqlGenerator = createSqlGenerator(DummyEntityWithEmbeddedIdAndReference.class); + + String sql = sqlGenerator.getFindOne(); + + assertSoftly(softly -> { + + softly.assertThat(sql).startsWith("SELECT") // + .contains(" LEFT OUTER JOIN other_entity other ") // + .contains(" ON ") // + .contains( + " other.dummy_entity_with_embedded_id_and_reference_one = dummy_entity_with_embedded_id_and_reference.one ") // + .contains( + " other.dummy_entity_with_embedded_id_and_reference_two = dummy_entity_with_embedded_id_and_reference.two ") // + .contains(" WHERE ") // + .contains("dummy_entity_with_embedded_id_and_reference.one = :one") // + .contains("dummy_entity_with_embedded_id_and_reference.two = :two"); + }); + } + @Test // DATAJDBC-111 void insert() { final String sql = sqlGenerator.getInsert(emptySet()); @@ -175,21 +350,14 @@ void update() { } @Test // DATAJDBC-340 - @Disabled // this is just broken right now void deleteByPath() { + sqlGenerator = createSqlGenerator(DummyEntity2.class); + final String sql = sqlGenerator .createDeleteByPath(PersistentPropertyPathTestUtils.getPath("embedded.other", DummyEntity2.class, context)); - assertThat(sql).containsSequence("DELETE FROM other_entity", // - "WHERE", // - "embedded_with_reference IN (", // - "SELECT ", // - "id ", // - "FROM", // - "dummy_entity2", // - "WHERE", // - "embedded_with_reference = :rootId"); + assertThat(sql).isEqualTo("DELETE FROM other_entity WHERE other_entity.dummy_entity2 = :id"); } @Test // DATAJDBC-340 @@ -276,12 +444,18 @@ void joinForEmbeddedWithReference() { SqlGenerator.Join join = generateJoin("embedded.other", DummyEntity2.class); assertSoftly(softly -> { - - softly.assertThat(join.getJoinTable().getName()).isEqualTo(SqlIdentifier.unquoted("other_entity")); - softly.assertThat(join.getJoinColumn().getTable()).isEqualTo(join.getJoinTable()); - softly.assertThat(join.getJoinColumn().getName()).isEqualTo(SqlIdentifier.unquoted("dummy_entity2")); - softly.assertThat(join.getParentId().getName()).isEqualTo(SqlIdentifier.unquoted("id")); - softly.assertThat(join.getParentId().getTable().getName()).isEqualTo(SqlIdentifier.unquoted("dummy_entity2")); + softly.assertThat(join.joinTable().getName()).isEqualTo(SqlIdentifier.unquoted("other_entity")); + softly.assertThat(join.columns()).extracting( // + pair -> pair.getFirst().getTable(), // + pair -> pair.getFirst().getName(), // + pair -> pair.getSecond().getTable().getName(), // + pair -> pair.getSecond().getName() // + ).contains(tuple( // + join.joinTable(), // + SqlIdentifier.unquoted("dummy_entity2"), // + SqlIdentifier.unquoted("dummy_entity2"), // + SqlIdentifier.unquoted("id") // + )); }); } @@ -301,6 +475,7 @@ void columnForEmbeddedWithReferenceProperty() { SqlIdentifier.unquoted("prefix_other_value")); } + @Nullable private SqlGenerator.Join generateJoin(String path, Class type) { return createSqlGenerator(type) .getJoin(context.getAggregatePath(PersistentPropertyPathTestUtils.getPath(path, type, context))); @@ -315,6 +490,7 @@ private SqlIdentifier getAlias(Object maybeAliased) { return null; } + @Nullable private org.springframework.data.relational.core.sql.Column generatedColumn(String path, Class type) { return createSqlGenerator(type) @@ -332,15 +508,47 @@ static class DummyEntity { @Embedded(onEmpty = OnEmpty.USE_NULL) CascadedEmbedded embeddable; } + record WrappedId(Long id) { + } + + static class DummyEntityWithWrappedId { + + @Id + @Embedded(onEmpty = OnEmpty.USE_NULL) WrappedId wrappedId; + + String name; + } + + record EmbeddedId(Long one, String two) { + } + + static class DummyEntityWithEmbeddedId { + + @Id + @Embedded(onEmpty = OnEmpty.USE_NULL) EmbeddedId embeddedId; + + String name; + + } + + static class DummyEntityWithEmbeddedIdAndReference { + + @Id + @Embedded(onEmpty = OnEmpty.USE_NULL) EmbeddedId embeddedId; + + String name; + OtherEntity other; + } + @SuppressWarnings("unused") static class CascadedEmbedded { String test; - @Embedded(onEmpty = OnEmpty.USE_NULL, prefix = "prefix2_") Embeddable prefixedEmbeddable; - @Embedded(onEmpty = OnEmpty.USE_NULL) Embeddable embeddable; + @Embedded(onEmpty = OnEmpty.USE_NULL, prefix = "prefix2_") NoId prefixedEmbeddable; + @Embedded(onEmpty = OnEmpty.USE_NULL) NoId embeddable; } @SuppressWarnings("unused") - static class Embeddable { + static class NoId { Long attr1; String attr2; } @@ -362,8 +570,7 @@ static class OtherEntity { } @Table("a") - private - record WithEmbeddedAndAggregateReference(@Id long id, + private record WithEmbeddedAndAggregateReference(@Id long id, @Embedded.Nullable(prefix = "nested_") WithAggregateReference nested) { } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorFixedNamingStrategyUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorFixedNamingStrategyUnitTests.java index 502b310b52..5ecbdd9cc8 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorFixedNamingStrategyUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorFixedNamingStrategyUnitTests.java @@ -30,11 +30,12 @@ import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; /** - * Unit tests the {@link SqlGenerator} with a fixed {@link NamingStrategy} implementation containing a hard wired + * Unit tests the {@link SqlGenerator} with a fixed {@link NamingStrategy} implementation containing a hard-wired * schema, table, and property prefix. * * @author Greg Turnquist * @author Mark Paluch + * @author Jens Schauder */ class SqlGeneratorFixedNamingStrategyUnitTests { @@ -90,7 +91,7 @@ void findOneWithOverriddenFixedTableName() { + "FROM \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_DUMMYENTITY\" " + "LEFT OUTER JOIN \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_REFERENCEDENTITY\" \"ref\" ON \"ref\".\"FIXEDCUSTOMTABLEPREFIX_DUMMYENTITY\" = \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_DUMMYENTITY\".\"FIXEDCUSTOMPROPERTYPREFIX_ID\" L" + "EFT OUTER JOIN \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_SECONDLEVELREFERENCEDENTITY\" \"ref_further\" ON \"ref_further\".\"FIXEDCUSTOMTABLEPREFIX_REFERENCEDENTITY\" = \"ref\".\"FIXEDCUSTOMPROPERTYPREFIX_L1ID\" " - + "WHERE \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_DUMMYENTITY\".\"FIXEDCUSTOMPROPERTYPREFIX_ID\" = :id"); + + "WHERE \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_DUMMYENTITY\".\"FIXEDCUSTOMPROPERTYPREFIX_ID\" = :FixedCustomPropertyPrefix_id"); softAssertions.assertAll(); } @@ -121,7 +122,7 @@ void cascadingDeleteFirstLevel() { String sql = sqlGenerator.createDeleteByPath(getPath("ref")); assertThat(sql).isEqualTo("DELETE FROM \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_REFERENCEDENTITY\" " - + "WHERE \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_REFERENCEDENTITY\".\"FIXEDCUSTOMTABLEPREFIX_DUMMYENTITY\" = :rootId"); + + "WHERE \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_REFERENCEDENTITY\".\"FIXEDCUSTOMTABLEPREFIX_DUMMYENTITY\" = :FixedCustomPropertyPrefix_id"); } @Test // DATAJDBC-107 @@ -136,7 +137,7 @@ void cascadingDeleteAllSecondLevel() { + "WHERE \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_SECONDLEVELREFERENCEDENTITY\".\"FIXEDCUSTOMTABLEPREFIX_REFERENCEDENTITY\" IN " + "(SELECT \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_REFERENCEDENTITY\".\"FIXEDCUSTOMPROPERTYPREFIX_L1ID\" " + "FROM \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_REFERENCEDENTITY\" " - + "WHERE \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_REFERENCEDENTITY\".\"FIXEDCUSTOMTABLEPREFIX_DUMMYENTITY\" = :rootId)"); + + "WHERE \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_REFERENCEDENTITY\".\"FIXEDCUSTOMTABLEPREFIX_DUMMYENTITY\" = :FixedCustomPropertyPrefix_id)"); } @Test // DATAJDBC-107 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 d095b27ccb..b0fcc9b1a1 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 @@ -153,7 +153,7 @@ void cascadingDeleteFirstLevel() { String sql = sqlGenerator.createDeleteByPath(getPath("ref", DummyEntity.class)); - assertThat(sql).isEqualTo("DELETE FROM referenced_entity WHERE referenced_entity.dummy_entity = :rootId"); + assertThat(sql).isEqualTo("DELETE FROM referenced_entity WHERE referenced_entity.dummy_entity = :id1"); } @Test // GH-537 @@ -170,7 +170,7 @@ void cascadingDeleteByPathSecondLevel() { String sql = sqlGenerator.createDeleteByPath(getPath("ref.further", DummyEntity.class)); assertThat(sql).isEqualTo( - "DELETE FROM second_level_referenced_entity WHERE second_level_referenced_entity.referenced_entity IN (SELECT referenced_entity.x_l1id FROM referenced_entity WHERE referenced_entity.dummy_entity = :rootId)"); + "DELETE FROM second_level_referenced_entity WHERE second_level_referenced_entity.referenced_entity IN (SELECT referenced_entity.x_l1id FROM referenced_entity WHERE referenced_entity.dummy_entity = :id1)"); } @Test // GH-537 @@ -220,7 +220,7 @@ void deleteMapByPath() { String sql = sqlGenerator.createDeleteByPath(getPath("mappedElements", DummyEntity.class)); - assertThat(sql).isEqualTo("DELETE FROM element WHERE element.dummy_entity = :rootId"); + assertThat(sql).isEqualTo("DELETE FROM element WHERE element.dummy_entity = :id1"); } @Test // DATAJDBC-101 @@ -375,7 +375,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 @@ -654,7 +655,7 @@ void readOnlyPropertyIncludedIntoQuery_when_generateFindOneSql() { + "entity_with_read_only_property.x_name AS x_name, " // + "entity_with_read_only_property.x_read_only_value AS x_read_only_value " // + "FROM entity_with_read_only_property " // - + "WHERE entity_with_read_only_property.x_id = :id" // + + "WHERE entity_with_read_only_property.x_id = :x_id" // ); } @@ -673,7 +674,7 @@ void deletingLongChain() { "WHERE chain2.chain3 IN (" + // "SELECT chain3.x_three " + // "FROM chain3 " + // - "WHERE chain3.chain4 = :rootId" + // + "WHERE chain3.chain4 = :x_four" + // ")))"); } @@ -682,7 +683,7 @@ void deletingLongChainNoId() { assertThat(createSqlGenerator(NoIdChain4.class) .createDeleteByPath(getPath("chain3.chain2.chain1.chain0", NoIdChain4.class))) // - .isEqualTo("DELETE FROM no_id_chain0 WHERE no_id_chain0.no_id_chain4 = :rootId"); + .isEqualTo("DELETE FROM no_id_chain0 WHERE no_id_chain0.no_id_chain4 = :x_four"); } @Test // DATAJDBC-359 @@ -698,7 +699,7 @@ void deletingLongChainNoIdWithBackreferenceNotReferencingTheRoot() { + "WHERE no_id_chain4.id_no_id_chain IN (" // + "SELECT id_no_id_chain.x_id " // + "FROM id_no_id_chain " // - + "WHERE id_no_id_chain.id_id_no_id_chain = :rootId" // + + "WHERE id_no_id_chain.id_id_no_id_chain = :x_id" // + "))"); } @@ -714,11 +715,18 @@ void joinForSimpleReference() { assertSoftly(softly -> { - softly.assertThat(join.getJoinTable().getName()).isEqualTo(SqlIdentifier.quoted("REFERENCED_ENTITY")); - softly.assertThat(join.getJoinColumn().getTable()).isEqualTo(join.getJoinTable()); - softly.assertThat(join.getJoinColumn().getName()).isEqualTo(SqlIdentifier.quoted("DUMMY_ENTITY")); - softly.assertThat(join.getParentId().getName()).isEqualTo(SqlIdentifier.quoted("id1")); - softly.assertThat(join.getParentId().getTable().getName()).isEqualTo(SqlIdentifier.quoted("DUMMY_ENTITY")); + softly.assertThat(join.joinTable().getName()).isEqualTo(SqlIdentifier.quoted("REFERENCED_ENTITY")); + softly.assertThat(join.columns()).extracting( // + pair -> pair.getFirst().getTable(), // + pair -> pair.getFirst().getName(), // + pair -> pair.getSecond().getTable().getName(), // + pair -> pair.getSecond().getName() // + ).contains(tuple( // + join.joinTable(), // + SqlIdentifier.quoted("DUMMY_ENTITY"), // + SqlIdentifier.quoted("DUMMY_ENTITY"), // + SqlIdentifier.quoted("id1") // + )); }); } @@ -745,13 +753,18 @@ void joinForSecondLevelReference() { SqlGenerator.Join join = generateJoin("ref.further", DummyEntity.class); assertSoftly(softly -> { - - softly.assertThat(join.getJoinTable().getName()) - .isEqualTo(SqlIdentifier.quoted("SECOND_LEVEL_REFERENCED_ENTITY")); - softly.assertThat(join.getJoinColumn().getTable()).isEqualTo(join.getJoinTable()); - softly.assertThat(join.getJoinColumn().getName()).isEqualTo(SqlIdentifier.quoted("REFERENCED_ENTITY")); - softly.assertThat(join.getParentId().getName()).isEqualTo(SqlIdentifier.quoted("X_L1ID")); - softly.assertThat(join.getParentId().getTable().getName()).isEqualTo(SqlIdentifier.quoted("REFERENCED_ENTITY")); + softly.assertThat(join.joinTable().getName()).isEqualTo(SqlIdentifier.quoted("SECOND_LEVEL_REFERENCED_ENTITY")); + softly.assertThat(join.columns()).extracting( // + pair -> pair.getFirst().getTable(), // + pair -> pair.getFirst().getName(), // + pair -> pair.getSecond().getTable().getName(), // + pair -> pair.getSecond().getName() // + ).contains(tuple( // + join.joinTable(), // + SqlIdentifier.quoted("REFERENCED_ENTITY"), // + SqlIdentifier.quoted("REFERENCED_ENTITY"), // + SqlIdentifier.quoted("X_L1ID") // + )); }); } @@ -759,19 +772,25 @@ void joinForSecondLevelReference() { void joinForOneToOneWithoutId() { SqlGenerator.Join join = generateJoin("child", ParentOfNoIdChild.class); - Table joinTable = join.getJoinTable(); + Table joinTable = join.joinTable(); assertSoftly(softly -> { softly.assertThat(joinTable.getName()).isEqualTo(SqlIdentifier.quoted("NO_ID_CHILD")); softly.assertThat(joinTable).isInstanceOf(Aliased.class); softly.assertThat(((Aliased) joinTable).getAlias()).isEqualTo(SqlIdentifier.quoted("child")); - softly.assertThat(join.getJoinColumn().getTable()).isEqualTo(joinTable); - softly.assertThat(join.getJoinColumn().getName()).isEqualTo(SqlIdentifier.quoted("PARENT_OF_NO_ID_CHILD")); - softly.assertThat(join.getParentId().getName()).isEqualTo(SqlIdentifier.quoted("X_ID")); - softly.assertThat(join.getParentId().getTable().getName()) - .isEqualTo(SqlIdentifier.quoted("PARENT_OF_NO_ID_CHILD")); + softly.assertThat(join.columns()).extracting( // + pair -> pair.getFirst().getTable(), // + pair -> pair.getFirst().getName(), // + pair -> pair.getSecond().getTable().getName(), // + pair -> pair.getSecond().getName() // + ).contains(tuple( // + join.joinTable(), // + SqlIdentifier.quoted("PARENT_OF_NO_ID_CHILD"), // + SqlIdentifier.quoted("PARENT_OF_NO_ID_CHILD"), // + SqlIdentifier.quoted("X_ID") // + )); }); } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlParametersFactoryTest.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlParametersFactoryUnitTests.java similarity index 86% rename from spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlParametersFactoryTest.java rename to spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlParametersFactoryUnitTests.java index 9efdb3aeab..b7371c6a7f 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlParametersFactoryTest.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlParametersFactoryUnitTests.java @@ -26,6 +26,7 @@ import java.util.List; import java.util.Objects; +import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.Test; import org.springframework.core.convert.converter.Converter; import org.springframework.data.annotation.Id; @@ -34,6 +35,7 @@ import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.relational.core.conversion.IdValueSource; import org.springframework.data.relational.core.mapping.Column; +import org.springframework.data.relational.core.mapping.Embedded; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.sql.SqlIdentifier; import org.springframework.jdbc.core.JdbcOperations; @@ -43,7 +45,7 @@ * * @author Chirag Tailor */ -class SqlParametersFactoryTest { +class SqlParametersFactoryUnitTests { RelationalMappingContext context = new JdbcMappingContext(); RelationResolver relationResolver = mock(RelationResolver.class); @@ -51,20 +53,20 @@ class SqlParametersFactoryTest { SqlParametersFactory sqlParametersFactory = new SqlParametersFactory(context, converter); @Test // DATAJDBC-412 - public void considersConfiguredWriteConverterForIdValueObjects_onRead() { + void considersConfiguredWriteConverterForIdValueObjects_onRead() { SqlParametersFactory sqlParametersFactory = createSqlParametersFactoryWithConverters( singletonList(IdValueToStringConverter.INSTANCE)); String rawId = "batman"; SqlIdentifierParameterSource sqlParameterSource = sqlParametersFactory.forQueryById(new IdValue(rawId), - WithValueObjectId.class, SqlGenerator.ID_SQL_PARAMETER); + WithValueObjectId.class); assertThat(sqlParameterSource.getValue("id")).isEqualTo(rawId); } @Test // DATAJDBC-349 - public void considersConfiguredWriteConverterForIdValueObjectsWhichReferencedInOneToManyRelationship() { + void considersConfiguredWriteConverterForIdValueObjectsWhichReferencedInOneToManyRelationship() { SqlParametersFactory sqlParametersFactory = createSqlParametersFactoryWithConverters( singletonList(IdValueToStringConverter.INSTANCE)); @@ -85,8 +87,7 @@ public void considersConfiguredWriteConverterForIdValueObjectsWhichReferencedInO assertThat(sqlParameterSource.getValue("DUMMYENTITYROOT")).isEqualTo(rawId); } - @Test - // DATAJDBC-146 + @Test // DATAJDBC-146 void identifiersGetAddedAsParameters() { long id = 4711L; @@ -100,8 +101,7 @@ void identifiersGetAddedAsParameters() { assertThat(sqlParameterSource.getValue("reference")).isEqualTo(reference); } - @Test - // DATAJDBC-146 + @Test // DATAJDBC-146 void additionalIdentifierForIdDoesNotLeadToDuplicateParameters() { long id = 4711L; @@ -113,8 +113,7 @@ void additionalIdentifierForIdDoesNotLeadToDuplicateParameters() { assertThat(sqlParameterSource.getValue("id")).isEqualTo(id); } - @Test - // DATAJDBC-235 + @Test // DATAJDBC-235 void considersConfiguredWriteConverter() { SqlParametersFactory sqlParametersFactory = createSqlParametersFactoryWithConverters( @@ -128,8 +127,7 @@ void considersConfiguredWriteConverter() { assertThat(sqlParameterSource.getValue("flag")).isEqualTo("T"); } - @Test - // DATAJDBC-412 + @Test // DATAJDBC-412 void considersConfiguredWriteConverterForIdValueObjects_onWrite() { SqlParametersFactory sqlParametersFactory = createSqlParametersFactoryWithConverters( @@ -146,8 +144,7 @@ void considersConfiguredWriteConverterForIdValueObjects_onWrite() { assertThat(sqlParameterSource.getValue("value")).isEqualTo(value); } - @Test - // GH-1405 + @Test // GH-1405 void parameterNamesGetSanitized() { WithIllegalCharacters entity = new WithIllegalCharacters(23L, "aValue"); @@ -162,6 +159,22 @@ void parameterNamesGetSanitized() { assertThat(sqlParameterSource.getValue("val&ue")).isNull(); } + @Test // GH-574 + void parametersForInsertForEmbeddedWrappedId() { + + SingleEmbeddedIdEntity entity = new SingleEmbeddedIdEntity(new WrappedPk(23L), "alpha"); + + SqlIdentifierParameterSource parameterSource = sqlParametersFactory.forInsert(entity, SingleEmbeddedIdEntity.class, + Identifier.empty(), IdValueSource.PROVIDED); + + SoftAssertions.assertSoftly(softly -> { + + softly.assertThat(parameterSource.getParameterNames()).containsExactlyInAnyOrder("id", "name"); + softly.assertThat(parameterSource.getValue("id")).isEqualTo(23L); + softly.assertThat(parameterSource.getValue("name")).isEqualTo("alpha"); + }); + } + @WritingConverter enum IdValueToStringConverter implements Converter { @@ -299,6 +312,17 @@ private SqlParametersFactory createSqlParametersFactoryWithConverters(List co MappingJdbcConverter converter = new MappingJdbcConverter(context, relationResolver, new JdbcCustomConversions(converters), new DefaultJdbcTypeFactory(mock(JdbcOperations.class))); + context.setSimpleTypeHolder(converter.getConversions().getSimpleTypeHolder()); + return new SqlParametersFactory(context, converter); } + + private record WrappedPk(Long id) { + } + + private record SingleEmbeddedIdEntity( // + @Id @Embedded(onEmpty = Embedded.OnEmpty.USE_NULL) WrappedPk wrappedPk, // + String name // + ) { + } } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryEmbeddedWithCollectionIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryEmbeddedWithCollectionIntegrationTests.java index 4e566f054c..456d9fd0b0 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryEmbeddedWithCollectionIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryEmbeddedWithCollectionIntegrationTests.java @@ -18,7 +18,6 @@ import static java.util.Arrays.*; import static org.assertj.core.api.Assertions.*; -import java.sql.SQLException; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -68,7 +67,7 @@ DummyEntityRepository dummyEntityRepository(JdbcRepositoryFactory factory) { @Autowired Dialect dialect; @Test // DATAJDBC-111 - void savesAnEntity() throws SQLException { + void savesAnEntity() { DummyEntity entity = repository.save(createDummyEntity()); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java index 27f4a47c29..ea84b3852b 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java @@ -39,6 +39,7 @@ import org.springframework.data.relational.core.dialect.Escaper; import org.springframework.data.relational.core.dialect.H2Dialect; import org.springframework.data.relational.core.mapping.Embedded; +import org.springframework.data.relational.core.mapping.Embedded.Nullable; import org.springframework.data.relational.core.mapping.MappedCollection; import org.springframework.data.relational.core.mapping.Table; import org.springframework.data.relational.core.sql.LockMode; @@ -65,8 +66,8 @@ public class PartTreeJdbcQueryUnitTests { private static final String TABLE = "\"users\""; - private static final String ALL_FIELDS = "\"users\".\"ID\" AS \"ID\", \"users\".\"AGE\" AS \"AGE\", \"users\".\"ACTIVE\" AS \"ACTIVE\", \"users\".\"LAST_NAME\" AS \"LAST_NAME\", \"users\".\"FIRST_NAME\" AS \"FIRST_NAME\", \"users\".\"DATE_OF_BIRTH\" AS \"DATE_OF_BIRTH\", \"users\".\"HOBBY_REFERENCE\" AS \"HOBBY_REFERENCE\", \"hated\".\"NAME\" AS \"HATED_NAME\", \"users\".\"USER_CITY\" AS \"USER_CITY\", \"users\".\"USER_STREET\" AS \"USER_STREET\""; - private static final String JOIN_CLAUSE = "FROM \"users\" LEFT OUTER JOIN \"HOBBY\" \"hated\" ON \"hated\".\"USERS\" = \"users\".\"ID\""; + private static final String ALL_FIELDS = "\"users\".\"AGE\" AS \"AGE\", \"users\".\"ACTIVE\" AS \"ACTIVE\", \"users\".\"LAST_NAME\" AS \"LAST_NAME\", \"users\".\"FIRST_NAME\" AS \"FIRST_NAME\", \"users\".\"DATE_OF_BIRTH\" AS \"DATE_OF_BIRTH\", \"users\".\"HOBBY_REFERENCE\" AS \"HOBBY_REFERENCE\", \"users\".\"ID\" AS \"ID\", \"users\".\"SUB_ID\" AS \"SUB_ID\", \"hated\".\"NAME\" AS \"HATED_NAME\", \"users\".\"USER_CITY\" AS \"USER_CITY\", \"users\".\"USER_STREET\" AS \"USER_STREET\""; + private static final String JOIN_CLAUSE = "FROM \"users\" LEFT OUTER JOIN \"HOBBY\" \"hated\" ON \"hated\".\"USERS_ID\" = \"users\".\"ID\" AND \"hated\".\"USERS_SUB_ID\" = \"users\".\"SUB_ID\""; private static final String BASE_SELECT = "SELECT " + ALL_FIELDS + " " + JOIN_CLAUSE; JdbcMappingContext mappingContext = new JdbcMappingContext(); @@ -778,7 +779,8 @@ interface UserRepository extends Repository { @Table("users") static class User { - @Id Long id; + @Id + @Nullable UserId id; String firstName; String lastName; Date dateOfBirth; @@ -786,7 +788,7 @@ static class User { Boolean active; @Embedded(prefix = "user_", onEmpty = Embedded.OnEmpty.USE_NULL) Address address; - @Embedded.Nullable AnotherEmbedded anotherEmbedded; + @Nullable AnotherEmbedded anotherEmbedded; List hobbies; Hobby hated; @@ -794,6 +796,9 @@ static class User { AggregateReference hobbyReference; } + record UserId(Long id, String subId) { + } + record Address(String street, String city) { } diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/CompositeIdAggregateTemplateHsqlIntegrationTests-hsql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/CompositeIdAggregateTemplateHsqlIntegrationTests-hsql.sql new file mode 100644 index 0000000000..604cbefb2a --- /dev/null +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/CompositeIdAggregateTemplateHsqlIntegrationTests-hsql.sql @@ -0,0 +1,46 @@ +CREATE TABLE SIMPLE_ENTITY +( + ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, + NAME VARCHAR(100) +); + +CREATE TABLE WITH_LIST_AND_COMPOSITE_ID +( + ONE BIGINT, + TWO VARCHAR(100), + NAME VARCHAR(100), + PRIMARY KEY (ONE, TWO) +); +CREATE TABLE WITH_LIST +( + ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, + NAME VARCHAR(100) +); + +CREATE TABLE CHILD +( + WITH_LIST_ID BIGINT REFERENCES WITH_LIST (ID), + WITH_LIST_KEY INT, + WITH_LIST_AND_COMPOSITE_ID_ONE BIGINT, + WITH_LIST_AND_COMPOSITE_ID_TWO VARCHAR(100), + WITH_LIST_AND_COMPOSITE_ID_KEY INT, + NAME VARCHAR(100), + SINGLE_REFERENCE_ONE BIGINT, + SINGLE_REFERENCE_TWO VARCHAR(100) +); + +CREATE TABLE SIMPLE_ENTITY_WITH_EMBEDDED_PK +( + ONE BIGINT, + TWO VARCHAR(100), + NAME VARCHAR(100), + PRIMARY KEY (ONE, TWO) +); + +CREATE TABLE SINGLE_REFERENCE +( + ONE BIGINT, + TWO VARCHAR(100), + NAME VARCHAR(100), + PRIMARY KEY (ONE, TWO) +); 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 21e80a6c98..7ca796018c 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 @@ -1,328 +1,328 @@ CREATE TABLE LEGO_SET ( - "id1" BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, - NAME VARCHAR(30) + "id1" BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, + NAME VARCHAR(30) ); CREATE TABLE MANUAL ( - "id2" BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, - LEGO_SET BIGINT, - "alternative" BIGINT, - CONTENT VARCHAR(2000) + "id2" BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, + LEGO_SET BIGINT, + "alternative" BIGINT, + CONTENT VARCHAR(2000) ); ALTER TABLE MANUAL - ADD FOREIGN KEY (LEGO_SET) - REFERENCES LEGO_SET ("id1"); + ADD FOREIGN KEY (LEGO_SET) + REFERENCES LEGO_SET ("id1"); CREATE TABLE ONE_TO_ONE_PARENT ( - "id3" BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, - content VARCHAR(30) + "id3" BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, + content VARCHAR(30) ); CREATE TABLE Child_No_Id ( - ONE_TO_ONE_PARENT INTEGER PRIMARY KEY, - content VARCHAR(30) + ONE_TO_ONE_PARENT INTEGER PRIMARY KEY, + content VARCHAR(30) ); CREATE TABLE SIMPLE_LIST_PARENT ( - ID BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, - NAME VARCHAR(100) + ID BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, + NAME VARCHAR(100) ); CREATE TABLE LIST_PARENT ( - "id4" BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, - NAME VARCHAR(100) + "id4" BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, + NAME VARCHAR(100) ); CREATE TABLE ELEMENT_NO_ID ( - CONTENT VARCHAR(100), - SIMPLE_LIST_PARENT_KEY BIGINT, - SIMPLE_LIST_PARENT BIGINT, - LIST_PARENT_KEY BIGINT, - LIST_PARENT BIGINT + CONTENT VARCHAR(100), + SIMPLE_LIST_PARENT_KEY BIGINT, + SIMPLE_LIST_PARENT BIGINT, + LIST_PARENT_KEY BIGINT, + LIST_PARENT BIGINT ); ALTER TABLE ELEMENT_NO_ID - ADD FOREIGN KEY (LIST_PARENT) - REFERENCES LIST_PARENT ("id4"); + ADD FOREIGN KEY (LIST_PARENT) + REFERENCES LIST_PARENT ("id4"); CREATE TABLE ARRAY_OWNER ( - ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, - DIGITS VARCHAR(20) ARRAY[10] NOT NULL, - MULTIDIMENSIONAL VARCHAR(20) ARRAY[10] NULL + ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, + DIGITS VARCHAR(20) ARRAY[10] NOT NULL, + MULTIDIMENSIONAL VARCHAR(20) ARRAY[10] NULL ); CREATE TABLE BYTE_ARRAY_OWNER ( - ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, - BINARY_DATA VARBINARY(20) NOT NULL + ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, + BINARY_DATA VARBINARY(20) NOT NULL ); CREATE TABLE DOUBLE_LIST_OWNER ( - ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, - DIGITS DOUBLE PRECISION ARRAY[10] + ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, + DIGITS DOUBLE PRECISION ARRAY[10] ); CREATE TABLE FLOAT_LIST_OWNER ( - ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, - DIGITS FLOAT ARRAY[10] + ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, + DIGITS FLOAT ARRAY[10] ); CREATE TABLE CHAIN4 ( - FOUR BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 40) PRIMARY KEY, - FOUR_VALUE VARCHAR(20) + FOUR BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 40) PRIMARY KEY, + FOUR_VALUE VARCHAR(20) ); CREATE TABLE CHAIN3 ( - THREE BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 30) PRIMARY KEY, - THREE_VALUE VARCHAR(20), - CHAIN4 BIGINT, - FOREIGN KEY (CHAIN4) REFERENCES CHAIN4 (FOUR) + THREE BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 30) PRIMARY KEY, + THREE_VALUE VARCHAR(20), + CHAIN4 BIGINT, + FOREIGN KEY (CHAIN4) REFERENCES CHAIN4 (FOUR) ); CREATE TABLE CHAIN2 ( - TWO BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 20) PRIMARY KEY, - TWO_VALUE VARCHAR(20), - CHAIN3 BIGINT, - FOREIGN KEY (CHAIN3) REFERENCES CHAIN3 (THREE) + TWO BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 20) PRIMARY KEY, + TWO_VALUE VARCHAR(20), + CHAIN3 BIGINT, + FOREIGN KEY (CHAIN3) REFERENCES CHAIN3 (THREE) ); CREATE TABLE CHAIN1 ( - ONE BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 10) PRIMARY KEY, - ONE_VALUE VARCHAR(20), - CHAIN2 BIGINT, - FOREIGN KEY (CHAIN2) REFERENCES CHAIN2 (TWO) + ONE BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 10) PRIMARY KEY, + ONE_VALUE VARCHAR(20), + CHAIN2 BIGINT, + FOREIGN KEY (CHAIN2) REFERENCES CHAIN2 (TWO) ); CREATE TABLE CHAIN0 ( - ZERO BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 0) PRIMARY KEY, - ZERO_VALUE VARCHAR(20), - CHAIN1 BIGINT, - FOREIGN KEY (CHAIN1) REFERENCES CHAIN1 (ONE) + ZERO BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 0) PRIMARY KEY, + ZERO_VALUE VARCHAR(20), + CHAIN1 BIGINT, + FOREIGN KEY (CHAIN1) REFERENCES CHAIN1 (ONE) ); CREATE TABLE NO_ID_CHAIN4 ( - FOUR BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 40) PRIMARY KEY, - FOUR_VALUE VARCHAR(20) + FOUR BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 40) PRIMARY KEY, + FOUR_VALUE VARCHAR(20) ); CREATE TABLE NO_ID_CHAIN3 ( - THREE_VALUE VARCHAR(20), - NO_ID_CHAIN4 BIGINT, - FOREIGN KEY (NO_ID_CHAIN4) REFERENCES NO_ID_CHAIN4 (FOUR) + THREE_VALUE VARCHAR(20), + NO_ID_CHAIN4 BIGINT, + FOREIGN KEY (NO_ID_CHAIN4) REFERENCES NO_ID_CHAIN4 (FOUR) ); CREATE TABLE NO_ID_CHAIN2 ( - TWO_VALUE VARCHAR(20), - NO_ID_CHAIN4 BIGINT, - FOREIGN KEY (NO_ID_CHAIN4) REFERENCES NO_ID_CHAIN4 (FOUR) + TWO_VALUE VARCHAR(20), + NO_ID_CHAIN4 BIGINT, + FOREIGN KEY (NO_ID_CHAIN4) REFERENCES NO_ID_CHAIN4 (FOUR) ); CREATE TABLE NO_ID_CHAIN1 ( - ONE_VALUE VARCHAR(20), - NO_ID_CHAIN4 BIGINT, - FOREIGN KEY (NO_ID_CHAIN4) REFERENCES NO_ID_CHAIN4 (FOUR) + ONE_VALUE VARCHAR(20), + NO_ID_CHAIN4 BIGINT, + FOREIGN KEY (NO_ID_CHAIN4) REFERENCES NO_ID_CHAIN4 (FOUR) ); CREATE TABLE NO_ID_CHAIN0 ( - ZERO_VALUE VARCHAR(20), - NO_ID_CHAIN4 BIGINT, - FOREIGN KEY (NO_ID_CHAIN4) REFERENCES NO_ID_CHAIN4 (FOUR) + ZERO_VALUE VARCHAR(20), + NO_ID_CHAIN4 BIGINT, + FOREIGN KEY (NO_ID_CHAIN4) REFERENCES NO_ID_CHAIN4 (FOUR) ); CREATE TABLE NO_ID_LIST_CHAIN4 ( - FOUR BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 40) PRIMARY KEY, - FOUR_VALUE VARCHAR(20) + FOUR BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 40) PRIMARY KEY, + FOUR_VALUE VARCHAR(20) ); CREATE TABLE NO_ID_LIST_CHAIN3 ( - THREE_VALUE VARCHAR(20), - NO_ID_LIST_CHAIN4 BIGINT, - NO_ID_LIST_CHAIN4_KEY BIGINT, - PRIMARY KEY (NO_ID_LIST_CHAIN4, - NO_ID_LIST_CHAIN4_KEY), - FOREIGN KEY (NO_ID_LIST_CHAIN4) REFERENCES NO_ID_LIST_CHAIN4 (FOUR) + THREE_VALUE VARCHAR(20), + NO_ID_LIST_CHAIN4 BIGINT, + NO_ID_LIST_CHAIN4_KEY BIGINT, + PRIMARY KEY (NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY), + FOREIGN KEY (NO_ID_LIST_CHAIN4) REFERENCES NO_ID_LIST_CHAIN4 (FOUR) ); CREATE TABLE NO_ID_LIST_CHAIN2 ( - TWO_VALUE VARCHAR(20), - NO_ID_LIST_CHAIN4 BIGINT, - NO_ID_LIST_CHAIN4_KEY BIGINT, - NO_ID_LIST_CHAIN3_KEY BIGINT, - PRIMARY KEY (NO_ID_LIST_CHAIN4, - NO_ID_LIST_CHAIN4_KEY, - NO_ID_LIST_CHAIN3_KEY), - FOREIGN KEY ( - NO_ID_LIST_CHAIN4, - NO_ID_LIST_CHAIN4_KEY - ) REFERENCES NO_ID_LIST_CHAIN3 ( - NO_ID_LIST_CHAIN4, - NO_ID_LIST_CHAIN4_KEY - ) + TWO_VALUE VARCHAR(20), + NO_ID_LIST_CHAIN4 BIGINT, + NO_ID_LIST_CHAIN4_KEY BIGINT, + NO_ID_LIST_CHAIN3_KEY BIGINT, + PRIMARY KEY (NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY), + FOREIGN KEY ( + NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY + ) REFERENCES NO_ID_LIST_CHAIN3 ( + NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY + ) ); CREATE TABLE NO_ID_LIST_CHAIN1 ( - ONE_VALUE VARCHAR(20), - NO_ID_LIST_CHAIN4 BIGINT, - NO_ID_LIST_CHAIN4_KEY BIGINT, - NO_ID_LIST_CHAIN3_KEY BIGINT, - NO_ID_LIST_CHAIN2_KEY BIGINT, - PRIMARY KEY (NO_ID_LIST_CHAIN4, - NO_ID_LIST_CHAIN4_KEY, - NO_ID_LIST_CHAIN3_KEY, - NO_ID_LIST_CHAIN2_KEY), - FOREIGN KEY ( - NO_ID_LIST_CHAIN4, - NO_ID_LIST_CHAIN4_KEY, - NO_ID_LIST_CHAIN3_KEY - ) REFERENCES NO_ID_LIST_CHAIN2 ( - NO_ID_LIST_CHAIN4, - NO_ID_LIST_CHAIN4_KEY, - NO_ID_LIST_CHAIN3_KEY - ) + ONE_VALUE VARCHAR(20), + NO_ID_LIST_CHAIN4 BIGINT, + NO_ID_LIST_CHAIN4_KEY BIGINT, + NO_ID_LIST_CHAIN3_KEY BIGINT, + NO_ID_LIST_CHAIN2_KEY BIGINT, + PRIMARY KEY (NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY, + NO_ID_LIST_CHAIN2_KEY), + FOREIGN KEY ( + NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY + ) REFERENCES NO_ID_LIST_CHAIN2 ( + NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY + ) ); CREATE TABLE NO_ID_LIST_CHAIN0 ( - ZERO_VALUE VARCHAR(20), - NO_ID_LIST_CHAIN4 BIGINT, - NO_ID_LIST_CHAIN4_KEY BIGINT, - NO_ID_LIST_CHAIN3_KEY BIGINT, - NO_ID_LIST_CHAIN2_KEY BIGINT, - NO_ID_LIST_CHAIN1_KEY BIGINT, - PRIMARY KEY (NO_ID_LIST_CHAIN4, - NO_ID_LIST_CHAIN4_KEY, - NO_ID_LIST_CHAIN3_KEY, - NO_ID_LIST_CHAIN2_KEY, - NO_ID_LIST_CHAIN1_KEY), - FOREIGN KEY ( - NO_ID_LIST_CHAIN4, - NO_ID_LIST_CHAIN4_KEY, - NO_ID_LIST_CHAIN3_KEY, - NO_ID_LIST_CHAIN2_KEY - ) REFERENCES NO_ID_LIST_CHAIN1 ( - NO_ID_LIST_CHAIN4, - NO_ID_LIST_CHAIN4_KEY, - NO_ID_LIST_CHAIN3_KEY, - NO_ID_LIST_CHAIN2_KEY - ) + ZERO_VALUE VARCHAR(20), + NO_ID_LIST_CHAIN4 BIGINT, + NO_ID_LIST_CHAIN4_KEY BIGINT, + NO_ID_LIST_CHAIN3_KEY BIGINT, + NO_ID_LIST_CHAIN2_KEY BIGINT, + NO_ID_LIST_CHAIN1_KEY BIGINT, + PRIMARY KEY (NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY, + NO_ID_LIST_CHAIN2_KEY, + NO_ID_LIST_CHAIN1_KEY), + FOREIGN KEY ( + NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY, + NO_ID_LIST_CHAIN2_KEY + ) REFERENCES NO_ID_LIST_CHAIN1 ( + NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY, + NO_ID_LIST_CHAIN2_KEY + ) ); - CREATE TABLE NO_ID_MAP_CHAIN4 ( - FOUR BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 40) PRIMARY KEY, - FOUR_VALUE VARCHAR(20) + FOUR BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 40) PRIMARY KEY, + FOUR_VALUE VARCHAR(20) ); CREATE TABLE NO_ID_MAP_CHAIN3 ( - THREE_VALUE VARCHAR(20), - NO_ID_MAP_CHAIN4 BIGINT, - NO_ID_MAP_CHAIN4_KEY VARCHAR(20), - PRIMARY KEY (NO_ID_MAP_CHAIN4, - NO_ID_MAP_CHAIN4_KEY), - FOREIGN KEY (NO_ID_MAP_CHAIN4) REFERENCES NO_ID_MAP_CHAIN4 (FOUR) + THREE_VALUE VARCHAR(20), + NO_ID_MAP_CHAIN4 BIGINT, + NO_ID_MAP_CHAIN4_KEY VARCHAR(20), + PRIMARY KEY (NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY), + FOREIGN KEY (NO_ID_MAP_CHAIN4) REFERENCES NO_ID_MAP_CHAIN4 (FOUR) ); CREATE TABLE NO_ID_MAP_CHAIN2 ( - TWO_VALUE VARCHAR(20), - NO_ID_MAP_CHAIN4 BIGINT, - NO_ID_MAP_CHAIN4_KEY VARCHAR(20), - NO_ID_MAP_CHAIN3_KEY VARCHAR(20), - PRIMARY KEY (NO_ID_MAP_CHAIN4, - NO_ID_MAP_CHAIN4_KEY, - NO_ID_MAP_CHAIN3_KEY), - FOREIGN KEY ( - NO_ID_MAP_CHAIN4, - NO_ID_MAP_CHAIN4_KEY - ) REFERENCES NO_ID_MAP_CHAIN3 ( - NO_ID_MAP_CHAIN4, - NO_ID_MAP_CHAIN4_KEY - ) + TWO_VALUE VARCHAR(20), + NO_ID_MAP_CHAIN4 BIGINT, + NO_ID_MAP_CHAIN4_KEY VARCHAR(20), + NO_ID_MAP_CHAIN3_KEY VARCHAR(20), + PRIMARY KEY (NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY), + FOREIGN KEY ( + NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY + ) REFERENCES NO_ID_MAP_CHAIN3 ( + NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY + ) ); CREATE TABLE NO_ID_MAP_CHAIN1 ( - ONE_VALUE VARCHAR(20), - NO_ID_MAP_CHAIN4 BIGINT, - NO_ID_MAP_CHAIN4_KEY VARCHAR(20), - NO_ID_MAP_CHAIN3_KEY VARCHAR(20), - NO_ID_MAP_CHAIN2_KEY VARCHAR(20), - PRIMARY KEY (NO_ID_MAP_CHAIN4, - NO_ID_MAP_CHAIN4_KEY, - NO_ID_MAP_CHAIN3_KEY, - NO_ID_MAP_CHAIN2_KEY), - FOREIGN KEY ( - NO_ID_MAP_CHAIN4, - NO_ID_MAP_CHAIN4_KEY, - NO_ID_MAP_CHAIN3_KEY - ) REFERENCES NO_ID_MAP_CHAIN2 ( - NO_ID_MAP_CHAIN4, - NO_ID_MAP_CHAIN4_KEY, - NO_ID_MAP_CHAIN3_KEY - ) + ONE_VALUE VARCHAR(20), + NO_ID_MAP_CHAIN4 BIGINT, + NO_ID_MAP_CHAIN4_KEY VARCHAR(20), + NO_ID_MAP_CHAIN3_KEY VARCHAR(20), + NO_ID_MAP_CHAIN2_KEY VARCHAR(20), + PRIMARY KEY (NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY, + NO_ID_MAP_CHAIN2_KEY), + FOREIGN KEY ( + NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY + ) REFERENCES NO_ID_MAP_CHAIN2 ( + NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY + ) ); CREATE TABLE NO_ID_MAP_CHAIN0 ( - ZERO_VALUE VARCHAR(20), - NO_ID_MAP_CHAIN4 BIGINT, - NO_ID_MAP_CHAIN4_KEY VARCHAR(20), - NO_ID_MAP_CHAIN3_KEY VARCHAR(20), - NO_ID_MAP_CHAIN2_KEY VARCHAR(20), - NO_ID_MAP_CHAIN1_KEY VARCHAR(20), - PRIMARY KEY (NO_ID_MAP_CHAIN4, - NO_ID_MAP_CHAIN4_KEY, - NO_ID_MAP_CHAIN3_KEY, - NO_ID_MAP_CHAIN2_KEY, - NO_ID_MAP_CHAIN1_KEY), - FOREIGN KEY ( - NO_ID_MAP_CHAIN4, - NO_ID_MAP_CHAIN4_KEY, - NO_ID_MAP_CHAIN3_KEY, - NO_ID_MAP_CHAIN2_KEY - ) REFERENCES NO_ID_MAP_CHAIN1 ( - NO_ID_MAP_CHAIN4, - NO_ID_MAP_CHAIN4_KEY, - NO_ID_MAP_CHAIN3_KEY, - NO_ID_MAP_CHAIN2_KEY - ) -); - -CREATE TABLE WITH_READ_ONLY ( - ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 40) PRIMARY KEY, - NAME VARCHAR(200), + ZERO_VALUE VARCHAR(20), + NO_ID_MAP_CHAIN4 BIGINT, + NO_ID_MAP_CHAIN4_KEY VARCHAR(20), + NO_ID_MAP_CHAIN3_KEY VARCHAR(20), + NO_ID_MAP_CHAIN2_KEY VARCHAR(20), + NO_ID_MAP_CHAIN1_KEY VARCHAR(20), + PRIMARY KEY (NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY, + NO_ID_MAP_CHAIN2_KEY, + NO_ID_MAP_CHAIN1_KEY), + FOREIGN KEY ( + NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY, + NO_ID_MAP_CHAIN2_KEY + ) REFERENCES NO_ID_MAP_CHAIN1 ( + NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY, + NO_ID_MAP_CHAIN2_KEY + ) +); + +CREATE TABLE WITH_READ_ONLY +( + ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 40) PRIMARY KEY, + NAME VARCHAR(200), READ_ONLY VARCHAR(200) DEFAULT 'from-db' ); CREATE TABLE VERSIONED_AGGREGATE ( - ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, - VERSION BIGINT + ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, + VERSION BIGINT ); @@ -334,7 +334,7 @@ CREATE TABLE WITH_LOCAL_DATE_TIME CREATE TABLE WITH_INSERT_ONLY ( - ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, + ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, INSERT_ONLY VARCHAR(100) ); @@ -345,30 +345,30 @@ CREATE TABLE WITH_ID_ONLY CREATE TABLE MULTIPLE_COLLECTIONS ( - ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, + 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) + NAME VARCHAR(100) ); CREATE TABLE LIST_ELEMENT ( - MULTIPLE_COLLECTIONS BIGINT, + MULTIPLE_COLLECTIONS BIGINT, MULTIPLE_COLLECTIONS_KEY INT, - NAME VARCHAR(100) + NAME VARCHAR(100) ); CREATE TABLE MAP_ELEMENT ( - MULTIPLE_COLLECTIONS BIGINT, + MULTIPLE_COLLECTIONS BIGINT, MULTIPLE_COLLECTIONS_KEY VARCHAR(10), - ENUM_MAP_OWNER BIGINT, - ENUM_MAP_OWNER_KEY VARCHAR(10), - NAME VARCHAR(100) + ENUM_MAP_OWNER BIGINT, + ENUM_MAP_OWNER_KEY VARCHAR(10), + NAME VARCHAR(100) ); CREATE TABLE AUTHOR @@ -379,12 +379,12 @@ CREATE TABLE AUTHOR CREATE TABLE BOOK ( AUTHOR BIGINT, - NAME VARCHAR(100) + NAME VARCHAR(100) ); CREATE TABLE ENUM_MAP_OWNER ( - ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, + ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, NAME VARCHAR(100) ); @@ -397,7 +397,7 @@ CREATE TABLE WITH_ONE_TO_ONE CREATE TABLE REFERENCED ( "renamed" VARCHAR(100), - ID BIGINT + ID BIGINT ); CREATE TABLE FIRST @@ -416,7 +416,13 @@ CREATE TABLE SEC CREATE TABLE THIRD ( - SEC BIGINT NOT NULL, - NAME VARCHAR(20) NOT NULL, + SEC BIGINT NOT NULL, + NAME VARCHAR(20) NOT NULL, FOREIGN KEY (SEC) REFERENCES SEC (ID) -); \ No newline at end of file +); + +CREATE TABLE SINGLE_EMBEDDED_ID_ENTITY +( + ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, + NAME VARCHAR(100) +) \ No newline at end of file diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/AggregatePath.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/AggregatePath.java index abd3e084d3..f721e34eea 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/AggregatePath.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/AggregatePath.java @@ -16,12 +16,21 @@ package org.springframework.data.relational.core.mapping; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Stream; import java.util.stream.StreamSupport; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PersistentPropertyPath; +import org.springframework.data.mapping.PropertyHandler; import org.springframework.data.relational.core.sql.SqlIdentifier; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -34,7 +43,7 @@ * @author Jens Schauder * @author Mark Paluch */ -public interface AggregatePath extends Iterable { +public interface AggregatePath extends Iterable, Comparable { /** * Returns the path that has the same beginning but is one segment shorter than this path. @@ -52,6 +61,15 @@ public interface AggregatePath extends Iterable { */ AggregatePath append(RelationalPersistentProperty property); + /** + * Creates a new path by extending the current path by the path passed as an argument. + * + * @param path must not be {@literal null}. + * @return Guaranteed to be not {@literal null}. + * @since 3.5 + */ + AggregatePath append(AggregatePath path); + /** * @return {@literal true} if this is a root path for the underlying type. */ @@ -227,6 +245,9 @@ default Stream stream() { */ AggregatePath getIdDefiningParentPath(); + @Nullable + AggregatePath getTail(); + record TableInfo( /* @@ -240,7 +261,7 @@ record TableInfo( */ @Nullable SqlIdentifier tableAlias, - ColumnInfo reverseColumnInfo, + ColumnInfos reverseColumnInfos, /* * The column used for the list index or map key of the leaf property of this path. @@ -256,13 +277,7 @@ record TableInfo( /* * The column name of the id column of the ancestor path that represents an actual table. */ - SqlIdentifier idColumnName, - - /* - * If the table owning ancestor has an id the column name of that id property is returned. Otherwise the reverse - * column is returned. - */ - SqlIdentifier effectiveIdColumnName) { + ColumnInfos idColumnInfos) { static TableInfo of(AggregatePath path) { @@ -273,18 +288,7 @@ static TableInfo of(AggregatePath path) { SqlIdentifier tableAlias = tableOwner.isRoot() ? null : AggregatePathTableUtils.constructTableAlias(tableOwner); - ColumnInfo reverseColumnInfo = null; - if (!tableOwner.isRoot()) { - - AggregatePath idDefiningParentPath = tableOwner.getIdDefiningParentPath(); - RelationalPersistentProperty leafProperty = tableOwner.getRequiredLeafProperty(); - - SqlIdentifier reverseColumnName = leafProperty - .getReverseColumnName(idDefiningParentPath.getRequiredLeafEntity()); - - reverseColumnInfo = new ColumnInfo(reverseColumnName, - AggregatePathTableUtils.prefixWithTableAlias(path, reverseColumnName)); - } + ColumnInfos reverseColumnInfos = computeReverseColumnInfo(path); ColumnInfo qualifierColumnInfo = null; if (!path.isRoot()) { @@ -300,27 +304,114 @@ static TableInfo of(AggregatePath path) { qualifierColumnType = path.getRequiredLeafProperty().getQualifierColumnType(); } - SqlIdentifier idColumnName = leafEntity.hasIdProperty() ? leafEntity.getIdColumn() : null; + ColumnInfos idColumnInfos = computeIdColumnInfos(tableOwner, leafEntity); - SqlIdentifier effectiveIdColumnName = tableOwner.isRoot() ? idColumnName : reverseColumnInfo.name(); + return new TableInfo(qualifiedTableName, tableAlias, reverseColumnInfos, qualifierColumnInfo, qualifierColumnType, + idColumnInfos); - return new TableInfo(qualifiedTableName, tableAlias, reverseColumnInfo, qualifierColumnInfo, qualifierColumnType, - idColumnName, effectiveIdColumnName); + } + + private static ColumnInfos computeIdColumnInfos(AggregatePath tableOwner, + RelationalPersistentEntity leafEntity) { + ColumnInfos idColumnInfos = ColumnInfos.empty(tableOwner); + if (!leafEntity.hasIdProperty()) { + return idColumnInfos; + } + + RelationalPersistentProperty idProperty = leafEntity.getRequiredIdProperty(); + AggregatePath idPath = tableOwner.append(idProperty); + + if (idProperty.isEntity()) { + ColumInfosBuilder ciBuilder = new ColumInfosBuilder(idPath); + idPath.getRequiredLeafEntity().doWithProperties((PropertyHandler) p -> { + AggregatePath idElementPath = idPath.append(p); + ciBuilder.add(idElementPath, ColumnInfo.of(idElementPath)); + }); + return ciBuilder.build(); + } else { + ColumInfosBuilder ciBuilder = new ColumInfosBuilder(idPath.getParentPath()); + ciBuilder.add(idPath, ColumnInfo.of(idPath)); + return ciBuilder.build(); + } } - } - record ColumnInfo( + private static ColumnInfos computeReverseColumnInfo(AggregatePath path) { + + AggregatePath tableOwner = AggregatePathTraversal.getTableOwningPath(path); + + if (tableOwner.isRoot()) { + return ColumnInfos.empty(tableOwner); + } + + AggregatePath idDefiningParentPath = tableOwner.getIdDefiningParentPath(); + RelationalPersistentProperty leafProperty = tableOwner.getRequiredLeafProperty(); + + RelationalPersistentProperty idProperty = idDefiningParentPath.getLeafEntity().getIdProperty(); + + if (idProperty != null) { + if (idProperty.isEntity()) { + + AggregatePath idBasePath = idDefiningParentPath.append(idProperty); + ColumInfosBuilder ciBuilder = new ColumInfosBuilder(idBasePath); + + RelationalPersistentEntity idEntity = idBasePath.getRequiredLeafEntity(); + idEntity.doWithProperties((PropertyHandler) p -> { + AggregatePath idElementPath = idBasePath.append(p); + SqlIdentifier name = idElementPath.getColumnInfo().name(); + name = name.transform(n -> idDefiningParentPath.getTableInfo().qualifiedTableName.getReference() + "_" + n); + + ciBuilder.add(idElementPath, name, name); + }); + + return ciBuilder.build(); + + } else { + + ColumInfosBuilder ciBuilder = new ColumInfosBuilder(idDefiningParentPath); + SqlIdentifier reverseColumnName = leafProperty + .getReverseColumnName(idDefiningParentPath.getRequiredLeafEntity()); + + ciBuilder.add(idProperty, reverseColumnName, + AggregatePathTableUtils.prefixWithTableAlias(path, reverseColumnName)); + + return ciBuilder.build(); + } + } else { - /* The name of the column used to represent this property in the database. */ - SqlIdentifier name, /* The alias for the column used to represent this property in the database. */ - SqlIdentifier alias) { + ColumInfosBuilder ciBuilder = new ColumInfosBuilder(idDefiningParentPath); + SqlIdentifier reverseColumnName = leafProperty + .getReverseColumnName(idDefiningParentPath.getRequiredLeafEntity()); + + ciBuilder.add(idDefiningParentPath, reverseColumnName, + AggregatePathTableUtils.prefixWithTableAlias(path, reverseColumnName)); + + return ciBuilder.build(); + } + + } + + @Deprecated(forRemoval = true) + public ColumnInfo reverseColumnInfo() { + return reverseColumnInfos.unique(); + } + + public ColumnInfos effectiveIdColumnInfos() { + return reverseColumnInfos.columnInfos.isEmpty() ? idColumnInfos : reverseColumnInfos; + } + } + + /** + * @param name The name of the column used to represent this property in the database. + * @param alias The alias for the column used to represent this property in the database. + */ + record ColumnInfo(SqlIdentifier name, SqlIdentifier alias) { /** * Create a {@link ColumnInfo} from an aggregate path. ColumnInfo can be created for simple type single-value * properties only. * - * @param path + * @param path the path to the {@literal ColumnInfo} for. * @return the {@link ColumnInfo}. * @throws IllegalArgumentException if the path is {@link #isRoot()}, {@link #isEmbedded()} or * {@link #isMultiValued()}. @@ -338,4 +429,102 @@ static ColumnInfo of(AggregatePath path) { return new ColumnInfo(columnName, AggregatePathTableUtils.prefixWithTableAlias(path, columnName)); } } + + /** + * A group of {@link ColumnInfo} values referenced by there respective {@link AggregatePath}. This is relevant for + * composite ids and references to such ids. + **/ + class ColumnInfos { + + private final AggregatePath basePath; + private final Map columnInfos; + + /** + * @param basePath The path on which all other paths in the other argument are based on. For the typical case of a + * composite id, this would be the path to the composite ids. + * @param columnInfos A map, mapping {@literal AggregatePath} instances to the respective {@literal ColumnInfo} + */ + private ColumnInfos(AggregatePath basePath, Map columnInfos) { + + this.basePath = basePath; + this.columnInfos = columnInfos; + } + + public static ColumnInfos empty(AggregatePath base) { + return new ColumnInfos(base, new HashMap<>()); + } + + public ColumnInfo unique() { + + Collection values = columnInfos.values(); + Assert.state(values.size() == 1, "ColumnInfo is not unique"); + return values.iterator().next(); + } + + public ColumnInfo any() { + + Collection values = columnInfos.values(); + return values.iterator().next(); + } + + public boolean isEmpty() { + return columnInfos.isEmpty(); + } + + public List toList(Function mapper) { + return columnInfos.values().stream().map(mapper).toList(); + } + + public void forEach(BiConsumer consumer) { + columnInfos.forEach(consumer); + } + + public T any(BiFunction consumer) { + + Map.Entry any = columnInfos.entrySet().iterator().next(); + return consumer.apply(any.getKey(), any.getValue()); + } + + public ColumnInfo get(AggregatePath path) { + return columnInfos.get(path); + } + + public AggregatePath fullPath(AggregatePath ap) { + return basePath.append(ap); + } + + public int size() { + return columnInfos.size(); + } + } + + class ColumInfosBuilder { + private final AggregatePath basePath; + + private final Map columnInfoMap = new TreeMap<>(); + + public ColumInfosBuilder(AggregatePath basePath) { + this.basePath = basePath; + } + + void add(AggregatePath path, SqlIdentifier name, SqlIdentifier alias) { + add(path, new ColumnInfo(name, alias)); + } + + public void add(RelationalPersistentProperty property, SqlIdentifier name, SqlIdentifier alias) { + add(basePath.append(property), name, alias); + } + + ColumnInfos build() { + return new ColumnInfos(basePath, columnInfoMap); + } + + public void add(AggregatePath path, ColumnInfo columnInfo) { + columnInfoMap.put(path.substract(basePath), columnInfo); + } + } + + @Nullable + AggregatePath substract(@Nullable AggregatePath basePath); + } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentEntity.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentEntity.java index 99b48363fc..e216ee4865 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentEntity.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentEntity.java @@ -154,6 +154,7 @@ public SqlIdentifier getQualifiedTableName() { } @Override + @Deprecated(forRemoval = true) public SqlIdentifier getIdColumn() { return getRequiredIdProperty().getColumnName(); } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DefaultAggregatePath.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DefaultAggregatePath.java index b0bcc78cb2..e0e1073da5 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DefaultAggregatePath.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DefaultAggregatePath.java @@ -21,6 +21,7 @@ import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.util.Lazy; +import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ConcurrentLruCache; @@ -97,6 +98,20 @@ public AggregatePath append(RelationalPersistentProperty property) { return nestedCache.get(property); } + @Override + public AggregatePath append(AggregatePath path) { + + if (path.isRoot()) { + return this; + } + + RelationalPersistentProperty baseProperty = path.getRequiredBaseProperty(); + AggregatePath appended = append(baseProperty); + AggregatePath tail = path.getTail(); + return tail == null ? appended : appended.append(tail); + + } + private AggregatePath doGetAggegatePath(RelationalPersistentProperty property) { PersistentPropertyPath newPath = isRoot() // @@ -194,6 +209,47 @@ public AggregatePath getIdDefiningParentPath() { return AggregatePathTraversal.getIdDefiningPath(this); } + @Override + public AggregatePath getTail() { + + if (getLength() <= 2) { + return null; + } + + AggregatePath tail = null; + for (RelationalPersistentProperty prop : this.path) { + if (tail == null) { + tail = context.getAggregatePath(context.getPersistentEntity(prop)); + } else { + tail = tail.append(prop); + } + } + return tail; + } + + @Override + @Nullable + public AggregatePath substract(@Nullable AggregatePath basePath) { + + if (basePath == null || basePath.isRoot()) { + return this; + } + + if (this.isRoot()) { + throw new IllegalStateException("Can't subtract from root path"); + } + + if (basePath.getRequiredBaseProperty().equals(getRequiredBaseProperty())) { + AggregatePath tail = this.getTail(); + if (tail == null) { + return null; + } + return tail.substract(basePath.getTail()); + } + + throw new IllegalStateException("Can't subtract [%s] from [%s]".formatted(basePath, this)); + } + /** * Finds and returns the longest path with ich identical or an ancestor to the current path and maps directly to a * table. @@ -240,7 +296,6 @@ public int hashCode() { return Objects.hash(context, rootType, path); } - @Override public String toString() { return "AggregatePath[" @@ -248,6 +303,11 @@ public String toString() { + ((isRoot()) ? "/" : path.toDotPath()); } + @Override + public int compareTo(@NonNull AggregatePath other) { + return toDotPath().compareTo(other.toDotPath()); + } + private static class AggregatePathIterator implements Iterator { private @Nullable AggregatePath current; diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/EmbeddedRelationalPersistentEntity.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/EmbeddedRelationalPersistentEntity.java index 78bfa01d37..6a3befcdf8 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/EmbeddedRelationalPersistentEntity.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/EmbeddedRelationalPersistentEntity.java @@ -51,6 +51,7 @@ public SqlIdentifier getTableName() { } @Override + @Deprecated(forRemoval = true) public SqlIdentifier getIdColumn() { throw new MappingException("Embedded entity does not have an id column"); } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java index eb6409e74e..47bd819900 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java @@ -148,9 +148,9 @@ protected RelationalPersistentProperty createPersistentProperty(Property propert } /** - * @since 3.2 * @return iff single query loading is enabled. * @see #setSingleQueryLoadingEnabled(boolean) + * @since 3.2 */ public boolean isSingleQueryLoadingEnabled() { return singleQueryLoadingEnabled; @@ -161,8 +161,8 @@ public boolean isSingleQueryLoadingEnabled() { * {@link org.springframework.data.relational.core.dialect.Dialect} supports it, Spring Data JDBC will try to use * Single Query Loading if possible. * - * @since 3.2 * @param singleQueryLoadingEnabled + * @since 3.2 */ public void setSingleQueryLoadingEnabled(boolean singleQueryLoadingEnabled) { this.singleQueryLoadingEnabled = singleQueryLoadingEnabled; @@ -217,7 +217,6 @@ private record AggregatePathCacheKey(RelationalPersistentEntity root, * Create a new AggregatePathCacheKey for a root entity. * * @param root the root entity. - * @return */ static AggregatePathCacheKey of(RelationalPersistentEntity root) { return new AggregatePathCacheKey(root, null); @@ -226,8 +225,7 @@ static AggregatePathCacheKey of(RelationalPersistentEntity root) { /** * Create a new AggregatePathCacheKey for a property path. * - * @param path - * @return + * @param path {@Literal AggregatePath} to obtain a cache key for. */ static AggregatePathCacheKey of(PersistentPropertyPath path) { return new AggregatePathCacheKey(path.getBaseProperty().getOwner(), path); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntity.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntity.java index 49e9b929c1..210ab1f7ec 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntity.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntity.java @@ -50,7 +50,9 @@ default SqlIdentifier getQualifiedTableName() { * Returns the column representing the identifier. * * @return will never be {@literal null}. + * @deprecated use {@code AggregatePath.getTableInfo().getIdColumnInfos()} instead. */ + @Deprecated(forRemoval = true) SqlIdentifier getIdColumn(); } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AnalyticFunction.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AnalyticFunction.java index b3af3e1e86..ecc46d3ede 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AnalyticFunction.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AnalyticFunction.java @@ -16,6 +16,7 @@ package org.springframework.data.relational.core.sql; import java.util.Arrays; +import java.util.Collection; /** * Represents an analytic function, also known as windowing function @@ -44,18 +45,62 @@ private AnalyticFunction(SimpleFunction function, Partition partition, OrderBy o this.orderBy = orderBy; } + /** + * Specify the {@literal PARTITION BY} clause of an analytic function + * + * @param partitionBy Typically, column but other expressions are fine to. + * @return a new {@literal AnalyticFunction} is partitioned by the given expressions, overwriting any expression + * previously present. + */ public AnalyticFunction partitionBy(Expression... partitionBy) { - return new AnalyticFunction(function, new Partition(partitionBy), orderBy); } + /** + * Specify the {@literal PARTITION BY} clause of an analytic function + * + * @param partitionBy Typically, column but other expressions are fine to. + * @return a new {@literal AnalyticFunction} is partitioned by the given expressions, overwriting any expression + * previously present. + * @since 3.5 + */ + public AnalyticFunction partitionBy(Collection partitionBy) { + return partitionBy(partitionBy.toArray(new Expression[0])); + } + + /** + * Specify the {@literal ORDER BY} clause of an analytic function + * + * @param orderBy Typically, column but other expressions are fine to. + * @return a new {@literal AnalyticFunction} is ordered by the given expressions, overwriting any expression + * previously present. + */ public AnalyticFunction orderBy(OrderByField... orderBy) { return new AnalyticFunction(function, partition, new OrderBy(orderBy)); } - public AnalyticFunction orderBy(Expression... orderByExpression) { + /** + * Specify the {@literal ORDER BY} clause of an analytic function + * + * @param orderBy Typically, column but other expressions are fine to. + * @return a new {@literal AnalyticFunction} is ordered by the given expressions, overwriting any expression + * previously present. + * @since 3.5 + */ + public AnalyticFunction orderBy(Collection orderBy) { + return orderBy(orderBy.toArray(new Expression[0])); + } + + /** + * Specify the {@literal ORDER BY} clause of an analytic function + * + * @param orderBy array of {@link Expression}. Typically, column but other expressions are fine to. + * @return a new {@literal AnalyticFunction} is ordered by the given expressions, overwriting any expression + * previously present. + */ + public AnalyticFunction orderBy(Expression... orderBy) { - final OrderByField[] orderByFields = Arrays.stream(orderByExpression) // + final OrderByField[] orderByFields = Arrays.stream(orderBy) // .map(OrderByField::from) // .toArray(OrderByField[]::new); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Conditions.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Conditions.java index aa7f4e70e7..c5f4fe0d88 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Conditions.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Conditions.java @@ -247,7 +247,7 @@ public static In in(Expression columnOrExpression, Expression... expressions) { * @param subselect the subselect. * @return the {@link In} condition. */ - public static In in(Column column, Select subselect) { + public static In in(Expression column, Select subselect) { Assert.notNull(column, "Column must not be null"); Assert.notNull(subselect, "Subselect must not be null"); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/TupleExpression.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/TupleExpression.java new file mode 100644 index 0000000000..e1699197ea --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/TupleExpression.java @@ -0,0 +1,54 @@ +package org.springframework.data.relational.core.sql; + +import org.jetbrains.annotations.NotNull; + +import static java.util.stream.Collectors.*; + +import java.util.List; + +/** + * A tuple as used in conditions like + * + *
+ *   WHERE (one, two) IN (select x, y from some_table)
+ * 
+ * + * @author Jens Schauder + * @since 3.5 + */ +public class TupleExpression extends AbstractSegment implements Expression { + + private final List expressions; + + private static Segment[] children(List expressions) { + return expressions.toArray(new Segment[0]); + } + + private TupleExpression(List expressions) { + + super(children(expressions)); + + this.expressions = expressions; + } + + public static TupleExpression create(Expression... expressions) { + return new TupleExpression(List.of(expressions)); + } + + public static TupleExpression create(List expressions) { + return new TupleExpression(expressions); + } + + public static Expression maybeWrap(List columns) { + + if (columns.size() == 1) { + return columns.get(0); + } + return new TupleExpression(columns); + } + + @Override + public String toString() { + return "(" + expressions.stream().map(Expression::toString).collect(joining(", ")) + ")"; + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ExpressionVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ExpressionVisitor.java index 32ce15dee1..40c21e1976 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ExpressionVisitor.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ExpressionVisitor.java @@ -48,7 +48,7 @@ class ExpressionVisitor extends TypedSubtreeVisitor implements PartR /** * Creates an {@code ExpressionVisitor}. * - * @param context must not be {@literal null}. + * @param context must not be {@literal null}. * @param aliasHandling controls if columns should be rendered as their alias or using their table names. * @since 2.3 */ @@ -78,6 +78,13 @@ Delegation enterMatched(Expression segment) { return Delegation.delegateTo(visitor); } + if (segment instanceof TupleExpression) { + + TupleVisitor visitor = new TupleVisitor(context); + partRenderer = visitor; + return Delegation.delegateTo(visitor); + } + if (segment instanceof AnalyticFunction) { AnalyticFunctionVisitor visitor = new AnalyticFunctionVisitor(context); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/TupleVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/TupleVisitor.java new file mode 100644 index 0000000000..fef8d8f688 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/TupleVisitor.java @@ -0,0 +1,72 @@ +/* + * Copyright 2019-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.render; + +import org.springframework.data.relational.core.sql.TupleExpression; +import org.springframework.data.relational.core.sql.Visitable; + +/** + * Visitor for rendering tuple expressions. + * + * @author Jens Schauder + * @since 3.5 + */ +class TupleVisitor extends TypedSingleConditionRenderSupport implements PartRenderer { + + private final StringBuilder part = new StringBuilder(); + private boolean needsComma = false; + + TupleVisitor(RenderContext context) { + super(context); + } + + @Override + Delegation leaveNested(Visitable segment) { + + if (hasDelegatedRendering()) { + + if (needsComma) { + part.append(", "); + } + + part.append(consumeRenderedPart()); + needsComma = true; + } + + return super.leaveNested(segment); + } + + @Override + Delegation enterMatched(TupleExpression segment) { + + part.append("("); + + return super.enterMatched(segment); + } + + @Override + Delegation leaveMatched(TupleExpression segment) { + + part.append(")"); + + return super.leaveMatched(segment); + } + + @Override + public CharSequence getRenderedPart() { + return part; + } +} 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 65b0ff095f..183050166f 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 @@ -167,9 +167,13 @@ private QueryMeta createInlineQuery(AggregatePath basePath, @Nullable Condition columns.add(rownumber); String rowCountAlias = aliases.getRowCountAlias(basePath); - Expression count = basePath.isRoot() ? new AliasedExpression(SQL.literalOf(1), rowCountAlias) - : AnalyticFunction.create("count", Expressions.just("*")) - .partitionBy(table.column(basePath.getTableInfo().reverseColumnInfo().name())).as(rowCountAlias); + Expression count = basePath.isRoot() ? new AliasedExpression(SQL.literalOf(1), rowCountAlias) // + : AnalyticFunction.create("count", Expressions.just("*")) // + .partitionBy( // + basePath.getTableInfo().reverseColumnInfos().toList( // + ci -> table.column(ci.name()) // + ) // + ).as(rowCountAlias); columns.add(count); String backReferenceAlias = null; @@ -178,7 +182,7 @@ private QueryMeta createInlineQuery(AggregatePath basePath, @Nullable Condition if (!basePath.isRoot()) { backReferenceAlias = aliases.getBackReferenceAlias(basePath); - columns.add(table.column(basePath.getTableInfo().reverseColumnInfo().name()).as(backReferenceAlias)); + columns.add(table.column(basePath.getTableInfo().reverseColumnInfos().unique().name()).as(backReferenceAlias)); keyAlias = aliases.getKeyAlias(basePath); Expression keyExpression = basePath.isQualified() @@ -238,9 +242,10 @@ private String getIdentifierProperty(List paths) { private static AnalyticFunction createRowNumberExpression(AggregatePath basePath, Table table, String rowNumberAlias) { + AggregatePath.ColumnInfos reverseColumnInfos = basePath.getTableInfo().reverseColumnInfos(); return AnalyticFunction.create("row_number") // - .partitionBy(table.column(basePath.getTableInfo().reverseColumnInfo().name())) // - .orderBy(table.column(basePath.getTableInfo().reverseColumnInfo().name())) // + .partitionBy(reverseColumnInfos.toList(ci -> table.column(ci.name()))) // + .orderBy(reverseColumnInfos.toList(ci -> table.column(ci.name()))) // .as(rowNumberAlias); } diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/ColumnInfosUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/ColumnInfosUnitTests.java new file mode 100644 index 0000000000..54f0bf0482 --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/ColumnInfosUnitTests.java @@ -0,0 +1,102 @@ +/* + * 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.mapping; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.NoSuchElementException; + +import org.junit.jupiter.api.Test; +import org.springframework.data.annotation.Id; +import org.springframework.data.relational.core.sql.SqlIdentifier; + +/** + * Unit tests for the construction of {@link org.springframework.data.relational.core.mapping.AggregatePath.ColumnInfos} + * + * @author Jens Schauder + */ +class ColumnInfosUnitTests { + + static final SqlIdentifier ID = SqlIdentifier.quoted("ID"); + RelationalMappingContext context = new RelationalMappingContext(); + + @Test // GH-574 + void emptyColumnInfos() { + + AggregatePath.ColumnInfos columnInfos = AggregatePath.ColumnInfos.empty(basePath(DummyEntity.class)); + + assertThat(columnInfos.isEmpty()).isTrue(); + assertThrows(NoSuchElementException.class, columnInfos::any); + assertThrows(IllegalStateException.class, columnInfos::unique); + assertThat(columnInfos.toList(ci -> { + throw new IllegalStateException("This should never get called"); + })).isEmpty(); + } + + @Test // GH-574 + void singleElementColumnInfos() { + + AggregatePath.ColumnInfos columnInfos = basePath(DummyEntity.class).getTableInfo().idColumnInfos(); + + assertThat(columnInfos.isEmpty()).isFalse(); + assertThat(columnInfos.any().name()).isEqualTo(ID); + assertThat(columnInfos.unique().name()).isEqualTo(ID); + assertThat(columnInfos.toList(ci -> ci.name())).containsExactly(ID); + } + + @Test // GH-574 + void multiElementColumnInfos() { + + AggregatePath.ColumnInfos columnInfos = basePath(DummyEntityWithCompositeId.class).getTableInfo().idColumnInfos(); + + assertThat(columnInfos.isEmpty()).isFalse(); + assertThat(columnInfos.any().name()).isEqualTo(SqlIdentifier.quoted("ONE")); + assertThrows(IllegalStateException.class, columnInfos::unique); + assertThat(columnInfos.toList(ci -> ci.name())) // + .containsExactly( // + SqlIdentifier.quoted("ONE"), // + SqlIdentifier.quoted("TWO") // + ); + + List collector = new ArrayList<>(); + columnInfos.forEach((ap, ci) -> collector.add(ap.toDotPath() + "+" + ci.name())); + assertThat(collector).containsExactly("one+\"ONE\"", "two+\"TWO\""); + + columnInfos.get(getPath(CompositeId.class, "one")); + + } + + private AggregatePath getPath(Class type, String name) { + return basePath(type).append(context.getPersistentEntity(type).getPersistentProperty(name)); + } + + private AggregatePath basePath(Class type) { + return context.getAggregatePath(context.getPersistentEntity(type)); + } + + record DummyEntity(@Id String id, String name) { + } + + record CompositeId(String one, String two) { + } + + record DummyEntityWithCompositeId(@Id @Embedded.Nullable CompositeId id, String name) { + } +} diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/DefaultAggregatePathUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/DefaultAggregatePathUnitTests.java index c173d0294f..1abdc4ddd1 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/DefaultAggregatePathUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/DefaultAggregatePathUnitTests.java @@ -22,6 +22,8 @@ import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.TreeSet; import java.util.stream.Collectors; import org.junit.jupiter.api.Test; @@ -62,9 +64,9 @@ void getParentPath() { assertSoftly(softly -> { - softly.assertThat(path("second.third2.value").getParentPath()).isEqualTo(path("second.third2")); - softly.assertThat(path("second.third2").getParentPath()).isEqualTo(path("second")); - softly.assertThat(path("second").getParentPath()).isEqualTo(path()); + softly.assertThat((Object) path("second.third2.value").getParentPath()).isEqualTo(path("second.third2")); + softly.assertThat((Object) path("second.third2").getParentPath()).isEqualTo(path("second")); + softly.assertThat((Object) path("second").getParentPath()).isEqualTo(path()); softly.assertThatThrownBy(() -> path().getParentPath()).isInstanceOf(IllegalStateException.class); }); @@ -94,14 +96,15 @@ void idDefiningPath() { assertSoftly(softly -> { - softly.assertThat(path("second.third2.value").getIdDefiningParentPath()).isEqualTo(path()); - softly.assertThat(path("second.third.value").getIdDefiningParentPath()).isEqualTo(path()); - softly.assertThat(path("secondList.third2.value").getIdDefiningParentPath()).isEqualTo(path()); - softly.assertThat(path("secondList.third.value").getIdDefiningParentPath()).isEqualTo(path()); - softly.assertThat(path("second2.third2.value").getIdDefiningParentPath()).isEqualTo(path()); - softly.assertThat(path("second2.third.value").getIdDefiningParentPath()).isEqualTo(path()); - softly.assertThat(path("withId.second.third2.value").getIdDefiningParentPath()).isEqualTo(path("withId")); - softly.assertThat(path("withId.second.third.value").getIdDefiningParentPath()).isEqualTo(path("withId")); + softly.assertThat((Object) path("second.third2.value").getIdDefiningParentPath()).isEqualTo(path()); + softly.assertThat((Object) path("second.third.value").getIdDefiningParentPath()).isEqualTo(path()); + softly.assertThat((Object) path("secondList.third2.value").getIdDefiningParentPath()).isEqualTo(path()); + softly.assertThat((Object) path("secondList.third.value").getIdDefiningParentPath()).isEqualTo(path()); + softly.assertThat((Object) path("second2.third2.value").getIdDefiningParentPath()).isEqualTo(path()); + softly.assertThat((Object) path("second2.third.value").getIdDefiningParentPath()).isEqualTo(path()); + softly.assertThat((Object) path("withId.second.third2.value").getIdDefiningParentPath()) + .isEqualTo(path("withId")); + softly.assertThat((Object) path("withId.second.third.value").getIdDefiningParentPath()).isEqualTo(path("withId")); }); } @@ -121,13 +124,13 @@ void reverseColumnName() { assertSoftly(softly -> { - softly.assertThat(path("second.third2").getTableInfo().reverseColumnInfo().name()) + softly.assertThat((Object) path("second.third2").getTableInfo().reverseColumnInfo().name()) .isEqualTo(quoted("DUMMY_ENTITY")); - softly.assertThat(path("second.third").getTableInfo().reverseColumnInfo().name()) + softly.assertThat((Object) path("second.third").getTableInfo().reverseColumnInfo().name()) .isEqualTo(quoted("DUMMY_ENTITY")); - softly.assertThat(path("secondList.third2").getTableInfo().reverseColumnInfo().name()) + softly.assertThat((Object) path("secondList.third2").getTableInfo().reverseColumnInfo().name()) .isEqualTo(quoted("DUMMY_ENTITY")); - softly.assertThat(path("secondList.third").getTableInfo().reverseColumnInfo().name()) + softly.assertThat((Object) path("secondList.third").getTableInfo().reverseColumnInfo().name()) .isEqualTo(quoted("DUMMY_ENTITY")); softly.assertThat(path("second2.third").getTableInfo().reverseColumnInfo().name()) .isEqualTo(quoted("DUMMY_ENTITY")); @@ -140,6 +143,17 @@ void reverseColumnName() { }); } + @Test // GH-574 + void reverseColumnNames() { + + assertSoftly(softly -> { + softly.assertThat(path(CompoundIdEntity.class, "second").getTableInfo().reverseColumnInfos().toList(x -> x)) + .extracting(AggregatePath.ColumnInfo::name) + .containsExactlyInAnyOrder(quoted("COMPOUND_ID_ENTITY_ONE"), quoted("COMPOUND_ID_ENTITY_TWO")); + + }); + } + @Test // GH-1525 void getQualifierColumn() { @@ -172,8 +186,9 @@ void extendBy() { assertSoftly(softly -> { - softly.assertThat(path().append(entity.getRequiredPersistentProperty("withId"))).isEqualTo(path("withId")); - softly.assertThat(path("withId").append(path("withId").getRequiredIdProperty())) + softly.assertThat((Object) path().append(entity.getRequiredPersistentProperty("withId"))) + .isEqualTo(path("withId")); + softly.assertThat((Object) path("withId").append(path("withId").getRequiredIdProperty())) .isEqualTo(path("withId.withIdId")); }); } @@ -229,9 +244,9 @@ void isMultiValued() { softly.assertThat(path("second").isMultiValued()).isFalse(); softly.assertThat(path("second.third2").isMultiValued()).isFalse(); softly.assertThat(path("secondList.third2").isMultiValued()).isTrue(); // this seems wrong as third2 is an - // embedded path into Second, held by - // List (so the parent is - // multi-valued but not third2). + // embedded path into Second, held by + // List (so the parent is + // multi-valued but not third2). // TODO: This test fails because MultiValued considers parents. // softly.assertThat(path("secondList.third.value").isMultiValued()).isFalse(); softly.assertThat(path("secondList").isMultiValued()).isTrue(); @@ -306,13 +321,13 @@ void getTableAlias() { softly.assertThat(path("second.third2").getTableInfo().tableAlias()).isEqualTo(quoted("second")); softly.assertThat(path("second.third2.value").getTableInfo().tableAlias()).isEqualTo(quoted("second")); softly.assertThat(path("second.third").getTableInfo().tableAlias()).isEqualTo(quoted("second_third")); // missing - // _ + // _ softly.assertThat(path("second.third.value").getTableInfo().tableAlias()).isEqualTo(quoted("second_third")); // missing - // _ + // _ softly.assertThat(path("secondList.third2").getTableInfo().tableAlias()).isEqualTo(quoted("secondList")); softly.assertThat(path("secondList.third2.value").getTableInfo().tableAlias()).isEqualTo(quoted("secondList")); softly.assertThat(path("secondList.third").getTableInfo().tableAlias()).isEqualTo(quoted("secondList_third")); // missing - // _ + // _ softly.assertThat(path("secondList.third.value").getTableInfo().tableAlias()) .isEqualTo(quoted("secondList_third")); // missing _ softly.assertThat(path("secondList").getTableInfo().tableAlias()).isEqualTo(quoted("secondList")); @@ -416,20 +431,6 @@ void getBaseProperty() { }); } - @Test // GH-1525 - void getIdColumnName() { - - assertSoftly(softly -> { - - softly.assertThat(path().getTableInfo().idColumnName()).isEqualTo(quoted("ENTITY_ID")); - softly.assertThat(path("withId").getTableInfo().idColumnName()).isEqualTo(quoted("WITH_ID_ID")); - - softly.assertThat(path("second").getTableInfo().idColumnName()).isNull(); - softly.assertThat(path("second.third2").getTableInfo().idColumnName()).isNull(); - softly.assertThat(path("withId.second").getTableInfo().idColumnName()).isNull(); - }); - } - @Test // GH-1525 void toDotPath() { @@ -452,43 +453,89 @@ void getRequiredPersistentPropertyPath() { }); } - @Test // GH-1525 - void getEffectiveIdColumnName() { + @Test + // GH-1525 + void getLength() { assertSoftly(softly -> { + softly.assertThat(path().getLength()).isEqualTo(1); + softly.assertThat(path().stream().collect(Collectors.toList())).hasSize(1); - softly.assertThat(path().getTableInfo().effectiveIdColumnName()).isEqualTo(quoted("ENTITY_ID")); - softly.assertThat(path("second.third2").getTableInfo().effectiveIdColumnName()).isEqualTo(quoted("DUMMY_ENTITY")); - softly.assertThat(path("withId.second.third").getTableInfo().effectiveIdColumnName()) - .isEqualTo(quoted("WITH_ID")); - softly.assertThat(path("withId.second.third2.value").getTableInfo().effectiveIdColumnName()) - .isEqualTo(quoted("WITH_ID")); + softly.assertThat(path("second.third2").getLength()).isEqualTo(3); + softly.assertThat(path("second.third2").stream().collect(Collectors.toList())).hasSize(3); + + softly.assertThat(path("withId.second.third").getLength()).isEqualTo(4); + softly.assertThat(path("withId.second.third2.value").getLength()).isEqualTo(5); }); } - @Test // GH-1525 - void getLength() { + @Test // GH-574 + void getTail() { - assertThat(path().getLength()).isEqualTo(1); - assertThat(path().stream().collect(Collectors.toList())).hasSize(1); + assertSoftly(softly -> { - assertThat(path("second.third2").getLength()).isEqualTo(3); - assertThat(path("second.third2").stream().collect(Collectors.toList())).hasSize(3); + softly.assertThat((Object) path().getTail()).isEqualTo(null); + softly.assertThat((Object) path("second").getTail()).isEqualTo(null); + softly.assertThat(path("second.third").getTail().toDotPath()).isEqualTo("third"); + softly.assertThat(path("second.third.value").getTail().toDotPath()).isEqualTo("third.value"); + }); + } + + @Test // GH-74 + void append() { + + assertSoftly(softly -> { + + softly.assertThat(path("second").append(path()).toDotPath()).isEqualTo("second"); + softly.assertThat(path().append(path("second")).toDotPath()).isEqualTo("second"); + softly.assertThat(path().append(path("second.third")).toDotPath()).isEqualTo("second.third"); + AggregatePath value = path("second.third.value").getTail().getTail(); + softly.assertThat(path("second.third").append(value).toDotPath()).isEqualTo("second.third.value"); + }); + } + + @Test // GH-574 + void sortPaths() { + + Set sorted = new TreeSet<>(); + + AggregatePath alpha = path(); + AggregatePath as = path("second"); + AggregatePath ast = path("second.third"); + AggregatePath aw = path("withId"); + + sorted.add(aw); + sorted.add(ast); + sorted.add(as); + sorted.add(alpha); + + assertThat(sorted).containsExactly(alpha, as, ast, aw); - assertThat(path("withId.second.third").getLength()).isEqualTo(4); - assertThat(path("withId.second.third2.value").getLength()).isEqualTo(5); } private AggregatePath path() { return context.getAggregatePath(entity); } + private AggregatePath path(RelationalPersistentEntity entity) { + return context.getAggregatePath(entity); + } + + private AggregatePath path(Class entityType, String path) { + return context.getAggregatePath(createSimplePath(entityType, path)); + } + private AggregatePath path(String path) { return context.getAggregatePath(createSimplePath(path)); } PersistentPropertyPath createSimplePath(String path) { - return PersistentPropertyPathTestUtils.getPath(context, path, DummyEntity.class); + return createSimplePath(entity.getType(), path); + } + + PersistentPropertyPath createSimplePath(Class entityType, String path) { + + return PersistentPropertyPathTestUtils.getPath(context, path, entityType); } @SuppressWarnings("unused") @@ -502,6 +549,12 @@ static class DummyEntity { WithId withId; } + record CompoundId(Long one, String two) { + } + + record CompoundIdEntity(@Id CompoundId id, Second second) { + } + @SuppressWarnings("unused") static class Second { Third third; diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/RelationalMappingContextUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/RelationalMappingContextUnitTests.java index 4af641fb13..231c819cc7 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/RelationalMappingContextUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/RelationalMappingContextUnitTests.java @@ -60,7 +60,7 @@ public void canObtainAggregatePath() { EntityWithUuid.class); AggregatePath aggregatePath = context.getAggregatePath(path); - assertThat(aggregatePath).isNotNull(); + assertThat((Object) aggregatePath).isNotNull(); } @Test // GH-1525 @@ -75,7 +75,7 @@ public void innerAggregatePathsGetCached() { AggregatePath one = context.getAggregatePath(path); AggregatePath two = context.getAggregatePath(path); - assertThat(one).isSameAs(two); + assertThat((Object) one).isSameAs(two); } @Test // GH-1525 @@ -87,7 +87,7 @@ public void rootAggregatePathsGetCached() { AggregatePath one = context.getAggregatePath(context.getRequiredPersistentEntity(EntityWithUuid.class)); AggregatePath two = context.getAggregatePath(context.getRequiredPersistentEntity(EntityWithUuid.class)); - assertThat(one).isSameAs(two); + assertThat((Object) one).isSameAs(two); } @Test // GH-1586 @@ -117,7 +117,7 @@ void aggregatePathsOfBasePropertyForDifferentInheritedEntitiesAreDifferent() { AggregatePath aggregatePath1 = context.getAggregatePath(path1); AggregatePath aggregatePath2 = context.getAggregatePath(path2); - assertThat(aggregatePath1).isNotEqualTo(aggregatePath2); + assertThat((Object) aggregatePath1).isNotEqualTo(aggregatePath2); } static class EntityWithUuid { @@ -128,6 +128,14 @@ static class WithEmbedded { @Embedded.Empty(prefix = "prnt_") Parent parent; } + static class WithEmbeddedId { + @Embedded.Nullable + @Id CompositeId id; + } + + private record CompositeId(int a, int b) { + } + static class Parent { @Embedded.Empty(prefix = "chld_") Child child; @@ -144,5 +152,4 @@ static class Base { static class Inherit1 extends Base {} static class Inherit2 extends Base {} - } diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/TupleExpressionUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/TupleExpressionUnitTests.java new file mode 100644 index 0000000000..b69a9ee10e --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/TupleExpressionUnitTests.java @@ -0,0 +1,48 @@ +/* + * 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; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +class TupleExpressionUnitTests { + + @Test // GH-574 + void singleExpressionDoesNotGetWrapped() { + + Column testColumn = Column.create("name", Table.create("employee")); + + Expression wrapped = TupleExpression.maybeWrap(List.of(testColumn)); + + assertThat(wrapped).isSameAs(testColumn); + } + + @Test // GH-574 + void multipleExpressionsDoGetWrapped() { + + Column testColumn1 = Column.create("first", Table.create("employee")); + Column testColumn2 = Column.create("last", Table.create("employee")); + + Expression wrapped = TupleExpression.maybeWrap(List.of(testColumn1, testColumn2)); + + assertThat(wrapped).isInstanceOf(TupleExpression.class); + } + +} diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/DeleteRendererUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/DeleteRendererUnitTests.java index b451fea90b..09edd54b55 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/DeleteRendererUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/DeleteRendererUnitTests.java @@ -27,10 +27,10 @@ * * @author Mark Paluch */ -public class DeleteRendererUnitTests { +class DeleteRendererUnitTests { @Test // DATAJDBC-335 - public void shouldRenderWithoutWhere() { + void shouldRenderWithoutWhere() { Table bar = SQL.table("bar"); @@ -40,7 +40,7 @@ public void shouldRenderWithoutWhere() { } @Test // DATAJDBC-335 - public void shouldRenderWithCondition() { + void shouldRenderWithCondition() { Table table = Table.create("bar"); @@ -52,7 +52,7 @@ public void shouldRenderWithCondition() { } @Test // DATAJDBC-335 - public void shouldConsiderTableAlias() { + void shouldConsiderTableAlias() { Table table = Table.create("bar").as("my_bar"); diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/SelectRendererUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/SelectRendererUnitTests.java index 4f2121656e..788605a294 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/SelectRendererUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/SelectRendererUnitTests.java @@ -17,6 +17,8 @@ import static org.assertj.core.api.Assertions.*; +import java.util.List; + import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.data.relational.core.dialect.PostgresDialect; @@ -24,8 +26,6 @@ import org.springframework.data.relational.core.sql.*; import org.springframework.util.StringUtils; -import java.util.List; - /** * Unit tests for {@link SqlRenderer}. * @@ -115,196 +115,6 @@ void shouldRenderCountFunctionWithAliasedColumn() { assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT COUNT(bar.foo), bar.foo AS foo_bar FROM bar"); } - @Test // DATAJDBC-309 - void shouldRenderSimpleJoin() { - - Table employee = SQL.table("employee"); - Table department = SQL.table("department"); - - Select select = Select.builder().select(employee.column("id"), department.column("name")).from(employee) // - .join(department).on(employee.column("department_id")).equals(department.column("id")) // - .build(); - - assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT employee.id, department.name FROM employee " - + "JOIN department ON employee.department_id = department.id"); - } - - @Test // DATAJDBC-340 - void shouldRenderOuterJoin() { - - Table employee = SQL.table("employee"); - Table department = SQL.table("department"); - - Select select = Select.builder().select(employee.column("id"), department.column("name")) // - .from(employee) // - .leftOuterJoin(department).on(employee.column("department_id")).equals(department.column("id")) // - .build(); - - assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT employee.id, department.name FROM employee " - + "LEFT OUTER JOIN department ON employee.department_id = department.id"); - } - - @Test // GH-1421 - void shouldRenderFullOuterJoin() { - - Table employee = SQL.table("employee"); - Table department = SQL.table("department"); - - Select select = Select.builder().select(employee.column("id"), department.column("name")) // - .from(employee) // - .join(department, Join.JoinType.FULL_OUTER_JOIN).on(employee.column("department_id")) - .equals(department.column("id")) // - .build(); - - assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT employee.id, department.name FROM employee " - + "FULL OUTER JOIN department ON employee.department_id = department.id"); - } - - @Test // DATAJDBC-309 - void shouldRenderSimpleJoinWithAnd() { - - Table employee = SQL.table("employee"); - Table department = SQL.table("department"); - - Select select = Select.builder().select(employee.column("id"), department.column("name")).from(employee) // - .join(department).on(employee.column("department_id")).equals(department.column("id")) // - .and(employee.column("tenant")).equals(department.column("tenant")) // - .build(); - - assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT employee.id, department.name FROM employee " // - + "JOIN department ON employee.department_id = department.id " // - + "AND employee.tenant = department.tenant"); - } - - @Test // #995 - void shouldRenderArbitraryJoinCondition() { - - Table employee = SQL.table("employee"); - Table department = SQL.table("department"); - - Select select = Select.builder() // - .select(employee.column("id"), department.column("name")) // - .from(employee) // - .join(department) // - .on(Conditions.isEqual(employee.column("department_id"), department.column("id")) // - .or(Conditions.isNotEqual(employee.column("tenant"), department.column("tenant")) // - )).build(); - - assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT employee.id, department.name FROM employee " // - + "JOIN department ON employee.department_id = department.id " // - + "OR employee.tenant != department.tenant"); - } - - @Test // #1009 - void shouldRenderJoinWithJustExpression() { - - Table employee = SQL.table("employee"); - Table department = SQL.table("department"); - - Select select = Select.builder().select(employee.column("id"), department.column("name")).from(employee) // - .join(department).on(Expressions.just("alpha")).equals(Expressions.just("beta")) // - .build(); - - assertThat(SqlRenderer.toString(select)) - .isEqualTo("SELECT employee.id, department.name FROM employee " + "JOIN department ON alpha = beta"); - } - - @Test // DATAJDBC-309 - void shouldRenderMultipleJoinWithAnd() { - - Table employee = SQL.table("employee"); - Table department = SQL.table("department"); - Table tenant = SQL.table("tenant").as("tenant_base"); - - Select select = Select.builder().select(employee.column("id"), department.column("name")).from(employee) // - .join(department).on(employee.column("department_id")).equals(department.column("id")) // - .and(employee.column("tenant")).equals(department.column("tenant")) // - .join(tenant).on(tenant.column("tenant_id")).equals(department.column("tenant")) // - .build(); - - assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT employee.id, department.name FROM employee " // - + "JOIN department ON employee.department_id = department.id " // - + "AND employee.tenant = department.tenant " // - + "JOIN tenant tenant_base ON tenant_base.tenant_id = department.tenant"); - } - - @Test // GH-1003 - void shouldRenderJoinWithInlineQuery() { - - Table employee = SQL.table("employee"); - Table department = SQL.table("department"); - - Select innerSelect = Select.builder() - .select(employee.column("id"), employee.column("department_Id"), employee.column("name")).from(employee) - .build(); - - InlineQuery one = InlineQuery.create(innerSelect, "one"); - - Select select = Select.builder().select(one.column("id"), department.column("name")).from(department) // - .join(one).on(one.column("department_id")).equals(department.column("id")) // - .build(); - - String sql = SqlRenderer.toString(select); - - assertThat(sql).isEqualTo("SELECT one.id, department.name FROM department " // - + "JOIN (SELECT employee.id, employee.department_Id, employee.name FROM employee) one " // - + "ON one.department_id = department.id"); - } - - @Test // GH-1362 - void shouldRenderNestedJoins() { - - Table merchantCustomers = Table.create("merchants_customers"); - Table customerDetails = Table.create("customer_details"); - - Select innerSelect = Select.builder().select(customerDetails.column("cd_user_id")).from(customerDetails) - .join(merchantCustomers) - .on(merchantCustomers.column("mc_user_id").isEqualTo(customerDetails.column("cd_user_id"))).build(); - - InlineQuery innerTable = InlineQuery.create(innerSelect, "inner"); - - Select select = Select.builder().select(merchantCustomers.asterisk()) // - .from(merchantCustomers) // - .join(innerTable).on(innerTable.column("i_user_id").isEqualTo(merchantCustomers.column("mc_user_id"))) // - .build(); - - String sql = SqlRenderer.toString(select); - - assertThat(sql).isEqualTo("SELECT merchants_customers.* FROM merchants_customers " + // - "JOIN (" + // - "SELECT customer_details.cd_user_id " + // - "FROM customer_details " + // - "JOIN merchants_customers ON merchants_customers.mc_user_id = customer_details.cd_user_id" + // - ") inner " + // - "ON inner.i_user_id = merchants_customers.mc_user_id"); - } - - @Test // GH-1003 - void shouldRenderJoinWithTwoInlineQueries() { - - Table employee = SQL.table("employee"); - Table department = SQL.table("department"); - - Select innerSelectOne = Select.builder() - .select(employee.column("id").as("empId"), employee.column("department_Id"), employee.column("name")) - .from(employee).build(); - Select innerSelectTwo = Select.builder().select(department.column("id"), department.column("name")).from(department) - .build(); - - InlineQuery one = InlineQuery.create(innerSelectOne, "one"); - InlineQuery two = InlineQuery.create(innerSelectTwo, "two"); - - Select select = Select.builder().select(one.column("empId"), two.column("name")).from(one) // - .join(two).on(two.column("department_id")).equals(one.column("empId")) // - .build(); - - String sql = SqlRenderer.toString(select); - assertThat(sql).isEqualTo("SELECT one.empId, two.name FROM (" // - + "SELECT employee.id AS empId, employee.department_Id, employee.name FROM employee) one " // - + "JOIN (SELECT department.id, department.name FROM department) two " // - + "ON two.department_id = one.empId"); - } - @Test // DATAJDBC-309 void shouldRenderOrderByName() { @@ -424,7 +234,6 @@ void shouldRenderSimpleFunctionWithSubselect() { Table floo = SQL.table("floo"); Column bah = floo.column("bah"); - Select subselect = Select.builder().select(bah).from(floo).build(); SimpleFunction func = SimpleFunction.create("func", List.of(SubselectExpression.of(subselect))); @@ -435,8 +244,8 @@ void shouldRenderSimpleFunctionWithSubselect() { .where(Conditions.isEqual(func, SQL.literalOf(23))) // .build(); - assertThat(SqlRenderer.toString(select)) - .isEqualTo("SELECT func(SELECT floo.bah FROM floo) AS alias FROM foo WHERE func(SELECT floo.bah FROM floo) = 23"); + assertThat(SqlRenderer.toString(select)).isEqualTo( + "SELECT func(SELECT floo.bah FROM floo) AS alias FROM foo WHERE func(SELECT floo.bah FROM floo) = 23"); } @Test // DATAJDBC-309 @@ -709,7 +518,7 @@ void asteriskOfAliasedTableUsesAlias() { assertThat(rendered).isEqualTo("SELECT e.*, e.id FROM employee e"); } - @Test + @Test // GH-1844 void rendersCaseExpression() { Table table = SQL.table("table"); @@ -724,7 +533,225 @@ void rendersCaseExpression() { .build(); String rendered = SqlRenderer.toString(select); - assertThat(rendered).isEqualTo("SELECT CASE WHEN table.name IS NULL THEN 1 WHEN table.name IS NOT NULL THEN table.name ELSE 3 END FROM table"); + assertThat(rendered).isEqualTo( + "SELECT CASE WHEN table.name IS NULL THEN 1 WHEN table.name IS NOT NULL THEN table.name ELSE 3 END FROM table"); + } + + @Test // GH-574 + void rendersTupleExpression() { + + Table table = SQL.table("table"); + Column first = table.column("first"); + Column middle = table.column("middle"); + Column last = table.column("last").as("anAlias"); + + TupleExpression tupleExpression = TupleExpression.create(first, SQL.literalOf(1), middle, last); // + + Select select = StatementBuilder.select(first) // + .from(table) // + .where(Conditions.in(tupleExpression, Expressions.just("some expression"))).build(); + + String rendered = SqlRenderer.toString(select); + assertThat(rendered).isEqualTo( + "SELECT table.first FROM table WHERE (table.first, 1, table.middle, table.last) IN (some expression)"); + } + + /** + * Tests for rendering joins. + */ + @Nested + class JoinsTests { + + @Test // DATAJDBC-309 + void shouldRenderSimpleJoin() { + + Table employee = SQL.table("employee"); + Table department = SQL.table("department"); + + Select select = Select.builder().select(employee.column("id"), department.column("name")).from(employee) // + .join(department).on(employee.column("department_id")).equals(department.column("id")) // + .build(); + + assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT employee.id, department.name FROM employee " + + "JOIN department ON employee.department_id = department.id"); + } + + @Test // DATAJDBC-340 + void shouldRenderOuterJoin() { + + Table employee = SQL.table("employee"); + Table department = SQL.table("department"); + + Select select = Select.builder().select(employee.column("id"), department.column("name")) // + .from(employee) // + .leftOuterJoin(department).on(employee.column("department_id")).equals(department.column("id")) // + .build(); + + assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT employee.id, department.name FROM employee " + + "LEFT OUTER JOIN department ON employee.department_id = department.id"); + } + + @Test // GH-1421 + void shouldRenderFullOuterJoin() { + + Table employee = SQL.table("employee"); + Table department = SQL.table("department"); + + Select select = Select.builder().select(employee.column("id"), department.column("name")) // + .from(employee) // + .join(department, Join.JoinType.FULL_OUTER_JOIN).on(employee.column("department_id")) + .equals(department.column("id")) // + .build(); + + assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT employee.id, department.name FROM employee " + + "FULL OUTER JOIN department ON employee.department_id = department.id"); + } + + @Test // DATAJDBC-309 + void shouldRenderSimpleJoinWithAnd() { + + Table employee = SQL.table("employee"); + Table department = SQL.table("department"); + + Select select = Select.builder().select(employee.column("id"), department.column("name")).from(employee) // + .join(department).on(employee.column("department_id")).equals(department.column("id")) // + .and(employee.column("tenant")).equals(department.column("tenant")) // + .build(); + + assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT employee.id, department.name FROM employee " // + + "JOIN department ON employee.department_id = department.id " // + + "AND employee.tenant = department.tenant"); + } + + @Test // #995 + void shouldRenderArbitraryJoinCondition() { + + Table employee = SQL.table("employee"); + Table department = SQL.table("department"); + + Select select = Select.builder() // + .select(employee.column("id"), department.column("name")) // + .from(employee) // + .join(department) // + .on(Conditions.isEqual(employee.column("department_id"), department.column("id")) // + .or(Conditions.isNotEqual(employee.column("tenant"), department.column("tenant")) // + )).build(); + + assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT employee.id, department.name FROM employee " // + + "JOIN department ON employee.department_id = department.id " // + + "OR employee.tenant != department.tenant"); + } + + @Test // #1009 + void shouldRenderJoinWithJustExpression() { + + Table employee = SQL.table("employee"); + Table department = SQL.table("department"); + + Select select = Select.builder().select(employee.column("id"), department.column("name")).from(employee) // + .join(department).on(Expressions.just("alpha")).equals(Expressions.just("beta")) // + .build(); + + assertThat(SqlRenderer.toString(select)) + .isEqualTo("SELECT employee.id, department.name FROM employee " + "JOIN department ON alpha = beta"); + } + + @Test // DATAJDBC-309 + void shouldRenderMultipleJoinWithAnd() { + + Table employee = SQL.table("employee"); + Table department = SQL.table("department"); + Table tenant = SQL.table("tenant").as("tenant_base"); + + Select select = Select.builder().select(employee.column("id"), department.column("name")).from(employee) // + .join(department).on(employee.column("department_id")).equals(department.column("id")) // + .and(employee.column("tenant")).equals(department.column("tenant")) // + .join(tenant).on(tenant.column("tenant_id")).equals(department.column("tenant")) // + .build(); + + assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT employee.id, department.name FROM employee " // + + "JOIN department ON employee.department_id = department.id " // + + "AND employee.tenant = department.tenant " // + + "JOIN tenant tenant_base ON tenant_base.tenant_id = department.tenant"); + } + + @Test // GH-1003 + void shouldRenderJoinWithInlineQuery() { + + Table employee = SQL.table("employee"); + Table department = SQL.table("department"); + + Select innerSelect = Select.builder() + .select(employee.column("id"), employee.column("department_Id"), employee.column("name")).from(employee) + .build(); + + InlineQuery one = InlineQuery.create(innerSelect, "one"); + + Select select = Select.builder().select(one.column("id"), department.column("name")).from(department) // + .join(one).on(one.column("department_id")).equals(department.column("id")) // + .build(); + + String sql = SqlRenderer.toString(select); + + assertThat(sql).isEqualTo("SELECT one.id, department.name FROM department " // + + "JOIN (SELECT employee.id, employee.department_Id, employee.name FROM employee) one " // + + "ON one.department_id = department.id"); + } + + @Test // GH-1362 + void shouldRenderNestedJoins() { + + Table merchantCustomers = Table.create("merchants_customers"); + Table customerDetails = Table.create("customer_details"); + + Select innerSelect = Select.builder().select(customerDetails.column("cd_user_id")).from(customerDetails) + .join(merchantCustomers) + .on(merchantCustomers.column("mc_user_id").isEqualTo(customerDetails.column("cd_user_id"))).build(); + + InlineQuery innerTable = InlineQuery.create(innerSelect, "inner"); + + Select select = Select.builder().select(merchantCustomers.asterisk()) // + .from(merchantCustomers) // + .join(innerTable).on(innerTable.column("i_user_id").isEqualTo(merchantCustomers.column("mc_user_id"))) // + .build(); + + String sql = SqlRenderer.toString(select); + + assertThat(sql).isEqualTo("SELECT merchants_customers.* FROM merchants_customers " + // + "JOIN (" + // + "SELECT customer_details.cd_user_id " + // + "FROM customer_details " + // + "JOIN merchants_customers ON merchants_customers.mc_user_id = customer_details.cd_user_id" + // + ") inner " + // + "ON inner.i_user_id = merchants_customers.mc_user_id"); + } + + @Test // GH-1003 + void shouldRenderJoinWithTwoInlineQueries() { + + Table employee = SQL.table("employee"); + Table department = SQL.table("department"); + + Select innerSelectOne = Select.builder() + .select(employee.column("id").as("empId"), employee.column("department_Id"), employee.column("name")) + .from(employee).build(); + Select innerSelectTwo = Select.builder().select(department.column("id"), department.column("name")) + .from(department).build(); + + InlineQuery one = InlineQuery.create(innerSelectOne, "one"); + InlineQuery two = InlineQuery.create(innerSelectTwo, "two"); + + Select select = Select.builder().select(one.column("empId"), two.column("name")).from(one) // + .join(two).on(two.column("department_id")).equals(one.column("empId")) // + .build(); + + String sql = SqlRenderer.toString(select); + assertThat(sql).isEqualTo("SELECT one.empId, two.name FROM (" // + + "SELECT employee.id AS empId, employee.department_Id, employee.name FROM employee) one " // + + "JOIN (SELECT department.id, department.name FROM department) two " // + + "ON two.department_id = one.empId"); + } + } /** @@ -742,8 +769,8 @@ class AnalyticFunctionsTests { void renderEmptyOver() { Select select = StatementBuilder.select( // - AnalyticFunction.create("MAX", salary) // - ) // + AnalyticFunction.create("MAX", salary) // + ) // .from(employee) // .build(); 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 7ec6678f8b..2ed989e332 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 @@ -20,11 +20,13 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.springframework.data.annotation.Id; import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.RelationalMappingContext; /** * Unit tests for the {@link AliasFactory}. + * * @author Jens Schauder */ class AliasFactoryUnitTests { @@ -55,8 +57,8 @@ void aliasSimpleProperty() { @Test // GH-1446 void nameGetsSanitized() { - String alias = aliasFactory.getColumnAlias( - context.getAggregatePath( context.getPersistentPropertyPath("evil", DummyEntity.class))); + String alias = aliasFactory + .getColumnAlias(context.getAggregatePath(context.getPersistentPropertyPath("evil", DummyEntity.class))); assertThat(alias).isEqualTo("c_ameannamecontains3illegal_characters_1"); } @@ -64,10 +66,10 @@ void nameGetsSanitized() { @Test // GH-1446 void aliasIsStable() { - String alias1 = aliasFactory.getColumnAlias( - context.getAggregatePath( context.getRequiredPersistentEntity(DummyEntity.class))); - String alias2 = aliasFactory.getColumnAlias( - context.getAggregatePath( context.getRequiredPersistentEntity(DummyEntity.class))); + String alias1 = aliasFactory + .getColumnAlias(context.getAggregatePath(context.getRequiredPersistentEntity(DummyEntity.class))); + String alias2 = aliasFactory + .getColumnAlias(context.getAggregatePath(context.getRequiredPersistentEntity(DummyEntity.class))); assertThat(alias1).isEqualTo(alias2); } @@ -79,10 +81,10 @@ class RnAlias { @Test // GH-1446 void aliasIsStable() { - String alias1 = aliasFactory.getRowNumberAlias( - context.getAggregatePath(context.getRequiredPersistentEntity(DummyEntity.class))); - String alias2 = aliasFactory.getRowNumberAlias( - context.getAggregatePath( context.getRequiredPersistentEntity(DummyEntity.class))); + String alias1 = aliasFactory + .getRowNumberAlias(context.getAggregatePath(context.getRequiredPersistentEntity(DummyEntity.class))); + String alias2 = aliasFactory + .getRowNumberAlias(context.getAggregatePath(context.getRequiredPersistentEntity(DummyEntity.class))); assertThat(alias1).isEqualTo(alias2); } @@ -90,11 +92,11 @@ void aliasIsStable() { @Test // GH-1446 void aliasProjectsOnTableReferencingPath() { - String alias1 = aliasFactory.getRowNumberAlias( - context.getAggregatePath(context.getRequiredPersistentEntity(DummyEntity.class))); + String alias1 = aliasFactory + .getRowNumberAlias(context.getAggregatePath(context.getRequiredPersistentEntity(DummyEntity.class))); - String alias2 = aliasFactory.getRowNumberAlias( - context.getAggregatePath(context.getPersistentPropertyPath("evil", DummyEntity.class))); + String alias2 = aliasFactory + .getRowNumberAlias(context.getAggregatePath(context.getPersistentPropertyPath("evil", DummyEntity.class))); assertThat(alias1).isEqualTo(alias2); } @@ -102,10 +104,10 @@ void aliasProjectsOnTableReferencingPath() { @Test // GH-1446 void rnAliasIsIndependentOfTableAlias() { - String alias1 = aliasFactory.getRowNumberAlias( - context.getAggregatePath(context.getRequiredPersistentEntity(DummyEntity.class))); - String alias2 = aliasFactory.getColumnAlias( - context.getAggregatePath(context.getRequiredPersistentEntity(DummyEntity.class))); + String alias1 = aliasFactory + .getRowNumberAlias(context.getAggregatePath(context.getRequiredPersistentEntity(DummyEntity.class))); + String alias2 = aliasFactory + .getColumnAlias(context.getAggregatePath(context.getRequiredPersistentEntity(DummyEntity.class))); assertThat(alias1).isNotEqualTo(alias2); } @@ -117,8 +119,8 @@ class BackReferenceAlias { @Test // GH-1446 void testBackReferenceAlias() { - String alias = aliasFactory.getBackReferenceAlias( - context.getAggregatePath(context.getPersistentPropertyPath("dummy", Reference.class))); + String alias = aliasFactory + .getBackReferenceAlias(context.getAggregatePath(context.getPersistentPropertyPath("dummy", Reference.class))); assertThat(alias).isEqualTo("br_dummy_entity_1"); } @@ -129,8 +131,8 @@ class KeyAlias { @Test // GH-1446 void testKeyAlias() { - String alias = aliasFactory.getKeyAlias( - context.getAggregatePath(context.getPersistentPropertyPath("dummy", Reference.class))); + String alias = aliasFactory + .getKeyAlias(context.getAggregatePath(context.getPersistentPropertyPath("dummy", Reference.class))); assertThat(alias).isEqualTo("key_dummy_entity_1"); } @@ -141,11 +143,11 @@ class TableAlias { @Test // GH-1448 void tableAliasIsDifferentForDifferentPathsToSameEntity() { - String alias = aliasFactory.getTableAlias( - context.getAggregatePath(context.getPersistentPropertyPath("dummy", Reference.class))); + String alias = aliasFactory + .getTableAlias(context.getAggregatePath(context.getPersistentPropertyPath("dummy", Reference.class))); - String alias2 = aliasFactory.getTableAlias( - context.getAggregatePath(context.getPersistentPropertyPath("dummy2", Reference.class))); + String alias2 = aliasFactory + .getTableAlias(context.getAggregatePath(context.getPersistentPropertyPath("dummy2", Reference.class))); assertThat(alias).isNotEqualTo(alias2); } @@ -158,6 +160,7 @@ static class DummyEntity { } static class Reference { + @Id Long id; DummyEntity dummy; DummyEntity dummy2; } diff --git a/src/main/antora/modules/ROOT/pages/jdbc/mapping.adoc b/src/main/antora/modules/ROOT/pages/jdbc/mapping.adoc index c3bba01ca0..c41b6cd42b 100644 --- a/src/main/antora/modules/ROOT/pages/jdbc/mapping.adoc +++ b/src/main/antora/modules/ROOT/pages/jdbc/mapping.adoc @@ -106,6 +106,9 @@ Also, the type of that aggregate is encoded in a type parameter. All references in an aggregate result in a foreign key relationship in the opposite direction in the database. By default, the name of the foreign key column is the table name of the referencing entity. +If the referenced id is an `@Embedded` id, the back reference consists of multiple columns, each named by a concatenation of + `_` + . +E.g. the back reference to a `Person` entity, with a composite id with the properties `firstName` and `lastName` will consist of the two columns `PERSON_FIRST_NAME` and `PERSON_LAST_NAME`. + Alternatively you may choose to have them named by the entity name of the referencing entity ignoring `@Table` annotations. You activate this behaviour by calling `setForeignKeyNaming(ForeignKeyNaming.IGNORE_RENAMING)` on the `RelationalMappingContext`. diff --git a/src/main/antora/modules/ROOT/partials/mapping.adoc b/src/main/antora/modules/ROOT/partials/mapping.adoc index 7e864516e2..ed80c37fab 100644 --- a/src/main/antora/modules/ROOT/partials/mapping.adoc +++ b/src/main/antora/modules/ROOT/partials/mapping.adoc @@ -149,6 +149,13 @@ Embedded entities containing a `Collection` or a `Map` will always be considered Such an entity will therefore never be `null` even when using @Embedded(onEmpty = USE_NULL). endif::[] +[[entity-persistence.embedded-ids]] +=== Embedded Ids + +Entities may be annotated with `@Id` and `@Embedded`, resulting in a composite id on the database side. +The full embedded entity is considered the id, and therefore the check for determining if an aggregate is considered a new aggregate requiring an insert or an existing one, asking for an update is based on that entity, not its elements. +Most use cases will require a custom `BeforeConvertCallback` to set the id for new aggregate. + [[entity-persistence.read-only-properties]] == Read Only Properties From 5fc12fe2646e047c082b541f9ad7f0a56cdbcacd Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Tue, 11 Feb 2025 15:18:52 +0100 Subject: [PATCH 17/21] Improved Composite Id support. Adds support for id generation by sequence as part of a composite id. Added a proper test for sorting by composite id element. Added a stand in test for projection by composite id element. The latter does not test the actual intended behaviour since projection don't work as intended yet. See https://github.com/spring-projects/spring-data-relational/issues/1821 Original pull request #1957 See #574 --- ...AggregateTemplateHsqlIntegrationTests.java | 50 +++++++ .../support/SimpleR2dbcRepository.java | 34 ++++- ...CompositeIdRepositoryIntegrationTests.java | 122 ++++++++++++++++++ 3 files changed, 202 insertions(+), 4 deletions(-) create mode 100644 spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/CompositeIdRepositoryIntegrationTests.java diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/CompositeIdAggregateTemplateHsqlIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/CompositeIdAggregateTemplateHsqlIntegrationTests.java index dc10524878..e053bc091f 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/CompositeIdAggregateTemplateHsqlIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/CompositeIdAggregateTemplateHsqlIntegrationTests.java @@ -18,7 +18,9 @@ import static org.assertj.core.api.Assertions.*; import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; @@ -26,6 +28,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.data.annotation.Id; +import org.springframework.data.domain.Sort; import org.springframework.data.jdbc.core.convert.DataAccessStrategy; import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.testing.DatabaseType; @@ -34,6 +37,7 @@ import org.springframework.data.jdbc.testing.TestConfiguration; import org.springframework.data.relational.core.mapping.Embedded; import org.springframework.data.relational.core.mapping.RelationalMappingContext; +import org.springframework.data.relational.core.query.Query; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; /** @@ -202,6 +206,52 @@ void saveAndLoadWithListAndCompositeId() { assertThat(reloaded).isEqualTo(entity); } + @Test // GH-574 + void sortByCompositeIdParts() { + + SimpleEntityWithEmbeddedPk alpha = template.insert( // + new SimpleEntityWithEmbeddedPk( // + new EmbeddedPk(23L, "x"), "alpha" // + )); + SimpleEntityWithEmbeddedPk bravo = template.insert( // + new SimpleEntityWithEmbeddedPk( // + new EmbeddedPk(22L, "a"), "bravo" // + )); + SimpleEntityWithEmbeddedPk charlie = template.insert( // + new SimpleEntityWithEmbeddedPk( // + new EmbeddedPk(21L, "z"), "charlie" // + ) // + ); + + assertThat( // + template.findAll(SimpleEntityWithEmbeddedPk.class, Sort.by("embeddedPk.one"))) // + .containsExactly( // + charlie, bravo, alpha // + ); + + assertThat( // + template.findAll(SimpleEntityWithEmbeddedPk.class, Sort.by("embeddedPk.two").descending())) // + .containsExactly( // + charlie, alpha, bravo // + ); + } + + @Test // GH-574 + void projectByCompositeIdParts() { + + SimpleEntityWithEmbeddedPk alpha = template.insert( // + new SimpleEntityWithEmbeddedPk( // + new EmbeddedPk(23L, "x"), "alpha" // + )); + + Query projectingQuery = Query.empty().columns( "embeddedPk.two", "name"); + SimpleEntityWithEmbeddedPk projected = template.findOne(projectingQuery, SimpleEntityWithEmbeddedPk.class).orElseThrow(); + + // Projection still does a full select, otherwise one would be null. + // See https://github.com/spring-projects/spring-data-relational/issues/1821 + assertThat(projected).isEqualTo(new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "x"), "alpha")); + } + private record WrappedPk(Long id) { } diff --git a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java index 3508facbb8..e3b33e4547 100644 --- a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java +++ b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java @@ -31,12 +31,16 @@ import org.springframework.data.domain.ScrollPosition; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Window; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.PropertyHandler; +import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.r2dbc.convert.R2dbcConverter; import org.springframework.data.r2dbc.core.R2dbcEntityOperations; import org.springframework.data.r2dbc.core.R2dbcEntityTemplate; import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy; import org.springframework.data.r2dbc.core.ReactiveSelectOperation; import org.springframework.data.r2dbc.repository.R2dbcRepository; +import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.core.query.Criteria; import org.springframework.data.relational.core.query.Query; @@ -67,6 +71,7 @@ public class SimpleR2dbcRepository implements R2dbcRepository { private final R2dbcEntityOperations entityOperations; private final Lazy idProperty; private final RelationalExampleMapper exampleMapper; + private MappingContext, ? extends RelationalPersistentProperty> mappingContext; /** * Create a new {@link SimpleR2dbcRepository}. @@ -81,11 +86,11 @@ public SimpleR2dbcRepository(RelationalEntityInformation entity, R2dbcEnt this.entity = entity; this.entityOperations = entityOperations; - this.idProperty = Lazy.of(() -> converter // - .getMappingContext() // + this.mappingContext = converter.getMappingContext(); + this.idProperty = Lazy.of(() -> mappingContext // .getRequiredPersistentEntity(this.entity.getJavaType()) // .getRequiredIdProperty()); - this.exampleMapper = new RelationalExampleMapper(converter.getMappingContext()); + this.exampleMapper = new RelationalExampleMapper(mappingContext); } /** @@ -359,7 +364,28 @@ private RelationalPersistentProperty getIdProperty() { } private Query getIdQuery(Object id) { - return Query.query(Criteria.where(getIdProperty().getName()).is(id)); + + Criteria criteria; + + RelationalPersistentProperty idProperty = getIdProperty(); + if (idProperty.isEmbedded()) { + + Criteria[] criteriaHolder = new Criteria[] { Criteria.empty() }; + + RelationalPersistentEntity idEntity = mappingContext.getRequiredPersistentEntity(idProperty.getType()); + PersistentPropertyAccessor accessor = idEntity.getPropertyAccessor(id); + idEntity.doWithProperties(new PropertyHandler() { + @Override + public void doWithPersistentProperty(RelationalPersistentProperty persistentProperty) { + criteriaHolder[0] = criteriaHolder [0].and(persistentProperty.getName()).is(accessor.getProperty(persistentProperty)); + } + }); + criteria = criteriaHolder[0]; + } else { + criteria = Criteria.where(idProperty.getName()).is(id); + } + + return Query.query(criteria); } /** diff --git a/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/CompositeIdRepositoryIntegrationTests.java b/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/CompositeIdRepositoryIntegrationTests.java new file mode 100644 index 0000000000..eeedcf1355 --- /dev/null +++ b/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/CompositeIdRepositoryIntegrationTests.java @@ -0,0 +1,122 @@ +/* + * Copyright 2019-2025 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.r2dbc.repository; + +import io.r2dbc.spi.ConnectionFactory; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.FilterType; +import org.springframework.dao.DataAccessException; +import org.springframework.data.annotation.Id; +import org.springframework.data.r2dbc.config.AbstractR2dbcConfiguration; +import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories; +import org.springframework.data.r2dbc.testing.H2TestSupport; +import org.springframework.data.relational.core.mapping.Embedded; +import org.springframework.data.relational.core.mapping.Table; +import org.springframework.data.repository.reactive.ReactiveCrudRepository; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import reactor.test.StepVerifier; + +import static org.assertj.core.api.Assertions.*; + +/** + * Integration tests for repositories of entities with a composite id. + * + * @author Jens Schauder + */ +@ExtendWith(SpringExtension.class) +public class CompositeIdRepositoryIntegrationTests { + + @Autowired private WithCompositeIdRepository repository; + private JdbcTemplate jdbc; + + @Configuration + @EnableR2dbcRepositories(includeFilters = @ComponentScan.Filter(value = WithCompositeIdRepository.class, + type = FilterType.ASSIGNABLE_TYPE), considerNestedRepositories = true) + static class TestConfiguration extends AbstractR2dbcConfiguration { + @Override + public ConnectionFactory connectionFactory() { + return H2TestSupport.createConnectionFactory(); + } + + } + + @BeforeEach + void before() { + + this.jdbc = new JdbcTemplate(createDataSource()); + + try { + this.jdbc.execute("DROP TABLE with_composite_id"); + } catch (DataAccessException e) {} + + this.jdbc.execute(""" + CREATE TABLE with_composite_id ( + one int, + two varchar(255), + name varchar(255), + primary key (one, two))"""); + this.jdbc.execute("INSERT INTO with_composite_id VALUES (42, 'HBAR','Walter')"); + this.jdbc.execute("INSERT INTO with_composite_id VALUES (23, '2PI','Jesse')"); + } + + /** + * Creates a {@link DataSource} to be used in this test. + * + * @return the {@link DataSource} to be used in this test. + */ + protected DataSource createDataSource() { + return H2TestSupport.createDataSource(); + } + + /** + * Creates a {@link ConnectionFactory} to be used in this test. + * + * @return the {@link ConnectionFactory} to be used in this test. + */ + protected ConnectionFactory createConnectionFactory() { + return H2TestSupport.createConnectionFactory(); + } + + @Test // GH-574 + void findAllById() { + repository.findById(new CompositeId(42, "HBAR")) // + .as(StepVerifier::create) // + .consumeNextWith(actual ->{ + assertThat(actual.name).isEqualTo("Walter"); + }).verifyComplete(); + } + + interface WithCompositeIdRepository extends ReactiveCrudRepository { + + } + + @Table("with_composite_id") + record WithCompositeId(@Id @Embedded.Nullable CompositeId pk, String name) { + } + + record CompositeId(Integer one, String two) { + } + +} From d99f65e65af593c342b96fd1e7e86e2f0295b9ee Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Wed, 12 Mar 2025 15:20:23 +0100 Subject: [PATCH 18/21] Polishing. Removed unused assignments. Original pull request #1957 See #574 --- .../data/r2dbc/core/R2dbcEntityTemplateUnitTests.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/core/R2dbcEntityTemplateUnitTests.java b/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/core/R2dbcEntityTemplateUnitTests.java index 77707d4dda..8c11f18781 100644 --- a/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/core/R2dbcEntityTemplateUnitTests.java +++ b/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/core/R2dbcEntityTemplateUnitTests.java @@ -299,8 +299,6 @@ void shouldSelectOneDoNotOverrideExistingLimit() { @Test // GH-220 void shouldUpdateByQuery() { - MockRowMetadata metadata = MockRowMetadata.builder() - .columnMetadata(MockColumnMetadata.builder().name("name").type(R2dbcType.VARCHAR).build()).build(); MockResult result = MockResult.builder().rowsUpdated(1).build(); recorder.addStubbing(s -> s.startsWith("UPDATE"), result); @@ -321,8 +319,6 @@ void shouldUpdateByQuery() { @Test // GH-220 void shouldDeleteByQuery() { - MockRowMetadata metadata = MockRowMetadata.builder() - .columnMetadata(MockColumnMetadata.builder().name("name").type(R2dbcType.VARCHAR).build()).build(); MockResult result = MockResult.builder().rowsUpdated(1).build(); recorder.addStubbing(s -> s.startsWith("DELETE"), result); From effcf8ccf73fd26920115b096adc6afd1c231d58 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 19 Mar 2025 15:47:33 +0100 Subject: [PATCH 19/21] Polishing. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce ColumnInfos.reduce(…) operation. --- .../data/jdbc/core/convert/Identifier.java | 20 +++++++++++ .../core/convert/JdbcIdentifierBuilder.java | 12 +++---- .../core/mapping/AggregatePath.java | 35 +++++++++++++++++-- 3 files changed, 58 insertions(+), 9 deletions(-) diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/Identifier.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/Identifier.java index 5f9284a54b..56f0c1a90d 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/Identifier.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/Identifier.java @@ -99,6 +99,25 @@ public static Identifier from(Map map) { return new Identifier(Collections.unmodifiableList(values)); } + /** + * Creates a new {@link Identifier} from the current instance and sets the value from {@link Identifier}. Existing key + * definitions for {@code name} are overwritten if they already exist. + * + * @param identifier the identifier to append. + * @return the {@link Identifier} containing all existing keys and the key part for {@code name}, {@code value}, and a + * {@link Class target type}. + * @since 3.5 + */ + public Identifier withPart(Identifier identifier) { + + Identifier result = this; + for (SingleIdentifierValue part : identifier.getParts()) { + result = result.withPart(part.getName(), part.getValue(), part.getTargetType()); + } + + return result; + } + /** * Creates a new {@link Identifier} from the current instance and sets the value for {@code key}. Existing key * definitions for {@code name} are overwritten if they already exist. @@ -188,6 +207,7 @@ public Object get(SqlIdentifier columnName) { return null; } + /** * A single value of an Identifier consisting of the column name, the value and the target type which is to be used to * store the element in the database. diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilder.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilder.java index 41e6b2c488..ef0ff6f467 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilder.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilder.java @@ -48,7 +48,7 @@ public static JdbcIdentifierBuilder empty() { public static JdbcIdentifierBuilder forBackReferences(JdbcConverter converter, AggregatePath path, Object value) { RelationalPersistentProperty idProperty = path.getIdDefiningParentPath().getRequiredIdProperty(); - AggregatePath.ColumnInfos reverseColumnInfos = path.getTableInfo().reverseColumnInfos(); + AggregatePath.ColumnInfos infos = path.getTableInfo().reverseColumnInfos(); // create property accessor RelationalMappingContext mappingContext = converter.getMappingContext(); @@ -62,16 +62,14 @@ public static JdbcIdentifierBuilder forBackReferences(JdbcConverter converter, A valueProvider = ap -> propertyPathAccessor.getProperty(ap.getRequiredPersistentPropertyPath()); } - final Identifier[] identifierHolder = new Identifier[] { Identifier.empty() }; - - reverseColumnInfos.forEach((ap, ci) -> { + Identifier identifierHolder = infos.reduce(Identifier.empty(), (ap, ci) -> { RelationalPersistentProperty property = ap.getRequiredLeafProperty(); - identifierHolder[0] = identifierHolder[0].withPart(ci.name(), valueProvider.apply(ap), + return Identifier.of(ci.name(), valueProvider.apply(ap), converter.getColumnType(property)); - }); + }, Identifier::withPart); - return new JdbcIdentifierBuilder(identifierHolder[0]); + return new JdbcIdentifierBuilder(identifierHolder); } /** diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/AggregatePath.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/AggregatePath.java index f721e34eea..4ea0e38b90 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/AggregatePath.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/AggregatePath.java @@ -23,6 +23,7 @@ import java.util.TreeMap; import java.util.function.BiConsumer; import java.util.function.BiFunction; +import java.util.function.BinaryOperator; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Stream; @@ -475,14 +476,44 @@ public List toList(Function mapper) { return columnInfos.values().stream().map(mapper).toList(); } + /** + * Performs a {@link Stream#reduce(Object, BiFunction, BinaryOperator)} on {@link ColumnInfo} and + * {@link AggregatePath} to reduce the results into a single {@code T} return value. + *

+ * If {@code ColumnInfos} is empty, then {@code identity} is returned. Without invoking {@code combiner}. The + * {@link BinaryOperator combiner} is called with the current state (or initial {@code identity}) and the + * accumulated {@code T} state to combine both into a single return value. + * + * @param identity the identity (initial) value for the combiner function. + * @param accumulator an associative, non-interfering (free of side effects), stateless function for incorporating + * an additional element into a result. + * @param combiner an associative, non-interfering, stateless function for combining two values, which must be + * compatible with the {@code accumulator} function. + * @return result of the function. + * @param type of the result. + * @since 3.5 + */ + public T reduce(T identity, BiFunction accumulator, BinaryOperator combiner) { + + T result = identity; + + for (Map.Entry entry : columnInfos.entrySet()) { + + T mapped = accumulator.apply(entry.getKey(), entry.getValue()); + result = combiner.apply(result, mapped); + } + + return result; + } + public void forEach(BiConsumer consumer) { columnInfos.forEach(consumer); } - public T any(BiFunction consumer) { + public T any(BiFunction mapper) { Map.Entry any = columnInfos.entrySet().iterator().next(); - return consumer.apply(any.getKey(), any.getValue()); + return mapper.apply(any.getKey(), any.getValue()); } public ColumnInfo get(AggregatePath path) { From eadd4f1f932ca20e21e7466b2530cad4e600c555 Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Thu, 20 Mar 2025 15:26:55 +0100 Subject: [PATCH 20/21] Applied changes requested in review. The major ones are: * directly construct joins * remove multiple places of duplication * naming * documentation Original pull request #1957 See #574 --- .../JdbcAggregateChangeExecutionContext.java | 91 +++--- .../core/convert/JdbcIdentifierBuilder.java | 52 +-- .../core/convert/MappingJdbcConverter.java | 65 ++-- .../data/jdbc/core/convert/SqlContext.java | 2 +- .../data/jdbc/core/convert/SqlGenerator.java | 123 ++++--- .../jdbc/core/convert/SqlGeneratorSource.java | 3 +- .../core/convert/SqlParametersFactory.java | 117 +++---- .../query/JdbcDeleteQueryCreator.java | 20 +- .../repository/query/JdbcQueryCreator.java | 149 ++------- .../jdbc/repository/query/SqlContext.java | 2 +- ...angeExecutorContextImmutableUnitTests.java | 7 +- ...gregateChangeExecutorContextUnitTests.java | 3 +- .../JdbcIdentifierBuilderUnitTests.java | 33 +- .../SqlGeneratorEmbeddedUnitTests.java | 95 +++--- .../core/convert/SqlGeneratorUnitTests.java | 60 ++-- .../core/mapping/AggregatePath.java | 300 +++++++++++------- .../core/mapping/ColumInfosBuilder.java | 84 +++++ .../core/mapping/DefaultAggregatePath.java | 9 +- .../mapping/RelationalPersistentEntity.java | 2 +- .../relational/core/sql/AnalyticFunction.java | 4 +- .../data/relational/core/sql/Expressions.java | 27 +- .../relational/core/sql/TupleExpression.java | 30 +- .../SingleQuerySqlGenerator.java | 14 +- .../core/mapping/AggregatePathAssertions.java | 76 +++++ .../mapping/AggregatePathSoftAssertions.java | 31 ++ .../core/mapping/ColumnInfosUnitTests.java | 22 +- .../DefaultAggregatePathUnitTests.java | 89 +++--- .../core/sql/TupleExpressionUnitTests.java | 11 +- .../ROOT/partials/mapping-annotations.adoc | 10 +- .../antora/modules/ROOT/partials/mapping.adoc | 36 +++ 30 files changed, 897 insertions(+), 670 deletions(-) create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/ColumInfosBuilder.java create mode 100644 spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/AggregatePathAssertions.java create mode 100644 spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/AggregatePathSoftAssertions.java diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutionContext.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutionContext.java index 2ec070ab76..ec1c151400 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutionContext.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutionContext.java @@ -17,6 +17,7 @@ import java.util.*; import java.util.function.BiConsumer; +import java.util.function.Function; import java.util.stream.Collectors; import org.springframework.dao.IncorrectUpdateSemanticsDataAccessException; @@ -72,19 +73,16 @@ class JdbcAggregateChangeExecutionContext { void executeInsertRoot(DbAction.InsertRoot insert) { - Object id = accessStrategy.insert(insert.getEntity(), insert.getEntityType(), Identifier.empty(), - insert.getIdValueSource()); + Object id = accessStrategy.insert(insert.getEntity(), insert.getEntityType(), Identifier.empty(), insert.getIdValueSource()); add(new DbActionExecutionResult(insert, id)); } void executeBatchInsertRoot(DbAction.BatchInsertRoot batchInsertRoot) { List> inserts = batchInsertRoot.getActions(); - List> insertSubjects = inserts.stream() - .map(insert -> InsertSubject.describedBy(insert.getEntity(), Identifier.empty())).collect(Collectors.toList()); + List> insertSubjects = inserts.stream().map(insert -> InsertSubject.describedBy(insert.getEntity(), Identifier.empty())).collect(Collectors.toList()); - Object[] ids = accessStrategy.insert(insertSubjects, batchInsertRoot.getEntityType(), - batchInsertRoot.getBatchValue()); + Object[] ids = accessStrategy.insert(insertSubjects, batchInsertRoot.getEntityType(), batchInsertRoot.getBatchValue()); for (int i = 0; i < inserts.size(); i++) { add(new DbActionExecutionResult(inserts.get(i), ids.length > 0 ? ids[i] : null)); @@ -94,17 +92,14 @@ void executeBatchInsertRoot(DbAction.BatchInsertRoot batchInsertRoot) { void executeInsert(DbAction.Insert insert) { Identifier parentKeys = getParentKeys(insert, converter); - Object id = accessStrategy.insert(insert.getEntity(), insert.getEntityType(), parentKeys, - insert.getIdValueSource()); + Object id = accessStrategy.insert(insert.getEntity(), insert.getEntityType(), parentKeys, insert.getIdValueSource()); add(new DbActionExecutionResult(insert, id)); } void executeBatchInsert(DbAction.BatchInsert batchInsert) { List> inserts = batchInsert.getActions(); - List> insertSubjects = inserts.stream() - .map(insert -> InsertSubject.describedBy(insert.getEntity(), getParentKeys(insert, converter))) - .collect(Collectors.toList()); + List> insertSubjects = inserts.stream().map(insert -> InsertSubject.describedBy(insert.getEntity(), getParentKeys(insert, converter))).collect(Collectors.toList()); Object[] ids = accessStrategy.insert(insertSubjects, batchInsert.getEntityType(), batchInsert.getBatchValue()); @@ -176,20 +171,34 @@ private Identifier getParentKeys(DbAction.WithDependingOn action, JdbcConvert Object id = getParentId(action); JdbcIdentifierBuilder identifier = JdbcIdentifierBuilder // - .forBackReferences(converter, context.getAggregatePath(action.getPropertyPath()), id); + .forBackReferences(converter, context.getAggregatePath(action.getPropertyPath()), + getValueProvider(id, context.getAggregatePath(action.getPropertyPath()), converter)); - for (Map.Entry, Object> qualifier : action.getQualifiers() - .entrySet()) { + for (Map.Entry, Object> qualifier : action.getQualifiers().entrySet()) { identifier = identifier.withQualifier(context.getAggregatePath(qualifier.getKey()), qualifier.getValue()); } return identifier.build(); } + static Function getValueProvider(Object idValue, AggregatePath path, JdbcConverter converter) { + + RelationalPersistentEntity entity = converter.getMappingContext().getPersistentEntity(path.getIdDefiningParentPath().getRequiredIdProperty().getType()); + + Function valueProvider = ap -> { + if (entity == null) { + return idValue; + } else { + PersistentPropertyPathAccessor propertyPathAccessor = entity.getPropertyPathAccessor(idValue); + return propertyPathAccessor.getProperty(ap.getRequiredPersistentPropertyPath()); + } + }; + return valueProvider; + } + private Object getParentId(DbAction.WithDependingOn action) { - DbAction.WithEntity idOwningAction = getIdOwningAction(action, - context.getAggregatePath(action.getPropertyPath()).getIdDefiningParentPath()); + DbAction.WithEntity idOwningAction = getIdOwningAction(action, context.getAggregatePath(action.getPropertyPath()).getIdDefiningParentPath()); return getPotentialGeneratedIdFrom(idOwningAction); } @@ -198,8 +207,7 @@ private DbAction.WithEntity getIdOwningAction(DbAction.WithEntity action, if (!(action instanceof DbAction.WithDependingOn withDependingOn)) { - Assert.state(idPath.isRoot(), - "When the id path is not empty the id providing action should be of type WithDependingOn"); + Assert.state(idPath.isRoot(), "When the id path is not empty the id providing action should be of type WithDependingOn"); return action; } @@ -267,20 +275,16 @@ List populateIdsIfNecessary() { if (newEntity != action.getEntity()) { - cascadingValues.stage(insert.getDependingOn(), insert.getPropertyPath(), - qualifierValue, newEntity); + cascadingValues.stage(insert.getDependingOn(), insert.getPropertyPath(), qualifierValue, newEntity); } else if (insert.getPropertyPath().getLeafProperty().isCollectionLike()) { - cascadingValues.gather(insert.getDependingOn(), insert.getPropertyPath(), - qualifierValue, newEntity); + cascadingValues.gather(insert.getDependingOn(), insert.getPropertyPath(), qualifierValue, newEntity); } } } if (roots.isEmpty()) { - throw new IllegalStateException( - String.format("Cannot retrieve the resulting instance(s) unless a %s or %s action was successfully executed", - DbAction.InsertRoot.class.getName(), DbAction.UpdateRoot.class.getName())); + throw new IllegalStateException(String.format("Cannot retrieve the resulting instance(s) unless a %s or %s action was successfully executed", DbAction.InsertRoot.class.getName(), DbAction.UpdateRoot.class.getName())); } Collections.reverse(roots); @@ -289,23 +293,19 @@ List populateIdsIfNecessary() { } @SuppressWarnings("unchecked") - private Object setIdAndCascadingProperties(DbAction.WithEntity action, @Nullable Object generatedId, - StagedValues cascadingValues) { + private Object setIdAndCascadingProperties(DbAction.WithEntity action, @Nullable Object generatedId, StagedValues cascadingValues) { S originalEntity = action.getEntity(); - RelationalPersistentEntity persistentEntity = (RelationalPersistentEntity) context - .getRequiredPersistentEntity(action.getEntityType()); - PersistentPropertyPathAccessor propertyAccessor = converter.getPropertyAccessor(persistentEntity, - originalEntity); + RelationalPersistentEntity persistentEntity = (RelationalPersistentEntity) context.getRequiredPersistentEntity(action.getEntityType()); + PersistentPropertyPathAccessor propertyAccessor = converter.getPropertyAccessor(persistentEntity, originalEntity); if (IdValueSource.GENERATED.equals(action.getIdValueSource())) { propertyAccessor.setProperty(persistentEntity.getRequiredIdProperty(), generatedId); } // set values of changed immutables referenced by this entity - cascadingValues.forEachPath(action, (persistentPropertyPath, o) -> propertyAccessor - .setProperty(getRelativePath(action, persistentPropertyPath), o)); + cascadingValues.forEachPath(action, (persistentPropertyPath, o) -> propertyAccessor.setProperty(getRelativePath(action, persistentPropertyPath), o)); return propertyAccessor.getBean(); } @@ -337,8 +337,7 @@ private void updateWithoutVersion(DbAction.UpdateRoot update) { if (!accessStrategy.update(update.getEntity(), update.getEntityType())) { - throw new IncorrectUpdateSemanticsDataAccessException( - String.format(UPDATE_FAILED, update.getEntity(), getIdFrom(update))); + throw new IncorrectUpdateSemanticsDataAccessException(String.format(UPDATE_FAILED, update.getEntity(), getIdFrom(update))); } } @@ -359,8 +358,7 @@ private void updateWithVersion(DbAction.UpdateRoot update) { */ private static class StagedValues { - static final List> aggregators = Arrays.asList(SetAggregator.INSTANCE, MapAggregator.INSTANCE, - ListAggregator.INSTANCE, SingleElementAggregator.INSTANCE); + static final List> aggregators = Arrays.asList(SetAggregator.INSTANCE, MapAggregator.INSTANCE, ListAggregator.INSTANCE, SingleElementAggregator.INSTANCE); Map> values = new HashMap<>(); @@ -368,12 +366,12 @@ private static class StagedValues { * Adds a value that needs to be set in an entity higher up in the tree of entities in the aggregate. If the * attribute to be set is multivalued this method expects only a single element. * - * @param action The action responsible for persisting the entity that needs the added value set. Must not be - * {@literal null}. - * @param path The path to the property in which to set the value. Must not be {@literal null}. + * @param action The action responsible for persisting the entity that needs the added value set. Must not be + * {@literal null}. + * @param path The path to the property in which to set the value. Must not be {@literal null}. * @param qualifier If {@code path} is a qualified multivalued properties this parameter contains the qualifier. May - * be {@literal null}. - * @param value The value to be set. Must not be {@literal null}. + * be {@literal null}. + * @param value The value to be set. Must not be {@literal null}. */ void stage(DbAction action, PersistentPropertyPath path, @Nullable Object qualifier, Object value) { @@ -386,11 +384,9 @@ StagedValue gather(DbAction action, PersistentPropertyPath path, @Nullabl MultiValueAggregator aggregator = getAggregatorFor(path); - Map valuesForPath = this.values.computeIfAbsent(action, - dbAction -> new HashMap<>()); + Map valuesForPath = this.values.computeIfAbsent(action, dbAction -> new HashMap<>()); - StagedValue stagedValue = valuesForPath.computeIfAbsent(path, - persistentPropertyPath -> new StagedValue(aggregator.createEmptyInstance())); + StagedValue stagedValue = valuesForPath.computeIfAbsent(path, persistentPropertyPath -> new StagedValue(aggregator.createEmptyInstance())); T currentValue = (T) stagedValue.value; stagedValue.value = aggregator.add(currentValue, qualifier, value); @@ -430,7 +426,8 @@ void forEachPath(DbAction dbAction, BiConsumer valueProvider) { - RelationalPersistentProperty idProperty = path.getIdDefiningParentPath().getRequiredIdProperty(); - AggregatePath.ColumnInfos infos = path.getTableInfo().reverseColumnInfos(); + return new JdbcIdentifierBuilder(forBackReference(converter, path, Identifier.empty(), valueProvider)); + } - // create property accessor - RelationalMappingContext mappingContext = converter.getMappingContext(); - RelationalPersistentEntity persistentEntity = mappingContext.getPersistentEntity(idProperty.getType()); + /** + * @param converter used for determining the column types to be used for different properties. Must not be {@literal null}. + * @param path the path for which needs to back reference an id. Must not be {@literal null}. + * @param defaultIdentifier Identifier to be used as a default when no backreference can be constructed. Must not be {@literal null}. + * @param valueProvider provides values for the {@link Identifier} based on an {@link AggregatePath}. Must not be {@literal null}. + * @return Guaranteed not to be {@literal null}. + */ + public static Identifier forBackReference(JdbcConverter converter, AggregatePath path, Identifier defaultIdentifier, Function valueProvider) { - Function valueProvider; - if (persistentEntity == null) { - valueProvider = ap -> value; - } else { - PersistentPropertyPathAccessor propertyPathAccessor = persistentEntity.getPropertyPathAccessor(value); - valueProvider = ap -> propertyPathAccessor.getProperty(ap.getRequiredPersistentPropertyPath()); - } + Identifier identifierToUse = defaultIdentifier; - Identifier identifierHolder = infos.reduce(Identifier.empty(), (ap, ci) -> { + AggregatePath idDefiningParentPath = path.getIdDefiningParentPath(); - RelationalPersistentProperty property = ap.getRequiredLeafProperty(); - return Identifier.of(ci.name(), valueProvider.apply(ap), - converter.getColumnType(property)); - }, Identifier::withPart); + // note that the idDefiningParentPath might not itself have an id property, but have a combination of back + // references and possibly keys, that form an id + if (idDefiningParentPath.hasIdProperty()) { - return new JdbcIdentifierBuilder(identifierHolder); + AggregatePath.ColumnInfos infos = path.getTableInfo().backReferenceColumnInfos(); + identifierToUse = infos.reduce(Identifier.empty(), (ap, ci) -> { + + RelationalPersistentProperty property = ap.getRequiredLeafProperty(); + return Identifier.of(ci.name(), valueProvider.apply(ap), + converter.getColumnType(property)); + }, Identifier::withPart); + } + + return identifierToUse; } + /** * Adds a qualifier to the identifier to build. A qualifier is a map key or a list index. * - * @param path path to the map that gets qualified by {@code value}. Must not be {@literal null}. + * @param path path to the map that gets qualified by {@code value}. Must not be {@literal null}. * @param value map key or list index qualifying the map identified by {@code path}. Must not be {@literal null}. * @return this builder. Guaranteed to be not {@literal null}. */ diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverter.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverter.java index 80e1975de2..707a860909 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverter.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverter.java @@ -80,7 +80,7 @@ public class MappingJdbcConverter extends MappingRelationalConverter implements * {@link #MappingJdbcConverter(RelationalMappingContext, RelationResolver, CustomConversions, JdbcTypeFactory)} * (MappingContext, RelationResolver, JdbcTypeFactory)} to convert arrays and large objects into JDBC-specific types. * - * @param context must not be {@literal null}. + * @param context must not be {@literal null}. * @param relationResolver used to fetch additional relations from the database. Must not be {@literal null}. */ public MappingJdbcConverter(RelationalMappingContext context, RelationResolver relationResolver) { @@ -98,12 +98,12 @@ public MappingJdbcConverter(RelationalMappingContext context, RelationResolver r /** * Creates a new {@link MappingJdbcConverter} given {@link MappingContext}. * - * @param context must not be {@literal null}. + * @param context must not be {@literal null}. * @param relationResolver used to fetch additional relations from the database. Must not be {@literal null}. - * @param typeFactory must not be {@literal null} + * @param typeFactory must not be {@literal null} */ public MappingJdbcConverter(RelationalMappingContext context, RelationResolver relationResolver, - CustomConversions conversions, JdbcTypeFactory typeFactory) { + CustomConversions conversions, JdbcTypeFactory typeFactory) { super(context, conversions); @@ -285,7 +285,7 @@ public R readAndResolve(TypeInformation type, RowDocument source, Identif @Override protected RelationalPropertyValueProvider newValueProvider(RowDocumentAccessor documentAccessor, - ValueExpressionEvaluator evaluator, ConversionContext context) { + ValueExpressionEvaluator evaluator, ConversionContext context) { if (context instanceof ResolvingConversionContext rcc) { @@ -314,7 +314,7 @@ class ResolvingRelationalPropertyValueProvider implements RelationalPropertyValu private final Identifier identifier; private ResolvingRelationalPropertyValueProvider(AggregatePathValueProvider delegate, RowDocumentAccessor accessor, - ResolvingConversionContext context, Identifier identifier) { + ResolvingConversionContext context, Identifier identifier) { AggregatePath path = context.aggregatePath(); @@ -323,7 +323,7 @@ private ResolvingRelationalPropertyValueProvider(AggregatePathValueProvider dele this.context = context; this.identifier = path.isEntity() ? potentiallyAppendIdentifier(identifier, path.getRequiredLeafEntity(), - property -> delegate.getValue(path.append(property))) + property -> delegate.getValue(path.append(property))) : identifier; } @@ -331,7 +331,7 @@ private ResolvingRelationalPropertyValueProvider(AggregatePathValueProvider dele * Conditionally append the identifier if the entity has an identifier property. */ static Identifier potentiallyAppendIdentifier(Identifier base, RelationalPersistentEntity entity, - Function getter) { + Function getter) { if (entity.hasIdProperty()) { @@ -361,7 +361,7 @@ public T getPropertyValue(RelationalPersistentProperty property) { if (property.isCollectionLike() || property.isMap()) { - Identifier identifier = constructIdentifier(aggregatePath); + Identifier identifier = JdbcIdentifierBuilder.forBackReference(MappingJdbcConverter.this, aggregatePath, this.identifier, getWrappedValueProvider(delegate::getValue, aggregatePath)); Iterable allByPath = relationResolver.findAllByPath(identifier, aggregatePath.getRequiredPersistentPropertyPath()); @@ -388,29 +388,6 @@ public T getPropertyValue(RelationalPersistentProperty property) { return (T) delegate.getValue(aggregatePath); } - private Identifier constructIdentifier(AggregatePath aggregatePath) { - - Identifier identifierToUse = this.identifier; - AggregatePath idDefiningParentPath = aggregatePath.getIdDefiningParentPath(); - - // note that the idDefiningParentPath might not itself have an id property, but have a combination of back - // references and possibly keys, that form an id - if (idDefiningParentPath.hasIdProperty()) { - - RelationalPersistentProperty idProperty = idDefiningParentPath.getRequiredIdProperty(); - AggregatePath idPath = idProperty.isEntity() ? idDefiningParentPath.append(idProperty) : idDefiningParentPath; - Identifier[] buildingIdentifier = new Identifier[] { Identifier.empty() }; - aggregatePath.getTableInfo().reverseColumnInfos().forEach((ap, ci) -> { - - Object value = delegate.getValue(idPath.append(ap)); - buildingIdentifier[0] = buildingIdentifier[0].withPart(ci.name(), value, - ap.getRequiredLeafProperty().getActualType()); - }); - identifierToUse = buildingIdentifier[0]; - } - return identifierToUse; - } - @Override public boolean hasValue(RelationalPersistentProperty property) { @@ -431,7 +408,7 @@ public boolean hasValue(RelationalPersistentProperty property) { return delegate.hasValue(toUse); } - return delegate.hasValue(aggregatePath.getTableInfo().reverseColumnInfos().any().alias()); + return delegate.hasValue(aggregatePath.getTableInfo().backReferenceColumnInfos().any().alias()); } return delegate.hasValue(aggregatePath); @@ -457,7 +434,7 @@ public boolean hasNonEmptyValue(RelationalPersistentProperty property) { return delegate.hasValue(toUse); } - return delegate.hasValue(aggregatePath.getTableInfo().reverseColumnInfos().any().alias()); + return delegate.hasValue(aggregatePath.getTableInfo().backReferenceColumnInfos().any().alias()); } return delegate.hasNonEmptyValue(aggregatePath); @@ -468,10 +445,26 @@ public RelationalPropertyValueProvider withContext(ConversionContext context) { return context == this.context ? this : new ResolvingRelationalPropertyValueProvider(delegate.withContext(context), accessor, - (ResolvingConversionContext) context, identifier); + (ResolvingConversionContext) context, identifier); } } + private static Function getWrappedValueProvider(Function valueProvider, AggregatePath aggregatePath) { + + AggregatePath idDefiningParentPath = aggregatePath.getIdDefiningParentPath(); + + if (!idDefiningParentPath.hasIdProperty()) { + return ap -> { + throw new IllegalStateException("This should never happen"); + }; + } + + RelationalPersistentProperty idProperty = idDefiningParentPath.getRequiredIdProperty(); + AggregatePath idPath = idProperty.isEntity() ? idDefiningParentPath.append(idProperty) : idDefiningParentPath; + return ap -> valueProvider.apply(idPath.append(ap)); + } + + /** * Marker object to indicate that the property value provider should resolve relations. * @@ -480,7 +473,7 @@ public RelationalPropertyValueProvider withContext(ConversionContext context) { * @param identifier */ private record ResolvingConversionContext(ConversionContext delegate, AggregatePath aggregatePath, - Identifier identifier) implements ConversionContext { + Identifier identifier) implements ConversionContext { @Override public S convert(Object source, TypeInformation typeHint) { diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlContext.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlContext.java index b1b4dfdc2d..c511e58dda 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlContext.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlContext.java @@ -69,7 +69,7 @@ Column getColumn(AggregatePath path) { */ Column getAnyReverseColumn(AggregatePath path) { - AggregatePath.ColumnInfo columnInfo = path.getTableInfo().reverseColumnInfos().any(); + AggregatePath.ColumnInfo columnInfo = path.getTableInfo().backReferenceColumnInfos().any(); return getTable(path).column(columnInfo.name()).as(columnInfo.alias()); } } 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 e1a61709e5..7e573c3f63 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 @@ -17,6 +17,7 @@ import java.util.*; import java.util.function.Function; +import java.util.function.Predicate; import java.util.stream.Collectors; import org.springframework.data.domain.Pageable; @@ -33,10 +34,8 @@ import org.springframework.data.relational.core.query.CriteriaDefinition; import org.springframework.data.relational.core.query.Query; import org.springframework.data.relational.core.sql.*; -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.Pair; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -60,7 +59,7 @@ * @author Viktor Ardelean * @author Kurt Niemi */ -class SqlGenerator { +public class SqlGenerator { static final SqlIdentifier VERSION_SQL_PARAMETER = SqlIdentifier.unquoted("___oldOptimisticLockingVersion"); static final SqlIdentifier IDS_SQL_PARAMETER = SqlIdentifier.unquoted("ids"); @@ -94,10 +93,6 @@ class SqlGenerator { private final QueryMapper queryMapper; private final Dialect dialect; - private final Function, Condition> inCondition; - private final Function, Condition> equalityCondition; - private final Function, Condition> notNullCondition; - /** * Create a new {@link SqlGenerator} given {@link RelationalMappingContext} and {@link RelationalPersistentEntity}. * @@ -116,11 +111,11 @@ class SqlGenerator { this.columns = new Columns(entity, mappingContext, converter); this.queryMapper = new QueryMapper(converter); this.dialect = dialect; + } - inCondition = inCondition(); - equalityCondition = equalityCondition(); - notNullCondition = isNotNullCondition(); + public SelectBuilder.SelectWhere createSelectBuilder(Table table, Predicate pathFilter) { + return createSelectBuilder(table, pathFilter, Collections.emptyList()); } /** @@ -197,7 +192,7 @@ private Condition getSubselectCondition(AggregatePath path, innerCondition = getSubselectCondition(parentPath, conditionFunction, selectFilterColumns); } - List idColumns = parentPathTableInfo.idColumnInfos().toList(ci -> subSelectTable.column(ci.name())); + List idColumns = parentPathTableInfo.idColumnInfos().toColumnList(subSelectTable); Select select = Select.builder() // .select(idColumns) // @@ -208,7 +203,7 @@ private Condition getSubselectCondition(AggregatePath path, } private Expression toExpression(Map columnsMap) { - return TupleExpression.maybeWrap(new ArrayList<>(columnsMap.values())); + return Expressions.of(new ArrayList<>(columnsMap.values())); } private BindMarker getBindMarker(SqlIdentifier columnName) { @@ -454,7 +449,7 @@ String createDeleteAllSql(@Nullable PersistentPropertyPath path) { - return createDeleteByPathAndCriteria(mappingContext.getAggregatePath(path), equalityCondition); + return createDeleteByPathAndCriteria(mappingContext.getAggregatePath(path), this::equalityCondition); } /** @@ -476,63 +471,55 @@ String createDeleteByPath(PersistentPropertyPath p * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. */ String createDeleteInByPath(PersistentPropertyPath path) { - return createDeleteByPathAndCriteria(mappingContext.getAggregatePath(path), inCondition); + return createDeleteByPathAndCriteria(mappingContext.getAggregatePath(path), this::inCondition); } /** - * Constructs a function for constructing a where condition. The where condition will be of the form - * {@literal IN :bind-marker} + * Constructs a where condition. The where condition will be of the form {@literal IN :bind-marker} */ - private Function, Condition> inCondition() { + private Condition inCondition(Map columnMap) { - return columnMap -> { + List columns = List.copyOf(columnMap.values()); - List columns = List.copyOf(columnMap.values()); - - if (columns.size() == 1) { - return Conditions.in(columns.get(0), getBindMarker(IDS_SQL_PARAMETER)); - } - return Conditions.in(TupleExpression.create(columns), getBindMarker(IDS_SQL_PARAMETER)); - }; + if (columns.size() == 1) { + return Conditions.in(columns.get(0), getBindMarker(IDS_SQL_PARAMETER)); + } + return Conditions.in(TupleExpression.create(columns), getBindMarker(IDS_SQL_PARAMETER)); } /** - * Constructs a function for constructing a where. The where condition will be of the form + * Constructs a where-condition. The where condition will be of the form * {@literal = :bind-marker-a AND = :bind-marker-b ...} */ - private Function, Condition> equalityCondition() { + private Condition equalityCondition(Map columnMap) { AggregatePath.ColumnInfos idColumnInfos = mappingContext.getAggregatePath(entity).getTableInfo().idColumnInfos(); - return columnMap -> { - - Condition result = null; - for (Map.Entry entry : columnMap.entrySet()) { - BindMarker bindMarker = getBindMarker(idColumnInfos.get(entry.getKey()).name()); - Comparison singleCondition = entry.getValue().isEqualTo(bindMarker); + Condition result = null; + for (Map.Entry entry : columnMap.entrySet()) { + BindMarker bindMarker = getBindMarker(idColumnInfos.get(entry.getKey()).name()); + Comparison singleCondition = entry.getValue().isEqualTo(bindMarker); - result = result == null ? singleCondition : result.and(singleCondition); - } - return result; - }; + result = result == null ? singleCondition : result.and(singleCondition); + } + Assert.state(result != null, "We need at least one condition"); + return result; } /** * Constructs a function for constructing where a condition. The where condition will be of the form * {@literal IS NOT NULL AND IS NOT NULL ... } */ - private Function, Condition> isNotNullCondition() { - - return columnMap -> { + private Condition isNotNullCondition(Map columnMap) { - Condition result = null; - for (Column column : columnMap.values()) { - Condition singleCondition = column.isNotNull(); + Condition result = null; + for (Column column : columnMap.values()) { + Condition singleCondition = column.isNotNull(); - result = result == null ? singleCondition : result.and(singleCondition); - } - return result; - }; + result = result == null ? singleCondition : result.and(singleCondition); + } + Assert.state(result != null, "We need at least one condition"); + return result; } private String createFindOneSql() { @@ -591,7 +578,11 @@ private SelectBuilder.SelectWhere selectBuilder() { private SelectBuilder.SelectWhere selectBuilder(Collection keyColumns) { - Table table = getTable(); + return createSelectBuilder(getTable(), ap -> false, keyColumns); + } + + private SelectBuilder.SelectWhere createSelectBuilder(Table table, Predicate pathFilter, + Collection keyColumns) { Set columnExpressions = new LinkedHashSet<>(); @@ -599,15 +590,19 @@ private SelectBuilder.SelectWhere selectBuilder(Collection keyCol for (PersistentPropertyPath path : mappingContext .findPersistentPropertyPaths(entity.getType(), p -> true)) { - AggregatePath extPath = mappingContext.getAggregatePath(path); + AggregatePath aggregatePath = mappingContext.getAggregatePath(path); + + if (pathFilter.test(aggregatePath)) { + continue; + } // add a join if necessary - Join join = getJoin(extPath); + Join join = getJoin(aggregatePath); if (join != null) { joinTables.add(join); } - Column column = getColumn(extPath); + Column column = getColumn(aggregatePath); if (column != null) { columnExpressions.add(column); } @@ -626,13 +621,7 @@ private static SelectBuilder.SelectJoin addJoins(SelectBuilder.SelectJoin baseSe for (Join join : joinTables) { - Condition condition = null; - for (Pair columnPair : join.columns) { - Comparison elementalCondition = columnPair.getFirst().isEqualTo(columnPair.getSecond()); - condition = condition == null ? elementalCondition : condition.and(elementalCondition); - } - - baseSelect = baseSelect.leftOuterJoin(join.joinTable).on(Objects.requireNonNull(condition)); + baseSelect = baseSelect.leftOuterJoin(join.joinTable).on(join.condition); } return baseSelect; } @@ -708,21 +697,25 @@ Join getJoin(AggregatePath path) { } Table currentTable = sqlContext.getTable(path); - AggregatePath.ColumnInfos backRefColumnInfos = path.getTableInfo().reverseColumnInfos(); + AggregatePath.ColumnInfos backRefColumnInfos = path.getTableInfo().backReferenceColumnInfos(); AggregatePath idDefiningParentPath = path.getIdDefiningParentPath(); Table parentTable = sqlContext.getTable(idDefiningParentPath); AggregatePath.ColumnInfos idColumnInfos = idDefiningParentPath.getTableInfo().idColumnInfos(); - List> joinConditions = new ArrayList<>(); + final Condition[] joinCondition = { null }; backRefColumnInfos.forEach((ap, ci) -> { - joinConditions.add(Pair.of(currentTable.column(ci.name()), parentTable.column(idColumnInfos.get(ap).name()))); + + Condition elementalCondition = currentTable.column(ci.name()) + .isEqualTo(parentTable.column(idColumnInfos.get(ap).name())); + joinCondition[0] = joinCondition[0] == null ? elementalCondition : joinCondition[0].and(elementalCondition); }); return new Join( // currentTable, // - joinConditions // + joinCondition[0] // ); + } private String createFindAllInListSql() { @@ -860,7 +853,7 @@ private String createDeleteByPathAndCriteria(AggregatePath path, Delete delete; Map columns = new TreeMap<>(); - AggregatePath.ColumnInfos columnInfos = path.getTableInfo().reverseColumnInfos(); + AggregatePath.ColumnInfos columnInfos = path.getTableInfo().backReferenceColumnInfos(); columnInfos.forEach((ag, ci) -> columns.put(ag, table.column(ci.name()))); if (isFirstNonRoot(path)) { @@ -1171,7 +1164,7 @@ SelectBuilder.SelectOrdered applyCriteria(@Nullable CriteriaDefinition criteria, /** * Value object representing a {@code JOIN} association. */ - record Join(Table joinTable, List> columns) { + record Join(Table joinTable, Condition condition) { } /** diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGeneratorSource.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGeneratorSource.java index 0a217dce63..9f5389e8c1 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGeneratorSource.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGeneratorSource.java @@ -19,6 +19,7 @@ import org.springframework.data.relational.core.dialect.Dialect; import org.springframework.data.relational.core.mapping.RelationalMappingContext; +import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.util.Assert; import org.springframework.util.ConcurrentReferenceHashMap; @@ -56,7 +57,7 @@ public Dialect getDialect() { return dialect; } - SqlGenerator getSqlGenerator(Class domainType) { + public SqlGenerator getSqlGenerator(Class domainType) { return CACHE.computeIfAbsent(domainType, t -> new SqlGenerator(context, converter, context.getRequiredPersistentEntity(t), dialect)); diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlParametersFactory.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlParametersFactory.java index 4e9ee941ed..37ddde10ae 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlParametersFactory.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlParametersFactory.java @@ -19,6 +19,8 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Function; import java.util.function.Predicate; import org.springframework.data.jdbc.core.mapping.JdbcValue; @@ -60,21 +62,19 @@ public SqlParametersFactory(RelationalMappingContext context, JdbcConverter conv /** * Creates the parameters for a SQL insert operation. * - * @param instance the entity to be inserted. Must not be {@code null}. - * @param domainType the type of the instance. Must not be {@code null}. - * @param identifier information about data that needs to be considered for the insert but which is not part of the - * entity. Namely references back to a parent entity and key/index columns for entities that are stored in a - * {@link Map} or {@link List}. + * @param instance the entity to be inserted. Must not be {@code null}. + * @param domainType the type of the instance. Must not be {@code null}. + * @param identifier information about data that needs to be considered for the insert but which is not part of the + * entity. Namely references back to a parent entity and key/index columns for entities that are stored in a + * {@link Map} or {@link List}. * @param idValueSource the {@link IdValueSource} for the insert. * @return the {@link SqlIdentifierParameterSource} for the insert. Guaranteed to not be {@code null}. * @since 2.4 */ - SqlIdentifierParameterSource forInsert(T instance, Class domainType, Identifier identifier, - IdValueSource idValueSource) { + SqlIdentifierParameterSource forInsert(T instance, Class domainType, Identifier identifier, IdValueSource idValueSource) { RelationalPersistentEntity persistentEntity = getRequiredPersistentEntity(domainType); - SqlIdentifierParameterSource parameterSource = getParameterSource(instance, persistentEntity, "", - PersistentProperty::isIdProperty); + SqlIdentifierParameterSource parameterSource = getParameterSource(instance, persistentEntity, "", PersistentProperty::isIdProperty); identifier.forEach((name, value, type) -> addConvertedPropertyValue(parameterSource, name, value, type)); @@ -96,21 +96,20 @@ SqlIdentifierParameterSource forInsert(T instance, Class domainType, Iden /** * Creates the parameters for a SQL update operation. * - * @param instance the entity to be updated. Must not be {@code null}. + * @param instance the entity to be updated. Must not be {@code null}. * @param domainType the type of the instance. Must not be {@code null}. * @return the {@link SqlIdentifierParameterSource} for the update. Guaranteed to not be {@code null}. * @since 2.4 */ SqlIdentifierParameterSource forUpdate(T instance, Class domainType) { - return getParameterSource(instance, getRequiredPersistentEntity(domainType), "", - RelationalPersistentProperty::isInsertOnly); + return getParameterSource(instance, getRequiredPersistentEntity(domainType), "", RelationalPersistentProperty::isInsertOnly); } /** * Creates the parameters for a SQL query by id. * - * @param id the entity id. Must not be {@code null}. + * @param id the entity id. Must not be {@code null}. * @param domainType the type of the instance. Must not be {@code null}. * @return the {@link SqlIdentifierParameterSource} for the query. Guaranteed to not be {@code null}. * @since 2.4 @@ -122,37 +121,26 @@ SqlIdentifierParameterSource forQueryById(Object id, Class domainType) { RelationalPersistentEntity entity = getRequiredPersistentEntity(domainType); RelationalPersistentProperty singleIdProperty = entity.getRequiredIdProperty(); - if (singleIdProperty.isEntity()) { - - RelationalPersistentEntity complexId = context.getPersistentEntity(singleIdProperty); - PersistentPropertyPathAccessor accessor = complexId.getPropertyPathAccessor(id); + RelationalPersistentEntity complexId = context.getPersistentEntity(singleIdProperty); - context.getAggregatePath(entity).getTableInfo().idColumnInfos().forEach((ap, ci) -> { - Object idValue = accessor.getProperty(ap.getRequiredPersistentPropertyPath()); + Function valueExtractor = complexId == null + ? ap -> id + : ap -> complexId.getPropertyPathAccessor(id).getProperty(ap.getRequiredPersistentPropertyPath()); - addConvertedPropertyValue( // + context.getAggregatePath(entity).getTableInfo().idColumnInfos() // + .forEach((ap, ci) -> addConvertedPropertyValue( // parameterSource, // ap.getRequiredLeafProperty(), // - idValue, // + valueExtractor.apply(ap), // ci.name() // - ); - }); - } else { - - addConvertedPropertyValue( // - parameterSource, // - singleIdProperty, // - id, // - singleIdProperty.getColumnName() // - ); - } + )); return parameterSource; } /** * Creates the parameters for a SQL query by ids. * - * @param ids the entity ids. Must not be {@code null}. + * @param ids the entity ids. Must not be {@code null}. * @param domainType the type of the instance. Must not be {@code null}. * @return the {@link SqlIdentifierParameterSource} for the query. Guaranteed to not be {@code null}. * @since 2.4 @@ -161,32 +149,27 @@ SqlIdentifierParameterSource forQueryByIds(Iterable ids, Class domainT SqlIdentifierParameterSource parameterSource = new SqlIdentifierParameterSource(); - RelationalPersistentEntity entity = context.getPersistentEntity(domainType); + RelationalPersistentEntity entity = context.getRequiredPersistentEntity(domainType); RelationalPersistentProperty singleIdProperty = entity.getRequiredIdProperty(); + RelationalPersistentEntity complexId = context.getPersistentEntity(singleIdProperty); + AggregatePath.ColumnInfos idColumnInfos = context.getAggregatePath(entity).getTableInfo().idColumnInfos(); - if (singleIdProperty.isEntity()) { - - RelationalPersistentEntity complexId = context.getPersistentEntity(singleIdProperty); + BiFunction valueExtractor = complexId == null + ? (id, ap) -> id + : (id, ap) -> complexId.getPropertyPathAccessor(id).getProperty(ap.getRequiredPersistentPropertyPath()); - AggregatePath.ColumnInfos idColumnInfos = context.getAggregatePath(entity).getTableInfo().idColumnInfos(); + List parameterValues = new ArrayList<>(); + for (Object id : ids) { - List parameterValues = new ArrayList<>(); - for (Object id : ids) { + List tupleList = new ArrayList<>(); + idColumnInfos.forEach((ap, ci) -> { + tupleList.add(valueExtractor.apply(id, ap)); + }); + parameterValues.add(tupleList.toArray(new Object[0])); + } - PersistentPropertyPathAccessor accessor = complexId.getPropertyPathAccessor(id); + parameterSource.addValue(SqlGenerator.IDS_SQL_PARAMETER, parameterValues); - List tupleList = new ArrayList<>(); - idColumnInfos.forEach((ap, ci) -> { - tupleList.add(accessor.getProperty(ap.getRequiredPersistentPropertyPath())); - }); - parameterValues.add(tupleList.toArray(new Object[0])); - } - - parameterSource.addValue(SqlGenerator.IDS_SQL_PARAMETER, parameterValues); - } else { - addConvertedPropertyValuesAsList(parameterSource, getRequiredPersistentEntity(domainType).getRequiredIdProperty(), - ids); - } return parameterSource; } @@ -201,27 +184,22 @@ SqlIdentifierParameterSource forQueryByIdentifier(Identifier identifier) { SqlIdentifierParameterSource parameterSource = new SqlIdentifierParameterSource(); - identifier.toMap() - .forEach((name, value) -> addConvertedPropertyValue(parameterSource, name, value, value.getClass())); + identifier.toMap().forEach((name, value) -> addConvertedPropertyValue(parameterSource, name, value, value.getClass())); return parameterSource; } - private void addConvertedPropertyValue(SqlIdentifierParameterSource parameterSource, - RelationalPersistentProperty property, @Nullable Object value, SqlIdentifier name) { + private void addConvertedPropertyValue(SqlIdentifierParameterSource parameterSource, RelationalPersistentProperty property, @Nullable Object value, SqlIdentifier name) { - addConvertedValue(parameterSource, value, name, converter.getColumnType(property), - converter.getTargetSqlType(property)); + addConvertedValue(parameterSource, value, name, converter.getColumnType(property), converter.getTargetSqlType(property)); } - private void addConvertedPropertyValue(SqlIdentifierParameterSource parameterSource, SqlIdentifier name, Object value, - Class javaType) { + private void addConvertedPropertyValue(SqlIdentifierParameterSource parameterSource, SqlIdentifier name, Object value, Class javaType) { addConvertedValue(parameterSource, value, name, javaType, JdbcUtil.targetSqlTypeFor(javaType)); } - private void addConvertedValue(SqlIdentifierParameterSource parameterSource, @Nullable Object value, - SqlIdentifier paramName, Class javaType, SQLType sqlType) { + private void addConvertedValue(SqlIdentifierParameterSource parameterSource, @Nullable Object value, SqlIdentifier paramName, Class javaType, SQLType sqlType) { JdbcValue jdbcValue = converter.writeJdbcValue( // value, // @@ -235,8 +213,7 @@ private void addConvertedValue(SqlIdentifierParameterSource parameterSource, @Nu jdbcValue.getJdbcType().getVendorTypeNumber()); } - private void addConvertedPropertyValuesAsList(SqlIdentifierParameterSource parameterSource, - RelationalPersistentProperty property, Iterable values) { + private void addConvertedPropertyValuesAsList(SqlIdentifierParameterSource parameterSource, RelationalPersistentProperty property, Iterable values) { List convertedIds = new ArrayList<>(); JdbcValue jdbcValue = null; @@ -262,14 +239,11 @@ private RelationalPersistentEntity getRequiredPersistentEntity(Class d return (RelationalPersistentEntity) context.getRequiredPersistentEntity(domainType); } - private SqlIdentifierParameterSource getParameterSource(@Nullable S instance, - RelationalPersistentEntity persistentEntity, String prefix, - Predicate skipProperty) { + private SqlIdentifierParameterSource getParameterSource(@Nullable S instance, RelationalPersistentEntity persistentEntity, String prefix, Predicate skipProperty) { SqlIdentifierParameterSource parameters = new SqlIdentifierParameterSource(); - PersistentPropertyAccessor propertyAccessor = instance != null ? persistentEntity.getPropertyAccessor(instance) - : NoValuePropertyAccessor.instance(); + PersistentPropertyAccessor propertyAccessor = instance != null ? persistentEntity.getPropertyAccessor(instance) : NoValuePropertyAccessor.instance(); persistentEntity.doWithAll(property -> { @@ -284,8 +258,7 @@ private SqlIdentifierParameterSource getParameterSource(@Nullable S insta Object value = propertyAccessor.getProperty(property); RelationalPersistentEntity embeddedEntity = context.getPersistentEntity(property.getTypeInformation()); - SqlIdentifierParameterSource additionalParameters = getParameterSource((T) value, - (RelationalPersistentEntity) embeddedEntity, prefix + property.getEmbeddedPrefix(), skipProperty); + SqlIdentifierParameterSource additionalParameters = getParameterSource((T) value, (RelationalPersistentEntity) embeddedEntity, prefix + property.getEmbeddedPrefix(), skipProperty); parameters.addAll(additionalParameters); } else { diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcDeleteQueryCreator.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcDeleteQueryCreator.java index a02c681b66..d669cf3e20 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcDeleteQueryCreator.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcDeleteQueryCreator.java @@ -89,13 +89,10 @@ protected List complete(@Nullable Criteria criteria, Sort sor Table table = Table.create(entityMetadata.getTableName()); MapSqlParameterSource parameterSource = new MapSqlParameterSource(); - SqlContext sqlContext = new SqlContext(); - Condition condition = criteria == null ? null : queryMapper.getMappedObject(parameterSource, criteria, table, entity); - List idColumns = context.getAggregatePath(entity).getTableInfo().idColumnInfos() - .toList(ci -> table.column(ci.name())); + List idColumns = context.getAggregatePath(entity).getTableInfo().idColumnInfos().toColumnList(table); // create select criteria query for subselect SelectWhere selectBuilder = StatementBuilder.select(idColumns).from(table); @@ -128,26 +125,23 @@ private void deleteRelations(List deleteChain, RelationalPersistentEntit AggregatePath aggregatePath = context.getAggregatePath(path); - // prevent duplication on recursive call - if (path.getLength() > 1 && !aggregatePath.getParentPath().isEmbedded()) { + if (aggregatePath.isEmbedded()){ continue; } - if (aggregatePath.isEntity() && !aggregatePath.isEmbedded()) { + if (aggregatePath.isEntity() ) { SqlContext sqlContext = new SqlContext(); - // MariaDB prior to 11.6 does not support aliases for delete statements + // MariaDB prior to 11.6 does not support aliases for delete statements Table table = sqlContext.getUnaliasedTable(aggregatePath); - List reverseColumns = aggregatePath.getTableInfo().reverseColumnInfos() - .toList(ci -> table.column(ci.name())); - Expression expression = TupleExpression.maybeWrap(reverseColumns); + List reverseColumns = aggregatePath.getTableInfo().backReferenceColumnInfos().toColumnList(table); + Expression expression = Expressions.of(reverseColumns); Condition inCondition = Conditions.in(expression, parentSelect); - List parentIdColumns = aggregatePath.getIdDefiningParentPath().getTableInfo().idColumnInfos() - .toList(ci -> table.column(ci.name())); + List parentIdColumns = aggregatePath.getIdDefiningParentPath().getTableInfo().idColumnInfos().toColumnList(table); Select select = StatementBuilder.select( // parentIdColumns // 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 7d22cd2fdc..7ed485532d 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 @@ -15,14 +15,14 @@ */ package org.springframework.data.jdbc.repository.query; -import java.util.ArrayList; -import java.util.List; import java.util.Optional; +import java.util.function.Predicate; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.core.convert.QueryMapper; +import org.springframework.data.jdbc.core.convert.SqlGeneratorSource; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.relational.core.dialect.Dialect; import org.springframework.data.relational.core.dialect.RenderContextFactory; @@ -65,6 +65,7 @@ class JdbcQueryCreator extends RelationalQueryCreator { private final boolean isSliceQuery; private final ReturnedType returnedType; private final Optional lockMode; + private final SqlGeneratorSource sqlGeneratorSource; /** * Creates new instance of this class with the given {@link PartTree}, {@link JdbcConverter}, {@link Dialect}, @@ -78,16 +79,44 @@ class JdbcQueryCreator extends RelationalQueryCreator { * @param accessor parameter metadata provider, must not be {@literal null}. * @param isSliceQuery flag denoting if the query returns a {@link org.springframework.data.domain.Slice}. * @param returnedType the {@link ReturnedType} to be returned by the query. Must not be {@literal null}. + * @deprecated use + * {@link JdbcQueryCreator#JdbcQueryCreator(RelationalMappingContext, PartTree, JdbcConverter, Dialect, RelationalEntityMetadata, RelationalParameterAccessor, boolean, ReturnedType, Optional, SqlGeneratorSource)} + * instead. */ + @Deprecated(since = "4.0", forRemoval = true) JdbcQueryCreator(RelationalMappingContext context, PartTree tree, JdbcConverter converter, Dialect dialect, RelationalEntityMetadata entityMetadata, RelationalParameterAccessor accessor, boolean isSliceQuery, ReturnedType returnedType, Optional lockMode) { + this(context, tree, converter, dialect, entityMetadata, accessor, isSliceQuery, returnedType, lockMode, + new SqlGeneratorSource(context, converter, dialect)); + } + + /** + * Creates new instance of this class with the given {@link PartTree}, {@link JdbcConverter}, {@link Dialect}, + * {@link RelationalEntityMetadata} and {@link RelationalParameterAccessor}. + * + * @param context the mapping context. Must not be {@literal null}. + * @param tree part tree, must not be {@literal null}. + * @param converter must not be {@literal null}. + * @param dialect must not be {@literal null}. + * @param entityMetadata relational entity metadata, must not be {@literal null}. + * @param accessor parameter metadata provider, must not be {@literal null}. + * @param isSliceQuery flag denoting if the query returns a {@link org.springframework.data.domain.Slice}. + * @param returnedType the {@link ReturnedType} to be returned by the query. Must not be {@literal null}. + * @param lockMode lock mode to be used for the query. + * @param sqlGeneratorSource the source providing SqlGenerator instances for generating SQL. Must not be + * {@literal null} + */ + JdbcQueryCreator(RelationalMappingContext context, PartTree tree, JdbcConverter converter, Dialect dialect, + RelationalEntityMetadata entityMetadata, RelationalParameterAccessor accessor, boolean isSliceQuery, + ReturnedType returnedType, Optional lockMode, SqlGeneratorSource sqlGeneratorSource) { super(tree, accessor); Assert.notNull(converter, "JdbcConverter must not be null"); Assert.notNull(dialect, "Dialect must not be null"); Assert.notNull(entityMetadata, "Relational entity metadata must not be null"); Assert.notNull(returnedType, "ReturnedType must not be null"); + Assert.notNull(sqlGeneratorSource, "SqlGeneratorSource must not be null"); this.context = context; this.tree = tree; @@ -99,6 +128,7 @@ class JdbcQueryCreator extends RelationalQueryCreator { this.isSliceQuery = isSliceQuery; this.returnedType = returnedType; this.lockMode = lockMode; + this.sqlGeneratorSource = sqlGeneratorSource; } /** @@ -228,122 +258,13 @@ SelectBuilder.SelectLimitOffset createSelectClause(RelationalPersistentEntity private SelectBuilder.SelectJoin selectBuilder(Table table) { - List columnExpressions = new ArrayList<>(); RelationalPersistentEntity entity = entityMetadata.getTableEntity(); - SqlContext sqlContext = new SqlContext(); - - List joinTables = new ArrayList<>(); - for (PersistentPropertyPath path : context - .findPersistentPropertyPaths(entity.getType(), p -> true)) { - - AggregatePath aggregatePath = context.getAggregatePath(path); - - if (returnedType.needsCustomConstruction()) { - if (!returnedType.getInputProperties().contains(aggregatePath.getRequiredBaseProperty().getName())) { - continue; - } - } - - // add a join if necessary - Join join = getJoin(sqlContext, aggregatePath); - if (join != null) { - joinTables.add(join); - } - - Column column = getColumn(sqlContext, aggregatePath); - if (column != null) { - columnExpressions.add(column); - } - } - - SelectBuilder.SelectAndFrom selectBuilder = StatementBuilder.select(columnExpressions); - SelectBuilder.SelectJoin baseSelect = selectBuilder.from(table); - - for (Join join : joinTables) { - - Condition condition = null; - for (int i = 0; i < join.joinColumns.size(); i++) { - Column parentColumn = join.parentId.get(i); - Column joinColumn = join.joinColumns.get(i); - Comparison singleCondition = joinColumn.isEqualTo(parentColumn); - condition = condition == null ? singleCondition : condition.and(singleCondition); - } - - Assert.state(condition != null, "No condition found"); - - baseSelect = baseSelect.leftOuterJoin(join.joinTable).on(condition); - } + Predicate filter = ap -> returnedType.needsCustomConstruction() + && !returnedType.getInputProperties().contains(ap.getRequiredBaseProperty().getName()); - return baseSelect; - } - - /** - * Create a {@link Column} for {@link AggregatePath}. - * - * @param sqlContext for generating SQL constructs. - * @param path the path to the column in question. - * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. - */ - @Nullable - private Column getColumn(SqlContext sqlContext, AggregatePath path) { - - // an embedded itself doesn't give an column, its members will though. - // if there is a collection or map on the path it won't get selected at all, but it will get loaded with a separate - // select - // only the parent path is considered in order to handle arrays that get stored as BINARY properly - if (path.isEmbedded() || path.getParentPath().isMultiValued()) { - return null; - } - - if (path.isEntity()) { - - if (path.isQualified() // - || path.isCollectionLike() // - || path.hasIdProperty() // - ) { - return null; - } - - // Simple entities without id include there backreference as an synthetic id in order to distinguish null entities - // from entities with only null values. - return sqlContext.getAnyReverseColumn(path); - } - - return sqlContext.getColumn(path); - } - - @Nullable - Join getJoin(SqlContext sqlContext, AggregatePath path) { - - if (!path.isEntity() || path.isEmbedded() || path.isMultiValued()) { - return null; - } - - Table currentTable = sqlContext.getTable(path); - - AggregatePath idDefiningParentPath = path.getIdDefiningParentPath(); - Table parentTable = sqlContext.getTable(idDefiningParentPath); - - List reverseColumns = path.getTableInfo().reverseColumnInfos().toList(ci -> currentTable.column(ci.name())); - List idColumns = idDefiningParentPath.getTableInfo().idColumnInfos() - .toList(ci -> parentTable.column(ci.name())); - return new Join( // - currentTable, // - reverseColumns, // - idColumns // + return (SelectBuilder.SelectJoin) sqlGeneratorSource.getSqlGenerator(entity.getType()).createSelectBuilder(table, filter ); } - /** - * Value object representing a {@code JOIN} association. - */ - private record Join(Table joinTable, List joinColumns, List parentId) { - - Join { - Assert.isTrue(joinColumns.size() == parentId.size(), - "Both sides of a join condition must have the same number of columns"); - } - - } } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/SqlContext.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/SqlContext.java index ab6db1ab51..0b83d2d575 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/SqlContext.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/SqlContext.java @@ -47,7 +47,7 @@ Column getColumn(AggregatePath path) { Column getAnyReverseColumn(AggregatePath path) { - AggregatePath.ColumnInfo anyReverseColumnInfo = path.getTableInfo().reverseColumnInfos().any(); + AggregatePath.ColumnInfo anyReverseColumnInfo = path.getTableInfo().backReferenceColumnInfos().any(); return getTable(path).column(anyReverseColumnInfo.name()).as(anyReverseColumnInfo.alias()); } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextImmutableUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextImmutableUnitTests.java index 78d05c03dc..13215e22bb 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextImmutableUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextImmutableUnitTests.java @@ -120,7 +120,8 @@ public void idGenerationOfChildInList() { assertThat(newRoot.list.get(0).id).isEqualTo(24L); } - @Test // GH-537 + @Test + // GH-537 void populatesIdsIfNecessaryForAllRootsThatWereProcessed() { DummyEntity root1 = new DummyEntity().withId(123L); @@ -151,7 +152,7 @@ void populatesIdsIfNecessaryForAllRootsThatWereProcessed() { } DbAction.Insert createInsert(DbAction.WithEntity parent, String propertyName, Object value, - @Nullable Object key) { + @Nullable Object key) { return new DbAction.Insert<>(value, getPersistentPropertyPath(propertyName), parent, key == null ? emptyMap() : singletonMap(toPath(propertyName), key), IdValueSource.GENERATED); @@ -166,7 +167,7 @@ PersistentPropertyPath getPersistentPropertyPath(S } Identifier createBackRef(long value) { - return JdbcIdentifierBuilder.forBackReferences(converter, toAggregatePath("content"), value).build(); + return JdbcIdentifierBuilder.forBackReferences(converter, toAggregatePath("content"), JdbcAggregateChangeExecutionContext.getValueProvider(value, toAggregatePath("content"), converter)).build(); } PersistentPropertyPath toPath(String path) { diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextUnitTests.java index eef22d5c94..9596111125 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextUnitTests.java @@ -18,6 +18,7 @@ import static java.util.Collections.*; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; +import static org.springframework.data.jdbc.core.JdbcAggregateChangeExecutionContext.*; import static org.springframework.data.jdbc.core.convert.JdbcIdentifierBuilder.*; import java.util.ArrayList; @@ -257,7 +258,7 @@ PersistentPropertyPath getPersistentPropertyPath(S } Identifier createBackRef(long value) { - return forBackReferences(converter, toAggregatePath("content"), value).build(); + return forBackReferences(converter, toAggregatePath("content"), getValueProvider(value, toAggregatePath("content"), converter)).build(); } PersistentPropertyPath toPath(String path) { diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilderUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilderUnitTests.java index b5ec5ece9f..e2d3f1cbae 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilderUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilderUnitTests.java @@ -21,14 +21,17 @@ import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.function.Function; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.core.PersistentPropertyPathTestUtils; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; +import org.springframework.data.mapping.PersistentPropertyPathAccessor; import org.springframework.data.relational.core.mapping.AggregatePath; import org.springframework.data.relational.core.mapping.Embedded; +import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; /** * Unit tests for the {@link JdbcIdentifierBuilder}. @@ -47,7 +50,7 @@ class WithSimpleId { @Test // DATAJDBC-326 void parametersWithPropertyKeysUseTheParentPropertyJdbcType() { - Identifier identifier = JdbcIdentifierBuilder.forBackReferences(converter, getPath("child"), "eins").build(); + Identifier identifier = JdbcIdentifierBuilder.forBackReferences(converter, getPath("child"), getValueProvider("eins", getPath("child"), converter)).build(); assertThat(identifier.getParts()) // .extracting("name", "value", "targetType") // @@ -62,7 +65,7 @@ void qualifiersForMaps() { AggregatePath path = getPath("children"); Identifier identifier = JdbcIdentifierBuilder // - .forBackReferences(converter, path, "parent-eins") // + .forBackReferences(converter, path, getValueProvider("parent-eins", path, converter)) // .withQualifier(path, "map-key-eins") // .build(); @@ -80,7 +83,7 @@ void qualifiersForLists() { AggregatePath path = getPath("moreChildren"); Identifier identifier = JdbcIdentifierBuilder // - .forBackReferences(converter, path, "parent-eins") // + .forBackReferences(converter, path, getValueProvider("parent-eins", path, converter)) // .withQualifier(path, "list-index-eins") // .build(); @@ -96,7 +99,7 @@ void qualifiersForLists() { void backreferenceAcrossEmbeddable() { Identifier identifier = JdbcIdentifierBuilder // - .forBackReferences(converter, getPath("embeddable.child"), "parent-eins") // + .forBackReferences(converter, getPath("embeddable.child"), getValueProvider("parent-eins", getPath("embeddable.child"), converter)) // .build(); assertThat(identifier.getParts()) // @@ -110,7 +113,7 @@ void backreferenceAcrossEmbeddable() { void backreferenceAcrossNoId() { Identifier identifier = JdbcIdentifierBuilder // - .forBackReferences(converter, getPath("noId.child"), "parent-eins") // + .forBackReferences(converter, getPath("noId.child"), getValueProvider("parent-eins", getPath("noId.child"), converter)) // .build(); assertThat(identifier.getParts()) // @@ -125,6 +128,24 @@ private AggregatePath getPath(String dotPath) { } } + /** + * copied from JdbcAggregateChangeExecutionContext + */ + static Function getValueProvider(Object idValue, AggregatePath path, JdbcConverter converter) { + + RelationalPersistentEntity entity = converter.getMappingContext().getPersistentEntity(path.getIdDefiningParentPath().getRequiredIdProperty().getType()); + + Function valueProvider = ap -> { + if (entity == null) { + return idValue; + } else { + PersistentPropertyPathAccessor propertyPathAccessor = entity.getPropertyPathAccessor(idValue); + return propertyPathAccessor.getProperty(ap.getRequiredPersistentPropertyPath()); + } + }; + return valueProvider; + } + @Nested class WithCompositeId { @@ -136,7 +157,7 @@ void forBackReferences() { AggregatePath path = getPath("children"); Identifier identifier = JdbcIdentifierBuilder // - .forBackReferences(converter, path, exampleId) // + .forBackReferences(converter, path, getValueProvider(exampleId, path, converter)) // .build(); assertThat(identifier.getParts()) // diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorEmbeddedUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorEmbeddedUnitTests.java index a6a7c63bae..6054e87ceb 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorEmbeddedUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorEmbeddedUnitTests.java @@ -89,42 +89,42 @@ void findOne() { @Test // GH-574 void findOneWrappedId() { - SqlGenerator sqlGenerator = createSqlGenerator(DummyEntityWithWrappedId.class); + SqlGenerator sqlGenerator = createSqlGenerator(WithWrappedId.class); String sql = sqlGenerator.getFindOne(); assertSoftly(softly -> { softly.assertThat(sql).startsWith("SELECT") // - .contains("dummy_entity_with_wrapped_id.name AS name") // - .contains("dummy_entity_with_wrapped_id.id") // - .contains("WHERE dummy_entity_with_wrapped_id.id = :id"); + .contains("with_wrapped_id.name AS name") // + .contains("with_wrapped_id.id") // + .contains("WHERE with_wrapped_id.id = :id"); }); } @Test // GH-574 void findOneEmbeddedId() { - SqlGenerator sqlGenerator = createSqlGenerator(DummyEntityWithEmbeddedId.class); + SqlGenerator sqlGenerator = createSqlGenerator(WithEmbeddedId.class); String sql = sqlGenerator.getFindOne(); assertSoftly(softly -> { softly.assertThat(sql).startsWith("SELECT") // - .contains("dummy_entity_with_embedded_id.name AS name") // - .contains("dummy_entity_with_embedded_id.one") // - .contains("dummy_entity_with_embedded_id.two") // + .contains("with_embedded_id.name AS name") // + .contains("with_embedded_id.one") // + .contains("with_embedded_id.two") // .contains(" WHERE ") // - .contains("dummy_entity_with_embedded_id.one = :one") // - .contains("dummy_entity_with_embedded_id.two = :two"); + .contains("with_embedded_id.one = :one") // + .contains("with_embedded_id.two = :two"); }); } @Test // GH-574 void deleteByIdEmbeddedId() { - SqlGenerator sqlGenerator = createSqlGenerator(DummyEntityWithEmbeddedId.class); + SqlGenerator sqlGenerator = createSqlGenerator(WithEmbeddedId.class); String sql = sqlGenerator.getDeleteById(); @@ -132,15 +132,15 @@ void deleteByIdEmbeddedId() { softly.assertThat(sql).startsWith("DELETE") // .contains(" WHERE ") // - .contains("dummy_entity_with_embedded_id.one = :one") // - .contains("dummy_entity_with_embedded_id.two = :two"); + .contains("with_embedded_id.one = :one") // + .contains("with_embedded_id.two = :two"); }); } @Test // GH-574 void deleteByIdInEmbeddedId() { - SqlGenerator sqlGenerator = createSqlGenerator(DummyEntityWithEmbeddedId.class); + SqlGenerator sqlGenerator = createSqlGenerator(WithEmbeddedId.class); String sql = sqlGenerator.getDeleteByIdIn(); @@ -148,33 +148,33 @@ void deleteByIdInEmbeddedId() { softly.assertThat(sql).startsWith("DELETE") // .contains(" WHERE ") // - .contains("(dummy_entity_with_embedded_id.one, dummy_entity_with_embedded_id.two) IN (:ids)"); + .contains("(with_embedded_id.one, with_embedded_id.two) IN (:ids)"); }); } @Test // GH-574 void deleteByPathEmbeddedId() { - SqlGenerator sqlGenerator = createSqlGenerator(DummyEntityWithEmbeddedId.class); + SqlGenerator sqlGenerator = createSqlGenerator(WithEmbeddedId.class); PersistentPropertyPath path = PersistentPropertyPathTestUtils.getPath("other", - DummyEntityWithEmbeddedIdAndReference.class, context); + WithEmbeddedIdAndReference.class, context); String sql = sqlGenerator.createDeleteByPath(path); assertSoftly(softly -> { softly.assertThat(sql).startsWith("DELETE FROM other_entity WHERE") // - .contains("other_entity.dummy_entity_with_embedded_id_and_reference_one = :one") // - .contains("other_entity.dummy_entity_with_embedded_id_and_reference_two = :two"); + .contains("other_entity.with_embedded_id_and_reference_one = :one") // + .contains("other_entity.with_embedded_id_and_reference_two = :two"); }); } @Test // GH-574 void deleteInByPathEmbeddedId() { - SqlGenerator sqlGenerator = createSqlGenerator(DummyEntityWithEmbeddedId.class); + SqlGenerator sqlGenerator = createSqlGenerator(WithEmbeddedId.class); PersistentPropertyPath path = PersistentPropertyPathTestUtils.getPath("other", - DummyEntityWithEmbeddedIdAndReference.class, context); + WithEmbeddedIdAndReference.class, context); String sql = sqlGenerator.createDeleteInByPath(path); @@ -183,14 +183,14 @@ void deleteInByPathEmbeddedId() { softly.assertThat(sql).startsWith("DELETE FROM other_entity WHERE") // .contains(" WHERE ") // .contains( - "(other_entity.dummy_entity_with_embedded_id_and_reference_one, other_entity.dummy_entity_with_embedded_id_and_reference_two) IN (:ids)"); + "(other_entity.with_embedded_id_and_reference_one, other_entity.with_embedded_id_and_reference_two) IN (:ids)"); }); } @Test // GH-574 void updateWithEmbeddedId() { - SqlGenerator sqlGenerator = createSqlGenerator(DummyEntityWithEmbeddedId.class); + SqlGenerator sqlGenerator = createSqlGenerator(WithEmbeddedId.class); String sql = sqlGenerator.getUpdate(); @@ -198,15 +198,15 @@ void updateWithEmbeddedId() { softly.assertThat(sql).startsWith("UPDATE") // .contains(" WHERE ") // - .contains("dummy_entity_with_embedded_id.one = :one") // - .contains("dummy_entity_with_embedded_id.two = :two"); + .contains("with_embedded_id.one = :one") // + .contains("with_embedded_id.two = :two"); }); } @Test // GH-574 void existsByIdEmbeddedId() { - SqlGenerator sqlGenerator = createSqlGenerator(DummyEntityWithEmbeddedId.class); + SqlGenerator sqlGenerator = createSqlGenerator(WithEmbeddedId.class); String sql = sqlGenerator.getExists(); @@ -214,8 +214,8 @@ void existsByIdEmbeddedId() { softly.assertThat(sql).startsWith("SELECT COUNT") // .contains(" WHERE ") // - .contains("dummy_entity_with_embedded_id.one = :one") // - .contains("dummy_entity_with_embedded_id.two = :two"); + .contains("with_embedded_id.one = :one") // + .contains("with_embedded_id.two = :two"); }); } @@ -269,24 +269,24 @@ void findAllInList() { @Test // GH-574 void findAllInListEmbeddedId() { - SqlGenerator sqlGenerator = createSqlGenerator(DummyEntityWithEmbeddedId.class); + SqlGenerator sqlGenerator = createSqlGenerator(WithEmbeddedId.class); String sql = sqlGenerator.getFindAllInList(); assertSoftly(softly -> { softly.assertThat(sql).startsWith("SELECT") // - .contains("dummy_entity_with_embedded_id.name AS name") // - .contains("dummy_entity_with_embedded_id.one") // - .contains("dummy_entity_with_embedded_id.two") // - .contains(" WHERE (dummy_entity_with_embedded_id.one, dummy_entity_with_embedded_id.two) IN (:ids)"); + .contains("with_embedded_id.name AS name") // + .contains("with_embedded_id.one") // + .contains("with_embedded_id.two") // + .contains(" WHERE (with_embedded_id.one, with_embedded_id.two) IN (:ids)"); }); } @Test // GH-574 void findOneWithReference() { - SqlGenerator sqlGenerator = createSqlGenerator(DummyEntityWithEmbeddedIdAndReference.class); + SqlGenerator sqlGenerator = createSqlGenerator(WithEmbeddedIdAndReference.class); String sql = sqlGenerator.getFindOne(); @@ -296,12 +296,12 @@ void findOneWithReference() { .contains(" LEFT OUTER JOIN other_entity other ") // .contains(" ON ") // .contains( - " other.dummy_entity_with_embedded_id_and_reference_one = dummy_entity_with_embedded_id_and_reference.one ") // + " other.with_embedded_id_and_reference_one = with_embedded_id_and_reference.one ") // .contains( - " other.dummy_entity_with_embedded_id_and_reference_two = dummy_entity_with_embedded_id_and_reference.two ") // + " other.with_embedded_id_and_reference_two = with_embedded_id_and_reference.two ") // .contains(" WHERE ") // - .contains("dummy_entity_with_embedded_id_and_reference.one = :one") // - .contains("dummy_entity_with_embedded_id_and_reference.two = :two"); + .contains("with_embedded_id_and_reference.one = :one") // + .contains("with_embedded_id_and_reference.two = :two"); }); } @@ -445,17 +445,8 @@ void joinForEmbeddedWithReference() { assertSoftly(softly -> { softly.assertThat(join.joinTable().getName()).isEqualTo(SqlIdentifier.unquoted("other_entity")); - softly.assertThat(join.columns()).extracting( // - pair -> pair.getFirst().getTable(), // - pair -> pair.getFirst().getName(), // - pair -> pair.getSecond().getTable().getName(), // - pair -> pair.getSecond().getName() // - ).contains(tuple( // - join.joinTable(), // - SqlIdentifier.unquoted("dummy_entity2"), // - SqlIdentifier.unquoted("dummy_entity2"), // - SqlIdentifier.unquoted("id") // - )); + softly.assertThat(join.condition()) + .isEqualTo(SqlGeneratorUnitTests.equalsCondition("dummy_entity2", "id", join.joinTable(), "dummy_entity2")); }); } @@ -511,7 +502,7 @@ static class DummyEntity { record WrappedId(Long id) { } - static class DummyEntityWithWrappedId { + static class WithWrappedId { @Id @Embedded(onEmpty = OnEmpty.USE_NULL) WrappedId wrappedId; @@ -522,7 +513,7 @@ static class DummyEntityWithWrappedId { record EmbeddedId(Long one, String two) { } - static class DummyEntityWithEmbeddedId { + static class WithEmbeddedId { @Id @Embedded(onEmpty = OnEmpty.USE_NULL) EmbeddedId embeddedId; @@ -531,7 +522,7 @@ static class DummyEntityWithEmbeddedId { } - static class DummyEntityWithEmbeddedIdAndReference { + static class WithEmbeddedIdAndReference { @Id @Embedded(onEmpty = OnEmpty.USE_NULL) EmbeddedId embeddedId; 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 b0fcc9b1a1..f700d2b08f 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 @@ -50,6 +50,7 @@ import org.springframework.data.relational.core.query.Criteria; import org.springframework.data.relational.core.query.Query; import org.springframework.data.relational.core.sql.Aliased; +import org.springframework.data.relational.core.sql.Comparison; import org.springframework.data.relational.core.sql.LockMode; import org.springframework.data.relational.core.sql.SqlIdentifier; import org.springframework.data.relational.core.sql.Table; @@ -85,6 +86,22 @@ class SqlGeneratorUnitTests { }); private SqlGenerator sqlGenerator; + static Comparison equalsCondition(Table parentTable, SqlIdentifier parentId, Table joinedTable, + SqlIdentifier joinedColumn) { + return org.springframework.data.relational.core.sql.Column.create(joinedColumn, joinedTable) + .isEqualTo(org.springframework.data.relational.core.sql.Column.create(parentId, parentTable)); + } + + static Comparison equalsCondition(SqlIdentifier parentTable, SqlIdentifier parentId, Table joinedTable, + SqlIdentifier joinedColumn) { + return equalsCondition(Table.create(parentTable), parentId, joinedTable, joinedColumn); + } + + static Comparison equalsCondition(String parentTable, String parentId, Table joinedTable, String joinedColumn) { + return equalsCondition(SqlIdentifier.unquoted(parentTable), SqlIdentifier.unquoted(parentId), joinedTable, + SqlIdentifier.unquoted(joinedColumn)); + } + @BeforeEach void setUp() { this.sqlGenerator = createSqlGenerator(DummyEntity.class); @@ -716,17 +733,9 @@ void joinForSimpleReference() { assertSoftly(softly -> { softly.assertThat(join.joinTable().getName()).isEqualTo(SqlIdentifier.quoted("REFERENCED_ENTITY")); - softly.assertThat(join.columns()).extracting( // - pair -> pair.getFirst().getTable(), // - pair -> pair.getFirst().getName(), // - pair -> pair.getSecond().getTable().getName(), // - pair -> pair.getSecond().getName() // - ).contains(tuple( // - join.joinTable(), // - SqlIdentifier.quoted("DUMMY_ENTITY"), // - SqlIdentifier.quoted("DUMMY_ENTITY"), // - SqlIdentifier.quoted("id1") // - )); + softly.assertThat(join.condition()).isEqualTo(equalsCondition(SqlIdentifier.quoted("DUMMY_ENTITY"), + SqlIdentifier.quoted("id1"), join.joinTable(), SqlIdentifier.quoted("DUMMY_ENTITY"))); + }); } @@ -754,17 +763,10 @@ void joinForSecondLevelReference() { assertSoftly(softly -> { softly.assertThat(join.joinTable().getName()).isEqualTo(SqlIdentifier.quoted("SECOND_LEVEL_REFERENCED_ENTITY")); - softly.assertThat(join.columns()).extracting( // - pair -> pair.getFirst().getTable(), // - pair -> pair.getFirst().getName(), // - pair -> pair.getSecond().getTable().getName(), // - pair -> pair.getSecond().getName() // - ).contains(tuple( // - join.joinTable(), // - SqlIdentifier.quoted("REFERENCED_ENTITY"), // - SqlIdentifier.quoted("REFERENCED_ENTITY"), // - SqlIdentifier.quoted("X_L1ID") // - )); + softly.assertThat(join.condition()) + .isEqualTo(equalsCondition(Table.create("REFERENCED_ENTITY").as(SqlIdentifier.quoted("ref")), + SqlIdentifier.quoted("X_L1ID"), join.joinTable(), SqlIdentifier.quoted("REFERENCED_ENTITY"))); + }); } @@ -779,18 +781,8 @@ void joinForOneToOneWithoutId() { softly.assertThat(joinTable.getName()).isEqualTo(SqlIdentifier.quoted("NO_ID_CHILD")); softly.assertThat(joinTable).isInstanceOf(Aliased.class); softly.assertThat(((Aliased) joinTable).getAlias()).isEqualTo(SqlIdentifier.quoted("child")); - - softly.assertThat(join.columns()).extracting( // - pair -> pair.getFirst().getTable(), // - pair -> pair.getFirst().getName(), // - pair -> pair.getSecond().getTable().getName(), // - pair -> pair.getSecond().getName() // - ).contains(tuple( // - join.joinTable(), // - SqlIdentifier.quoted("PARENT_OF_NO_ID_CHILD"), // - SqlIdentifier.quoted("PARENT_OF_NO_ID_CHILD"), // - SqlIdentifier.quoted("X_ID") // - )); + softly.assertThat(join.condition()).isEqualTo(equalsCondition(SqlIdentifier.quoted("PARENT_OF_NO_ID_CHILD"), + SqlIdentifier.quoted("X_ID"), join.joinTable(), SqlIdentifier.quoted("PARENT_OF_NO_ID_CHILD"))); }); } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/AggregatePath.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/AggregatePath.java index 4ea0e38b90..6fba2086f8 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/AggregatePath.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/AggregatePath.java @@ -20,11 +20,9 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.TreeMap; import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.BinaryOperator; -import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Stream; import java.util.stream.StreamSupport; @@ -32,17 +30,23 @@ import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.PropertyHandler; +import org.springframework.data.relational.core.sql.Column; import org.springframework.data.relational.core.sql.SqlIdentifier; +import org.springframework.data.relational.core.sql.Table; +import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** * Represents a path within an aggregate starting from the aggregate root. The path can be iterated from the leaf to its * root. + *

+ * It implements {@link Comparable} so that collections of {@code AggregatePath} instances can be sorted in a consistent + * way. * - * @since 3.2 * @author Jens Schauder * @author Mark Paluch + * @since 3.2 */ public interface AggregatePath extends Iterable, Comparable { @@ -246,39 +250,59 @@ default Stream stream() { */ AggregatePath getIdDefiningParentPath(); + /** + * The path resulting from removing the first element of the {@link AggregatePath}. + * + * @return {@literal null} for any {@link AggregatePath} having less than two elements. + * @since 3.5.0 + */ @Nullable AggregatePath getTail(); - record TableInfo( - - /* - * The fully qualified name of the table this path is tied to or of the longest ancestor path that is actually - * tied to a table. - */ - SqlIdentifier qualifiedTableName, - - /* - * The alias used for the table on which this path is based. - */ - @Nullable SqlIdentifier tableAlias, - - ColumnInfos reverseColumnInfos, - - /* - * The column used for the list index or map key of the leaf property of this path. - */ - @Nullable ColumnInfo qualifierColumnInfo, + /** + * Subtract the {@literal basePath} from {@literal this} {@literal AggregatePath} by removing the {@literal basePath} + * from the beginning of {@literal this}. + * + * @param basePath the path to be removed. + * @return an AggregatePath that ends like the original {@literal AggregatePath} but has {@literal basePath} removed + * from the beginning. + */ + @Nullable + AggregatePath subtract(@Nullable AggregatePath basePath); - /* - * The type of the qualifier column of the leaf property of this path or {@literal null} if this is not - * applicable. - */ - @Nullable Class qualifierColumnType, + /** + * Compares this {@code AggregatePath} to another {@code AggregatePath} based on their dot path notation. + *

+ * This is used to get {@code AggregatePath} instances sorted in a consistent way. Since this order affects generated + * SQL this also affects query caches and similar. + * + * @param other the {@code AggregatePath} to compare to. Must not be {@literal null}. + * @return a negative integer, zero, or a positive integer as this object's path is less than, equal to, or greater + * than the specified object's path. + */ + @Override + default int compareTo(@NonNull AggregatePath other) { + return toDotPath().compareTo(other.toDotPath()); + } - /* - * The column name of the id column of the ancestor path that represents an actual table. - */ - ColumnInfos idColumnInfos) { + /** + * Information about a table underlying an entity. + * + * @param qualifiedTableName the fully qualified name of the table this path is tied to or of the longest ancestor + * path that is actually tied to a table. Must not be {@literal null}. + * @param tableAlias the alias used for the table on which this path is based. May be {@literal null}. + * @param backReferenceColumnInfos information about the columns used to reference back to the owning entity. Must not + * be {@literal null}. Since 3.5. + * @param qualifierColumnInfo the column used for the list index or map key of the leaf property of this path. May be + * {@literal null}. + * @param qualifierColumnType the type of the qualifier column of the leaf property of this path or {@literal null} if + * this is not applicable. May be {@literal null}. + * @param idColumnInfos the column name of the id column of the ancestor path that represents an actual table. Must + * not be {@literal null}. + */ + record TableInfo(SqlIdentifier qualifiedTableName, @Nullable SqlIdentifier tableAlias, + ColumnInfos backReferenceColumnInfos, @Nullable ColumnInfo qualifierColumnInfo, + @Nullable Class qualifierColumnType, ColumnInfos idColumnInfos) { static TableInfo of(AggregatePath path) { @@ -289,7 +313,7 @@ static TableInfo of(AggregatePath path) { SqlIdentifier tableAlias = tableOwner.isRoot() ? null : AggregatePathTableUtils.constructTableAlias(tableOwner); - ColumnInfos reverseColumnInfos = computeReverseColumnInfo(path); + ColumnInfos backReferenceColumnInfos = computeBackReferenceColumnInfos(path); ColumnInfo qualifierColumnInfo = null; if (!path.isRoot()) { @@ -307,8 +331,8 @@ static TableInfo of(AggregatePath path) { ColumnInfos idColumnInfos = computeIdColumnInfos(tableOwner, leafEntity); - return new TableInfo(qualifiedTableName, tableAlias, reverseColumnInfos, qualifierColumnInfo, qualifierColumnType, - idColumnInfos); + return new TableInfo(qualifiedTableName, tableAlias, backReferenceColumnInfos, qualifierColumnInfo, + qualifierColumnType, idColumnInfos); } @@ -337,7 +361,7 @@ private static ColumnInfos computeIdColumnInfos(AggregatePath tableOwner, } } - private static ColumnInfos computeReverseColumnInfo(AggregatePath path) { + private static ColumnInfos computeBackReferenceColumnInfos(AggregatePath path) { AggregatePath tableOwner = AggregatePathTraversal.getTableOwningPath(path); @@ -346,65 +370,79 @@ private static ColumnInfos computeReverseColumnInfo(AggregatePath path) { } AggregatePath idDefiningParentPath = tableOwner.getIdDefiningParentPath(); - RelationalPersistentProperty leafProperty = tableOwner.getRequiredLeafProperty(); - - RelationalPersistentProperty idProperty = idDefiningParentPath.getLeafEntity().getIdProperty(); + RelationalPersistentProperty idProperty = idDefiningParentPath.getRequiredLeafEntity().getIdProperty(); - if (idProperty != null) { - if (idProperty.isEntity()) { + AggregatePath basePath = idProperty != null && idProperty.isEntity() ? idDefiningParentPath.append(idProperty) + : idDefiningParentPath; + ColumInfosBuilder ciBuilder = new ColumInfosBuilder(basePath); - AggregatePath idBasePath = idDefiningParentPath.append(idProperty); - ColumInfosBuilder ciBuilder = new ColumInfosBuilder(idBasePath); + if (idProperty != null && idProperty.isEntity()) { - RelationalPersistentEntity idEntity = idBasePath.getRequiredLeafEntity(); - idEntity.doWithProperties((PropertyHandler) p -> { - AggregatePath idElementPath = idBasePath.append(p); - SqlIdentifier name = idElementPath.getColumnInfo().name(); - name = name.transform(n -> idDefiningParentPath.getTableInfo().qualifiedTableName.getReference() + "_" + n); + RelationalPersistentEntity idEntity = basePath.getRequiredLeafEntity(); + idEntity.doWithProperties((PropertyHandler) p -> { + AggregatePath idElementPath = basePath.append(p); + SqlIdentifier name = idElementPath.getColumnInfo().name(); + name = name.transform(n -> idDefiningParentPath.getTableInfo().qualifiedTableName.getReference() + "_" + n); - ciBuilder.add(idElementPath, name, name); - }); - - return ciBuilder.build(); - - } else { - - ColumInfosBuilder ciBuilder = new ColumInfosBuilder(idDefiningParentPath); - SqlIdentifier reverseColumnName = leafProperty - .getReverseColumnName(idDefiningParentPath.getRequiredLeafEntity()); - - ciBuilder.add(idProperty, reverseColumnName, - AggregatePathTableUtils.prefixWithTableAlias(path, reverseColumnName)); + ciBuilder.add(idElementPath, name, name); + }); - return ciBuilder.build(); - } } else { - ColumInfosBuilder ciBuilder = new ColumInfosBuilder(idDefiningParentPath); + RelationalPersistentProperty leafProperty = tableOwner.getRequiredLeafProperty(); SqlIdentifier reverseColumnName = leafProperty .getReverseColumnName(idDefiningParentPath.getRequiredLeafEntity()); + SqlIdentifier alias = AggregatePathTableUtils.prefixWithTableAlias(path, reverseColumnName); - ciBuilder.add(idDefiningParentPath, reverseColumnName, - AggregatePathTableUtils.prefixWithTableAlias(path, reverseColumnName)); - - return ciBuilder.build(); + if (idProperty != null) { + ciBuilder.add(idProperty, reverseColumnName, alias); + } else { + ciBuilder.add(idDefiningParentPath, reverseColumnName, alias); + } } + return ciBuilder.build(); + } + @Override + public ColumnInfos backReferenceColumnInfos() { + return backReferenceColumnInfos; } + /** + * Returns the unique {@link ColumnInfo} referencing the parent table, if such exists. + * + * @return guaranteed not to be {@literal null}. + * @throws IllegalStateException if there is not exactly one back referencing column. + * @deprecated since there might be more than one reverse column instead. Use {@link #backReferenceColumnInfos()} + * instead. + */ @Deprecated(forRemoval = true) public ColumnInfo reverseColumnInfo() { - return reverseColumnInfos.unique(); + return backReferenceColumnInfos.unique(); } + /** + * The id columns of the underlying table. + *

+ * These might be: + *

    + *
  • the columns representing the id of the entity in question.
  • + *
  • the columns representing the id of a parent entity, which _owns_ the table. Note that this case also covers + * the first case.
  • + *
  • or the backReferenceColumns.
  • + *
+ * + * @return ColumnInfos representing the effective id of this entity. Guaranteed not to be {@literal null}. + */ public ColumnInfos effectiveIdColumnInfos() { - return reverseColumnInfos.columnInfos.isEmpty() ? idColumnInfos : reverseColumnInfos; + return backReferenceColumnInfos.columnInfos.isEmpty() ? idColumnInfos : backReferenceColumnInfos; } } /** - * @param name The name of the column used to represent this property in the database. - * @param alias The alias for the column used to represent this property in the database. + * @param name the name of the column used to represent this property in the database. + * @param alias the alias for the column used to represent this property in the database. + * @since 3.2 */ record ColumnInfo(SqlIdentifier name, SqlIdentifier alias) { @@ -432,29 +470,49 @@ static ColumnInfo of(AggregatePath path) { } /** - * A group of {@link ColumnInfo} values referenced by there respective {@link AggregatePath}. This is relevant for - * composite ids and references to such ids. - **/ + * A group of {@link ColumnInfo} values referenced by there respective {@link AggregatePath}. It is used in a similar + * way as {@literal ColumnInfo} when one needs to consider more than a single column. This is relevant for composite + * ids and references to such ids. + * + * @author Jens Schauder + * @since 3.5 + */ class ColumnInfos { private final AggregatePath basePath; private final Map columnInfos; + private final Map> columnCache = new HashMap<>(); /** + * Creates a new ColumnInfos instances based on the arguments. + * * @param basePath The path on which all other paths in the other argument are based on. For the typical case of a * composite id, this would be the path to the composite ids. * @param columnInfos A map, mapping {@literal AggregatePath} instances to the respective {@literal ColumnInfo} */ - private ColumnInfos(AggregatePath basePath, Map columnInfos) { + ColumnInfos(AggregatePath basePath, Map columnInfos) { this.basePath = basePath; this.columnInfos = columnInfos; } - public static ColumnInfos empty(AggregatePath base) { - return new ColumnInfos(base, new HashMap<>()); + /** + * An empty {@literal ColumnInfos} instance with a fixed base path. Useful as a base when collecting + * {@link ColumnInfo} instances into an {@literal ColumnInfos} instance. + * + * @param basePath The path on which paths in the {@literal ColumnInfos} or derived objects will be based on. + * @return an empty instance save the {@literal basePath}. + */ + public static ColumnInfos empty(AggregatePath basePath) { + return new ColumnInfos(basePath, new HashMap<>()); } + /** + * If this instance contains exactly one {@link ColumnInfo} it will be returned. + * + * @return the unique {@literal ColumnInfo} if present. + * @throws IllegalStateException if the number of contained {@literal ColumnInfo} instances is not exactly 1. + */ public ColumnInfo unique() { Collection values = columnInfos.values(); @@ -462,18 +520,41 @@ public ColumnInfo unique() { return values.iterator().next(); } + /** + * Any of the contained {@link ColumnInfo} instances. + * + * @return a {@link ColumnInfo} instance. + * @throws java.util.NoSuchElementException if no instance is available. + */ public ColumnInfo any() { Collection values = columnInfos.values(); return values.iterator().next(); } + /** + * Checks if {@literal this} instance is empty, i.e. does not contain any {@link ColumnInfo} instance. + * + * @return {@literal true} iff the collection of {@literal ColumnInfo} is empty. + */ public boolean isEmpty() { return columnInfos.isEmpty(); } - public List toList(Function mapper) { - return columnInfos.values().stream().map(mapper).toList(); + /** + * Converts the given {@link Table} into a list of {@link Column}s. This method retrieves and caches the list of + * columns for the specified table. If the columns are not already cached, it computes the list by mapping + * {@code columnInfos} to their corresponding {@link Column} in the provided table and then stores the result in the + * cache. + * + * @param table the {@link Table} for which the columns should be generated; must not be {@literal null}. + * @return a list of {@link Column}s associated with the specified {@link Table}. Guaranteed no to be + * {@literal null}. + */ + public List toColumnList(Table table) { + + return columnCache.computeIfAbsent(table, + t -> columnInfos.values().stream().map(columnInfo -> t.column(columnInfo.name)).toList()); } /** @@ -489,8 +570,8 @@ public List toList(Function mapper) { * an additional element into a result. * @param combiner an associative, non-interfering, stateless function for combining two values, which must be * compatible with the {@code accumulator} function. - * @return result of the function. * @param type of the result. + * @return result of the function. * @since 3.5 */ public T reduce(T identity, BiFunction accumulator, BinaryOperator combiner) { @@ -506,56 +587,57 @@ public T reduce(T identity, BiFunction accumul return result; } + /** + * Calls the consumer for each pair of {@link AggregatePath} and {@literal ColumnInfo}. + * + * @param consumer the function to call. + */ public void forEach(BiConsumer consumer) { columnInfos.forEach(consumer); } + /** + * Calls the {@literal mapper} for each pair one pair of {@link AggregatePath} and {@link ColumnInfo}, if there is + * any. + * + * @param mapper the function to call. + * @return the result of the mapper + * @throws java.util.NoSuchElementException if this {@literal ColumnInfo} is empty. + */ public T any(BiFunction mapper) { Map.Entry any = columnInfos.entrySet().iterator().next(); return mapper.apply(any.getKey(), any.getValue()); } + /** + * Gets the {@link ColumnInfo} for the provided {@link AggregatePath} + * + * @param path for which to return the {@literal ColumnInfo} + * @return {@literal ColumnInfo} for the given path. + */ public ColumnInfo get(AggregatePath path) { return columnInfos.get(path); } + /** + * Constructs an {@link AggregatePath} from the {@literal basePath} and the provided argument. + * + * @param ap {@literal AggregatePath} to be appended to the {@literal basePath}. + * @return the combined (@literal AggregatePath} + */ public AggregatePath fullPath(AggregatePath ap) { return basePath.append(ap); } + /** + * Number of {@literal ColumnInfo} elements in this instance. + * + * @return the size of the collection of {@literal ColumnInfo}. + */ public int size() { return columnInfos.size(); } } - class ColumInfosBuilder { - private final AggregatePath basePath; - - private final Map columnInfoMap = new TreeMap<>(); - - public ColumInfosBuilder(AggregatePath basePath) { - this.basePath = basePath; - } - - void add(AggregatePath path, SqlIdentifier name, SqlIdentifier alias) { - add(path, new ColumnInfo(name, alias)); - } - - public void add(RelationalPersistentProperty property, SqlIdentifier name, SqlIdentifier alias) { - add(basePath.append(property), name, alias); - } - - ColumnInfos build() { - return new ColumnInfos(basePath, columnInfoMap); - } - - public void add(AggregatePath path, ColumnInfo columnInfo) { - columnInfoMap.put(path.substract(basePath), columnInfo); - } - } - - @Nullable - AggregatePath substract(@Nullable AggregatePath basePath); - } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/ColumInfosBuilder.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/ColumInfosBuilder.java new file mode 100644 index 0000000000..9df469eb7b --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/ColumInfosBuilder.java @@ -0,0 +1,84 @@ +/* + * Copyright 2025 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.mapping; + +import org.springframework.data.relational.core.sql.SqlIdentifier; + +import java.util.Map; +import java.util.TreeMap; + +/** + * A builder for {@link AggregatePath.ColumnInfos} instances. + * + * @author Jens Schauder + * @since 3.5 + */ +class ColumInfosBuilder { + + private final AggregatePath basePath; + private final Map columnInfoMap = new TreeMap<>(); + + /** + * Start construction with just the {@literal basePath} which all other paths are build upon. + * + * @param basePath must not be null. + */ + ColumInfosBuilder(AggregatePath basePath) { + this.basePath = basePath; + } + + /** + * Adds a {@link AggregatePath.ColumnInfo} to the {@link AggregatePath.ColumnInfos} under construction. + * + * @param path referencing the {@literal ColumnInfo}. + * @param name of the column. + * @param alias alias for the column. + */ + void add(AggregatePath path, SqlIdentifier name, SqlIdentifier alias) { + add(path, new AggregatePath.ColumnInfo(name, alias)); + } + + /** + * Adds a {@link AggregatePath.ColumnInfo} to the {@link AggregatePath.ColumnInfos} under construction. + * + * @param property referencing the {@literal ColumnInfo}. + * @param name of the column. + * @param alias alias for the column. + */ + void add(RelationalPersistentProperty property, SqlIdentifier name, SqlIdentifier alias) { + add(basePath.append(property), name, alias); + } + + /** + * Adds a {@link AggregatePath.ColumnInfo} to the {@link AggregatePath.ColumnInfos} under construction. + * + * @param path the path referencing the {@literal ColumnInfo} + * @param columnInfo the {@literal ColumnInfo} added. + */ + void add(AggregatePath path, AggregatePath.ColumnInfo columnInfo) { + columnInfoMap.put(path.subtract(basePath), columnInfo); + } + + /** + * Build the final {@link AggregatePath.ColumnInfos} instance. + * + * @return a {@literal ColumnInfos} instance containing all the added {@link AggregatePath.ColumnInfo} instances. + */ + AggregatePath.ColumnInfos build() { + return new AggregatePath.ColumnInfos(basePath, columnInfoMap); + } + +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DefaultAggregatePath.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DefaultAggregatePath.java index e0e1073da5..4246d92ead 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DefaultAggregatePath.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DefaultAggregatePath.java @@ -229,7 +229,7 @@ public AggregatePath getTail() { @Override @Nullable - public AggregatePath substract(@Nullable AggregatePath basePath) { + public AggregatePath subtract(@Nullable AggregatePath basePath) { if (basePath == null || basePath.isRoot()) { return this; @@ -244,7 +244,7 @@ public AggregatePath substract(@Nullable AggregatePath basePath) { if (tail == null) { return null; } - return tail.substract(basePath.getTail()); + return tail.subtract(basePath.getTail()); } throw new IllegalStateException("Can't subtract [%s] from [%s]".formatted(basePath, this)); @@ -303,11 +303,6 @@ public String toString() { + ((isRoot()) ? "/" : path.toDotPath()); } - @Override - public int compareTo(@NonNull AggregatePath other) { - return toDotPath().compareTo(other.toDotPath()); - } - private static class AggregatePathIterator implements Iterator { private @Nullable AggregatePath current; diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntity.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntity.java index 210ab1f7ec..7edf18b4d7 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntity.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntity.java @@ -50,7 +50,7 @@ default SqlIdentifier getQualifiedTableName() { * Returns the column representing the identifier. * * @return will never be {@literal null}. - * @deprecated use {@code AggregatePath.getTableInfo().getIdColumnInfos()} instead. + * @deprecated because an entity may have multiple id columns. Use {@code AggregatePath.getTableInfo().getIdColumnInfos()} instead. */ @Deprecated(forRemoval = true) SqlIdentifier getIdColumn(); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AnalyticFunction.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AnalyticFunction.java index ecc46d3ede..687591bd5e 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AnalyticFunction.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AnalyticFunction.java @@ -64,7 +64,7 @@ public AnalyticFunction partitionBy(Expression... partitionBy) { * previously present. * @since 3.5 */ - public AnalyticFunction partitionBy(Collection partitionBy) { + public AnalyticFunction partitionBy(Collection partitionBy) { return partitionBy(partitionBy.toArray(new Expression[0])); } @@ -87,7 +87,7 @@ public AnalyticFunction orderBy(OrderByField... orderBy) { * previously present. * @since 3.5 */ - public AnalyticFunction orderBy(Collection orderBy) { + public AnalyticFunction orderBy(Collection orderBy) { return orderBy(orderBy.toArray(new Expression[0])); } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Expressions.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Expressions.java index 328c37218a..cf6c7c76d5 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Expressions.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Expressions.java @@ -15,15 +15,17 @@ */ package org.springframework.data.relational.core.sql; +import java.util.List; + /** * Factory for common {@link Expression}s. * * @author Mark Paluch * @author Jens Schauder - * @since 1.1 * @see SQL * @see Conditions * @see Functions + * @since 1.1 */ public abstract class Expressions { @@ -61,8 +63,29 @@ public static Expression cast(Expression expression, String targetType) { return Cast.create(expression, targetType); } + /** + * Creates an {@link Expression} based on the provided list of {@link Column}s. + *

+ * If the list contains only a single column, this method returns that column directly + * as the resulting {@link Expression}. Otherwise, it creates and returns a + * {@link TupleExpression} that represents multiple columns as a single expression. + * + * @param columns the list of {@link Column}s to include in the expression; + * must not be {@literal null}. + * @return an {@link Expression} corresponding to the input columns: either a single column or a {@link TupleExpression} for multiple columns. + * @since 3.5 + */ + public static Expression of(List columns) { + + if (columns.size() == 1) { + return columns.get(0); + } + return new TupleExpression(columns); + } + // Utility constructor. - private Expressions() {} + private Expressions() { + } static public class SimpleExpression extends AbstractSegment implements Expression { diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/TupleExpression.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/TupleExpression.java index e1699197ea..aa0e89bebc 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/TupleExpression.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/TupleExpression.java @@ -1,6 +1,20 @@ -package org.springframework.data.relational.core.sql; +/* + * Copyright 2025 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. + */ -import org.jetbrains.annotations.NotNull; +package org.springframework.data.relational.core.sql; import static java.util.stream.Collectors.*; @@ -8,7 +22,7 @@ /** * A tuple as used in conditions like - * + * *

  *   WHERE (one, two) IN (select x, y from some_table)
  * 
@@ -24,7 +38,7 @@ private static Segment[] children(List expressions) { return expressions.toArray(new Segment[0]); } - private TupleExpression(List expressions) { + TupleExpression(List expressions) { super(children(expressions)); @@ -39,14 +53,6 @@ public static TupleExpression create(List expressions) { return new TupleExpression(expressions); } - public static Expression maybeWrap(List columns) { - - if (columns.size() == 1) { - return columns.get(0); - } - return new TupleExpression(columns); - } - @Override public String toString() { return "(" + expressions.stream().map(Expression::toString).collect(joining(", ")) + ")"; 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 183050166f..2038f721ed 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 @@ -169,10 +169,7 @@ private QueryMeta createInlineQuery(AggregatePath basePath, @Nullable Condition String rowCountAlias = aliases.getRowCountAlias(basePath); Expression count = basePath.isRoot() ? new AliasedExpression(SQL.literalOf(1), rowCountAlias) // : AnalyticFunction.create("count", Expressions.just("*")) // - .partitionBy( // - basePath.getTableInfo().reverseColumnInfos().toList( // - ci -> table.column(ci.name()) // - ) // + .partitionBy(basePath.getTableInfo().backReferenceColumnInfos().toColumnList(table) // ).as(rowCountAlias); columns.add(count); @@ -182,7 +179,8 @@ private QueryMeta createInlineQuery(AggregatePath basePath, @Nullable Condition if (!basePath.isRoot()) { backReferenceAlias = aliases.getBackReferenceAlias(basePath); - columns.add(table.column(basePath.getTableInfo().reverseColumnInfos().unique().name()).as(backReferenceAlias)); + columns + .add(table.column(basePath.getTableInfo().backReferenceColumnInfos().unique().name()).as(backReferenceAlias)); keyAlias = aliases.getKeyAlias(basePath); Expression keyExpression = basePath.isQualified() @@ -242,10 +240,10 @@ private String getIdentifierProperty(List paths) { private static AnalyticFunction createRowNumberExpression(AggregatePath basePath, Table table, String rowNumberAlias) { - AggregatePath.ColumnInfos reverseColumnInfos = basePath.getTableInfo().reverseColumnInfos(); + AggregatePath.ColumnInfos reverseColumnInfos = basePath.getTableInfo().backReferenceColumnInfos(); return AnalyticFunction.create("row_number") // - .partitionBy(reverseColumnInfos.toList(ci -> table.column(ci.name()))) // - .orderBy(reverseColumnInfos.toList(ci -> table.column(ci.name()))) // + .partitionBy(reverseColumnInfos.toColumnList(table)) // + .orderBy(reverseColumnInfos.toColumnList(table)) // .as(rowNumberAlias); } diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/AggregatePathAssertions.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/AggregatePathAssertions.java new file mode 100644 index 0000000000..aec43445ca --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/AggregatePathAssertions.java @@ -0,0 +1,76 @@ +/* + * Copyright 2025 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.mapping; + +import org.assertj.core.api.AbstractAssert; + +public class AggregatePathAssertions extends AbstractAssert { + + // Constructor for initializing with an AggregatePath instance + public AggregatePathAssertions(AggregatePath actual) { + super(actual, AggregatePathAssertions.class); + } + + /** + * Entry point for creating assertions for AggregatePath. + */ + public static AggregatePathAssertions assertThat(AggregatePath actual) { + return new AggregatePathAssertions(actual); + } + + /** + * Example custom assertion method: Asserts that the AggregatePath has a specific property. + */ + public AggregatePathAssertions hasPath(String expectedPath) { + isNotNull(); + + if (!actual.toDotPath().equals(expectedPath)) { // Adjust this condition based on your AggregatePath's path logic + failWithMessage("Expected path to be <%s> but was <%s>", expectedPath, actual.toString()); + } + return this; + } + + public AggregatePathAssertions isRoot() { + isNotNull(); + + if (!actual.isRoot()) { + failWithMessage("Expected AggregatePath to be root path, but it was not"); + } + return this; + } + + public AggregatePathAssertions isNotRoot() { + isNotNull(); + + if (actual.isRoot()) { + failWithMessage("Expected AggregatePath not to be root path, but it was."); + } + return this; + } + + /** + * Example custom assertion method: Validates the depth of the path. + */ + public AggregatePathAssertions hasLength(int expectedLength) { + isNotNull(); + + if (actual.getLength() != expectedLength) { + failWithMessage("Expected path length to be <%d> but was <%d>", expectedLength, actual.getLength()); + } + return this; + } + +} \ No newline at end of file diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/AggregatePathSoftAssertions.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/AggregatePathSoftAssertions.java new file mode 100644 index 0000000000..1734552905 --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/AggregatePathSoftAssertions.java @@ -0,0 +1,31 @@ +/* + * Copyright 2025 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.mapping; + +import org.assertj.core.api.SoftAssertions; +import org.assertj.core.api.SoftAssertionsProvider; + +import java.util.function.Consumer; + +public class AggregatePathSoftAssertions extends SoftAssertions { + public AggregatePathAssertions assertAggregatePath(AggregatePath actual) { + return proxy(AggregatePathAssertions.class, AggregatePath.class, actual); + } + + static void assertAggregatePathsSoftly(Consumer softly) { + SoftAssertionsProvider.assertSoftly(AggregatePathSoftAssertions.class, softly); + } +} diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/ColumnInfosUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/ColumnInfosUnitTests.java index 54f0bf0482..fa1db8e202 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/ColumnInfosUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/ColumnInfosUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 the original author or authors. + * Copyright 2025 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. @@ -26,14 +26,16 @@ import org.junit.jupiter.api.Test; import org.springframework.data.annotation.Id; import org.springframework.data.relational.core.sql.SqlIdentifier; +import org.springframework.data.relational.core.sql.Table; /** * Unit tests for the construction of {@link org.springframework.data.relational.core.mapping.AggregatePath.ColumnInfos} - * + * * @author Jens Schauder */ class ColumnInfosUnitTests { + static final Table TABLE = Table.create("dummy"); static final SqlIdentifier ID = SqlIdentifier.quoted("ID"); RelationalMappingContext context = new RelationalMappingContext(); @@ -45,9 +47,7 @@ void emptyColumnInfos() { assertThat(columnInfos.isEmpty()).isTrue(); assertThrows(NoSuchElementException.class, columnInfos::any); assertThrows(IllegalStateException.class, columnInfos::unique); - assertThat(columnInfos.toList(ci -> { - throw new IllegalStateException("This should never get called"); - })).isEmpty(); + assertThat(columnInfos.toColumnList(TABLE)).isEmpty(); } @Test // GH-574 @@ -58,21 +58,21 @@ void singleElementColumnInfos() { assertThat(columnInfos.isEmpty()).isFalse(); assertThat(columnInfos.any().name()).isEqualTo(ID); assertThat(columnInfos.unique().name()).isEqualTo(ID); - assertThat(columnInfos.toList(ci -> ci.name())).containsExactly(ID); + assertThat(columnInfos.toColumnList(TABLE)).containsExactly(TABLE.column(ID)); } @Test // GH-574 void multiElementColumnInfos() { - AggregatePath.ColumnInfos columnInfos = basePath(DummyEntityWithCompositeId.class).getTableInfo().idColumnInfos(); + AggregatePath.ColumnInfos columnInfos = basePath(WithCompositeId.class).getTableInfo().idColumnInfos(); assertThat(columnInfos.isEmpty()).isFalse(); assertThat(columnInfos.any().name()).isEqualTo(SqlIdentifier.quoted("ONE")); assertThrows(IllegalStateException.class, columnInfos::unique); - assertThat(columnInfos.toList(ci -> ci.name())) // + assertThat(columnInfos.toColumnList(TABLE)) // .containsExactly( // - SqlIdentifier.quoted("ONE"), // - SqlIdentifier.quoted("TWO") // + TABLE.column(SqlIdentifier.quoted("ONE")), // + TABLE.column(SqlIdentifier.quoted("TWO")) // ); List collector = new ArrayList<>(); @@ -97,6 +97,6 @@ record DummyEntity(@Id String id, String name) { record CompositeId(String one, String two) { } - record DummyEntityWithCompositeId(@Id @Embedded.Nullable CompositeId id, String name) { + record WithCompositeId(@Id @Embedded.Nullable CompositeId id, String name) { } } diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/DefaultAggregatePathUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/DefaultAggregatePathUnitTests.java index 1abdc4ddd1..dfd90d2a43 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/DefaultAggregatePathUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/DefaultAggregatePathUnitTests.java @@ -30,7 +30,9 @@ import org.springframework.data.annotation.Id; import org.springframework.data.annotation.ReadOnlyProperty; import org.springframework.data.mapping.PersistentPropertyPath; +import org.springframework.data.relational.core.sql.Column; import org.springframework.data.relational.core.sql.SqlIdentifier; +import org.springframework.data.relational.core.sql.Table; /** * Tests for {@link AggregatePath}. @@ -48,7 +50,7 @@ void isNotRootForNonRootPath() { AggregatePath path = context.getAggregatePath(context.getPersistentPropertyPath("entityId", DummyEntity.class)); - assertThat(path.isRoot()).isFalse(); + AggregatePathAssertions.assertThat(path).isNotRoot(); } @Test // GH-1525 @@ -56,17 +58,17 @@ void isRootForRootPath() { AggregatePath path = context.getAggregatePath(entity); - assertThat(path.isRoot()).isTrue(); + AggregatePathAssertions.assertThat(path).isRoot(); } @Test // GH-1525 void getParentPath() { - assertSoftly(softly -> { + AggregatePathSoftAssertions.assertAggregatePathsSoftly(softly -> { - softly.assertThat((Object) path("second.third2.value").getParentPath()).isEqualTo(path("second.third2")); - softly.assertThat((Object) path("second.third2").getParentPath()).isEqualTo(path("second")); - softly.assertThat((Object) path("second").getParentPath()).isEqualTo(path()); + softly.assertAggregatePath(path("second.third2.value").getParentPath()).hasPath("second.third2"); + softly.assertAggregatePath(path("second.third2").getParentPath()).hasPath("second"); + softly.assertAggregatePath(path("second").getParentPath()).isRoot(); softly.assertThatThrownBy(() -> path().getParentPath()).isInstanceOf(IllegalStateException.class); }); @@ -77,13 +79,13 @@ void getRequiredLeafEntity() { assertSoftly(softly -> { + RelationalPersistentEntity secondEntity = context.getRequiredPersistentEntity(Second.class); + RelationalPersistentEntity thirdEntity = context.getRequiredPersistentEntity(Third.class); + softly.assertThat(path().getRequiredLeafEntity()).isEqualTo(entity); - softly.assertThat(path("second").getRequiredLeafEntity()) - .isEqualTo(context.getRequiredPersistentEntity(Second.class)); - softly.assertThat(path("second.third").getRequiredLeafEntity()) - .isEqualTo(context.getRequiredPersistentEntity(Third.class)); - softly.assertThat(path("secondList").getRequiredLeafEntity()) - .isEqualTo(context.getRequiredPersistentEntity(Second.class)); + softly.assertThat(path("second").getRequiredLeafEntity()).isEqualTo(secondEntity); + softly.assertThat(path("second.third").getRequiredLeafEntity()).isEqualTo(thirdEntity); + softly.assertThat(path("secondList").getRequiredLeafEntity()).isEqualTo(secondEntity); softly.assertThatThrownBy(() -> path("secondList.third.value").getRequiredLeafEntity()) .isInstanceOf(IllegalStateException.class); @@ -94,17 +96,16 @@ void getRequiredLeafEntity() { @Test // GH-1525 void idDefiningPath() { - assertSoftly(softly -> { + AggregatePathSoftAssertions.assertAggregatePathsSoftly(softly -> { - softly.assertThat((Object) path("second.third2.value").getIdDefiningParentPath()).isEqualTo(path()); - softly.assertThat((Object) path("second.third.value").getIdDefiningParentPath()).isEqualTo(path()); - softly.assertThat((Object) path("secondList.third2.value").getIdDefiningParentPath()).isEqualTo(path()); - softly.assertThat((Object) path("secondList.third.value").getIdDefiningParentPath()).isEqualTo(path()); - softly.assertThat((Object) path("second2.third2.value").getIdDefiningParentPath()).isEqualTo(path()); - softly.assertThat((Object) path("second2.third.value").getIdDefiningParentPath()).isEqualTo(path()); - softly.assertThat((Object) path("withId.second.third2.value").getIdDefiningParentPath()) - .isEqualTo(path("withId")); - softly.assertThat((Object) path("withId.second.third.value").getIdDefiningParentPath()).isEqualTo(path("withId")); + softly.assertAggregatePath(path("second.third2.value").getIdDefiningParentPath()).isRoot(); + softly.assertAggregatePath(path("second.third.value").getIdDefiningParentPath()).isRoot(); + softly.assertAggregatePath(path("secondList.third2.value").getIdDefiningParentPath()).isRoot(); + softly.assertAggregatePath(path("secondList.third.value").getIdDefiningParentPath()).isRoot(); + softly.assertAggregatePath(path("second2.third2.value").getIdDefiningParentPath()).isRoot(); + softly.assertAggregatePath(path("second2.third.value").getIdDefiningParentPath()).isRoot(); + softly.assertAggregatePath(path("withId.second.third2.value").getIdDefiningParentPath()).hasPath("withId"); + softly.assertAggregatePath(path("withId.second.third.value").getIdDefiningParentPath()).hasPath("withId"); }); } @@ -147,8 +148,10 @@ void reverseColumnName() { void reverseColumnNames() { assertSoftly(softly -> { - softly.assertThat(path(CompoundIdEntity.class, "second").getTableInfo().reverseColumnInfos().toList(x -> x)) - .extracting(AggregatePath.ColumnInfo::name) + softly + .assertThat(path(CompoundIdEntity.class, "second").getTableInfo().backReferenceColumnInfos() + .toColumnList(Table.create("dummy"))) + .extracting(Column::getName) .containsExactlyInAnyOrder(quoted("COMPOUND_ID_ENTITY_ONE"), quoted("COMPOUND_ID_ENTITY_TWO")); }); @@ -183,13 +186,11 @@ void getQualifierColumnType() { @Test // GH-1525 void extendBy() { + AggregatePathSoftAssertions.assertAggregatePathsSoftly(softly -> { - assertSoftly(softly -> { - - softly.assertThat((Object) path().append(entity.getRequiredPersistentProperty("withId"))) - .isEqualTo(path("withId")); - softly.assertThat((Object) path("withId").append(path("withId").getRequiredIdProperty())) - .isEqualTo(path("withId.withIdId")); + softly.assertAggregatePath(path().append(entity.getRequiredPersistentProperty("withId"))).hasPath("withId"); + softly.assertAggregatePath(path("withId").append(path("withId").getRequiredIdProperty())) + .hasPath("withId.withIdId"); }); } @@ -244,11 +245,11 @@ void isMultiValued() { softly.assertThat(path("second").isMultiValued()).isFalse(); softly.assertThat(path("second.third2").isMultiValued()).isFalse(); softly.assertThat(path("secondList.third2").isMultiValued()).isTrue(); // this seems wrong as third2 is an + // embedded path into Second, held by // List (so the parent is // multi-valued but not third2). - // TODO: This test fails because MultiValued considers parents. - // softly.assertThat(path("secondList.third.value").isMultiValued()).isFalse(); + softly.assertThat(path("secondList.third.value").isMultiValued()).isTrue(); softly.assertThat(path("secondList").isMultiValued()).isTrue(); }); } @@ -453,8 +454,7 @@ void getRequiredPersistentPropertyPath() { }); } - @Test - // GH-1525 + @Test // GH-1525 void getLength() { assertSoftly(softly -> { @@ -472,25 +472,24 @@ void getLength() { @Test // GH-574 void getTail() { - assertSoftly(softly -> { + AggregatePathSoftAssertions.assertAggregatePathsSoftly(softly -> { - softly.assertThat((Object) path().getTail()).isEqualTo(null); - softly.assertThat((Object) path("second").getTail()).isEqualTo(null); - softly.assertThat(path("second.third").getTail().toDotPath()).isEqualTo("third"); - softly.assertThat(path("second.third.value").getTail().toDotPath()).isEqualTo("third.value"); + softly.assertAggregatePath(path().getTail()).isNull(); + softly.assertAggregatePath(path("second").getTail()).isNull(); + softly.assertAggregatePath(path("second.third").getTail()).hasPath("third"); + softly.assertAggregatePath(path("second.third.value").getTail()).hasPath("third.value"); }); } @Test // GH-74 void append() { - assertSoftly(softly -> { - - softly.assertThat(path("second").append(path()).toDotPath()).isEqualTo("second"); - softly.assertThat(path().append(path("second")).toDotPath()).isEqualTo("second"); - softly.assertThat(path().append(path("second.third")).toDotPath()).isEqualTo("second.third"); + AggregatePathSoftAssertions.assertAggregatePathsSoftly(softly -> { + softly.assertAggregatePath(path("second").append(path())).hasPath("second"); + softly.assertAggregatePath(path().append(path("second"))).hasPath("second"); + softly.assertAggregatePath(path().append(path("second.third"))).hasPath("second.third"); AggregatePath value = path("second.third.value").getTail().getTail(); - softly.assertThat(path("second.third").append(value).toDotPath()).isEqualTo("second.third.value"); + softly.assertAggregatePath(path("second.third").append(value)).hasPath("second.third.value"); }); } diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/TupleExpressionUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/TupleExpressionUnitTests.java index b69a9ee10e..6ee97eacd7 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/TupleExpressionUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/TupleExpressionUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 the original author or authors. + * Copyright 2025 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. @@ -22,6 +22,11 @@ import org.junit.jupiter.api.Test; +/** + * Unit tests for construction of {@link TupleExpression}. + * + * @author Jens Schauder + */ class TupleExpressionUnitTests { @Test // GH-574 @@ -29,7 +34,7 @@ void singleExpressionDoesNotGetWrapped() { Column testColumn = Column.create("name", Table.create("employee")); - Expression wrapped = TupleExpression.maybeWrap(List.of(testColumn)); + Expression wrapped = Expressions.of(List.of(testColumn)); assertThat(wrapped).isSameAs(testColumn); } @@ -40,7 +45,7 @@ void multipleExpressionsDoGetWrapped() { Column testColumn1 = Column.create("first", Table.create("employee")); Column testColumn2 = Column.create("last", Table.create("employee")); - Expression wrapped = TupleExpression.maybeWrap(List.of(testColumn1, testColumn2)); + Expression wrapped = Expressions.of(List.of(testColumn1, testColumn2)); assertThat(wrapped).isInstanceOf(TupleExpression.class); } diff --git a/src/main/antora/modules/ROOT/partials/mapping-annotations.adoc b/src/main/antora/modules/ROOT/partials/mapping-annotations.adoc index e98d076c5d..4f5c4bac54 100644 --- a/src/main/antora/modules/ROOT/partials/mapping-annotations.adoc +++ b/src/main/antora/modules/ROOT/partials/mapping-annotations.adoc @@ -1,7 +1,13 @@ The `RelationalConverter` can use metadata to drive the mapping of objects to rows. The following annotations are available: -* `@Id`: Applied at the field level to mark the primary key. +* `@Embedded`: an entity with this annotation will be mapped to the table of the parent entity, instead of a separate table. Allows to specify if the resulting columns should have a common prefix. +If all columns resulting from such an entity are `null` either the annotated entity will be `null` or _empty_, i.e. all of its properties will be `null`, depending on the value of `@Embedded.onEmpty()` +May be combined with `@Id` to form a composite id. +* `@Id`: Applied at the field level to mark the primary key. It may be combined with `@Embedded` to form a composite id. +* `@InsertOnlyProperty`: Marks a property as only to be written during insert. Such a property on an aggregate root will only be written once and never updated. Note that on a nested entity, all save operations result in an insert therefore this annotation has no effect on properties of nested entities. +* `@MappedCollection`: Allows for configuration how a collection, or a single nested entity gets mapped. `idColumn` specifies the column used for referencing the parent entities primary key. `keyColumn` specifies the column used to store the index of a `List` or the key of a `Map`. +* `@Sequence`: specify a database sequence for generating values for the annotated property. * `@Table`: Applied at the class level to indicate this class is a candidate for mapping to the database. You can specify the name of the table where the database is stored. * `@Transient`: By default, all fields are mapped to the row. @@ -22,3 +28,5 @@ However, this is not recommended, since it may cause problems with other tools. The value is `null` (`zero` for primitive types) is considered as marker for entities to be new. The initially stored value is `zero` (`one` for primitive types). The version gets incremented automatically on every update. + + diff --git a/src/main/antora/modules/ROOT/partials/mapping.adoc b/src/main/antora/modules/ROOT/partials/mapping.adoc index ed80c37fab..8295714e4c 100644 --- a/src/main/antora/modules/ROOT/partials/mapping.adoc +++ b/src/main/antora/modules/ROOT/partials/mapping.adoc @@ -156,6 +156,42 @@ Entities may be annotated with `@Id` and `@Embedded`, resulting in a composite i The full embedded entity is considered the id, and therefore the check for determining if an aggregate is considered a new aggregate requiring an insert or an existing one, asking for an update is based on that entity, not its elements. Most use cases will require a custom `BeforeConvertCallback` to set the id for new aggregate. +==== +.Simple entity with composite id +[source,java] +---- +@Table("PERSON_WITH_COMPOSITE_ID") +record Person( <1> + @Id @Embedded.Nullable Name pk, <2> + String nickName, + Integer age +) { +} + +record Name(String first, String last) { +} +---- + +.Matching table for simple entity with composite id +[source,sql] +---- +CREATE TABLE PERSON_WITH_COMPOSITE_ID ( + FIRST VARCHAR(100), + LAST VARCHAR(100), + NICK_NAME VARCHAR(100), + AGE INT, + PRIMARY KEY (FIRST, LAST) <3> +); + + +---- +<1> Entities may be represented as records without any special consideration +<2> `pk` is marked as id and embedded +<3> the two columns from the embedded `Name` entity make up the primary key in the database. + +Details of the create tables will depend on the database used. +==== + [[entity-persistence.read-only-properties]] == Read Only Properties From ffab8412744797812ece950cc7240cfdeddf6c51 Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Tue, 6 May 2025 12:04:00 +0200 Subject: [PATCH 21/21] Polishing --- .../JdbcAggregateChangeExecutionContext.java | 69 ++++++++++++------- .../data/jdbc/core/convert/Identifier.java | 3 +- .../core/convert/JdbcIdentifierBuilder.java | 27 ++++---- .../core/convert/MappingJdbcConverter.java | 27 ++++---- .../data/jdbc/core/convert/SqlContext.java | 1 + .../data/jdbc/core/convert/SqlGenerator.java | 10 ++- .../jdbc/core/convert/SqlGeneratorSource.java | 1 - .../core/convert/SqlParametersFactory.java | 59 +++++++++------- .../query/JdbcDeleteQueryCreator.java | 17 +++-- .../repository/query/JdbcQueryCreator.java | 12 +++- ...AggregateTemplateHsqlIntegrationTests.java | 7 +- ...angeExecutorContextImmutableUnitTests.java | 28 ++++---- ...gregateChangeExecutorContextUnitTests.java | 3 +- .../JdbcIdentifierBuilderUnitTests.java | 13 ++-- .../SqlGeneratorEmbeddedUnitTests.java | 6 +- .../support/SimpleR2dbcRepository.java | 3 +- ...CompositeIdRepositoryIntegrationTests.java | 10 +-- .../core/mapping/AggregatePath.java | 11 +-- .../core/mapping/ColumInfosBuilder.java | 16 ++--- .../core/mapping/DefaultAggregatePath.java | 1 - .../mapping/RelationalPersistentEntity.java | 3 +- .../relational/core/sql/AnalyticFunction.java | 4 +- .../data/relational/core/sql/Expressions.java | 17 +++-- .../relational/core/sql/TupleExpression.java | 2 +- .../core/sql/render/TupleVisitor.java | 4 +- .../core/mapping/AggregatePathAssertions.java | 36 +++++----- .../mapping/AggregatePathSoftAssertions.java | 14 +++- .../modules/ROOT/pages/jdbc/mapping.adoc | 3 +- .../ROOT/partials/mapping-annotations.adoc | 10 ++- .../antora/modules/ROOT/partials/mapping.adoc | 2 +- 30 files changed, 249 insertions(+), 170 deletions(-) diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutionContext.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutionContext.java index ec1c151400..57dc1ff487 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutionContext.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutionContext.java @@ -73,16 +73,19 @@ class JdbcAggregateChangeExecutionContext { void executeInsertRoot(DbAction.InsertRoot insert) { - Object id = accessStrategy.insert(insert.getEntity(), insert.getEntityType(), Identifier.empty(), insert.getIdValueSource()); + Object id = accessStrategy.insert(insert.getEntity(), insert.getEntityType(), Identifier.empty(), + insert.getIdValueSource()); add(new DbActionExecutionResult(insert, id)); } void executeBatchInsertRoot(DbAction.BatchInsertRoot batchInsertRoot) { List> inserts = batchInsertRoot.getActions(); - List> insertSubjects = inserts.stream().map(insert -> InsertSubject.describedBy(insert.getEntity(), Identifier.empty())).collect(Collectors.toList()); + List> insertSubjects = inserts.stream() + .map(insert -> InsertSubject.describedBy(insert.getEntity(), Identifier.empty())).collect(Collectors.toList()); - Object[] ids = accessStrategy.insert(insertSubjects, batchInsertRoot.getEntityType(), batchInsertRoot.getBatchValue()); + Object[] ids = accessStrategy.insert(insertSubjects, batchInsertRoot.getEntityType(), + batchInsertRoot.getBatchValue()); for (int i = 0; i < inserts.size(); i++) { add(new DbActionExecutionResult(inserts.get(i), ids.length > 0 ? ids[i] : null)); @@ -92,14 +95,17 @@ void executeBatchInsertRoot(DbAction.BatchInsertRoot batchInsertRoot) { void executeInsert(DbAction.Insert insert) { Identifier parentKeys = getParentKeys(insert, converter); - Object id = accessStrategy.insert(insert.getEntity(), insert.getEntityType(), parentKeys, insert.getIdValueSource()); + Object id = accessStrategy.insert(insert.getEntity(), insert.getEntityType(), parentKeys, + insert.getIdValueSource()); add(new DbActionExecutionResult(insert, id)); } void executeBatchInsert(DbAction.BatchInsert batchInsert) { List> inserts = batchInsert.getActions(); - List> insertSubjects = inserts.stream().map(insert -> InsertSubject.describedBy(insert.getEntity(), getParentKeys(insert, converter))).collect(Collectors.toList()); + List> insertSubjects = inserts.stream() + .map(insert -> InsertSubject.describedBy(insert.getEntity(), getParentKeys(insert, converter))) + .collect(Collectors.toList()); Object[] ids = accessStrategy.insert(insertSubjects, batchInsert.getEntityType(), batchInsert.getBatchValue()); @@ -174,7 +180,8 @@ private Identifier getParentKeys(DbAction.WithDependingOn action, JdbcConvert .forBackReferences(converter, context.getAggregatePath(action.getPropertyPath()), getValueProvider(id, context.getAggregatePath(action.getPropertyPath()), converter)); - for (Map.Entry, Object> qualifier : action.getQualifiers().entrySet()) { + for (Map.Entry, Object> qualifier : action.getQualifiers() + .entrySet()) { identifier = identifier.withQualifier(context.getAggregatePath(qualifier.getKey()), qualifier.getValue()); } @@ -183,7 +190,8 @@ private Identifier getParentKeys(DbAction.WithDependingOn action, JdbcConvert static Function getValueProvider(Object idValue, AggregatePath path, JdbcConverter converter) { - RelationalPersistentEntity entity = converter.getMappingContext().getPersistentEntity(path.getIdDefiningParentPath().getRequiredIdProperty().getType()); + RelationalPersistentEntity entity = converter.getMappingContext() + .getPersistentEntity(path.getIdDefiningParentPath().getRequiredIdProperty().getType()); Function valueProvider = ap -> { if (entity == null) { @@ -198,7 +206,8 @@ static Function getValueProvider(Object idValue, Aggregat private Object getParentId(DbAction.WithDependingOn action) { - DbAction.WithEntity idOwningAction = getIdOwningAction(action, context.getAggregatePath(action.getPropertyPath()).getIdDefiningParentPath()); + DbAction.WithEntity idOwningAction = getIdOwningAction(action, + context.getAggregatePath(action.getPropertyPath()).getIdDefiningParentPath()); return getPotentialGeneratedIdFrom(idOwningAction); } @@ -207,7 +216,8 @@ private DbAction.WithEntity getIdOwningAction(DbAction.WithEntity action, if (!(action instanceof DbAction.WithDependingOn withDependingOn)) { - Assert.state(idPath.isRoot(), "When the id path is not empty the id providing action should be of type WithDependingOn"); + Assert.state(idPath.isRoot(), + "When the id path is not empty the id providing action should be of type WithDependingOn"); return action; } @@ -284,7 +294,9 @@ List populateIdsIfNecessary() { } if (roots.isEmpty()) { - throw new IllegalStateException(String.format("Cannot retrieve the resulting instance(s) unless a %s or %s action was successfully executed", DbAction.InsertRoot.class.getName(), DbAction.UpdateRoot.class.getName())); + throw new IllegalStateException( + String.format("Cannot retrieve the resulting instance(s) unless a %s or %s action was successfully executed", + DbAction.InsertRoot.class.getName(), DbAction.UpdateRoot.class.getName())); } Collections.reverse(roots); @@ -293,19 +305,23 @@ List populateIdsIfNecessary() { } @SuppressWarnings("unchecked") - private Object setIdAndCascadingProperties(DbAction.WithEntity action, @Nullable Object generatedId, StagedValues cascadingValues) { + private Object setIdAndCascadingProperties(DbAction.WithEntity action, @Nullable Object generatedId, + StagedValues cascadingValues) { S originalEntity = action.getEntity(); - RelationalPersistentEntity persistentEntity = (RelationalPersistentEntity) context.getRequiredPersistentEntity(action.getEntityType()); - PersistentPropertyPathAccessor propertyAccessor = converter.getPropertyAccessor(persistentEntity, originalEntity); + RelationalPersistentEntity persistentEntity = (RelationalPersistentEntity) context + .getRequiredPersistentEntity(action.getEntityType()); + PersistentPropertyPathAccessor propertyAccessor = converter.getPropertyAccessor(persistentEntity, + originalEntity); if (IdValueSource.GENERATED.equals(action.getIdValueSource())) { propertyAccessor.setProperty(persistentEntity.getRequiredIdProperty(), generatedId); } // set values of changed immutables referenced by this entity - cascadingValues.forEachPath(action, (persistentPropertyPath, o) -> propertyAccessor.setProperty(getRelativePath(action, persistentPropertyPath), o)); + cascadingValues.forEachPath(action, (persistentPropertyPath, o) -> propertyAccessor + .setProperty(getRelativePath(action, persistentPropertyPath), o)); return propertyAccessor.getBean(); } @@ -337,7 +353,8 @@ private void updateWithoutVersion(DbAction.UpdateRoot update) { if (!accessStrategy.update(update.getEntity(), update.getEntityType())) { - throw new IncorrectUpdateSemanticsDataAccessException(String.format(UPDATE_FAILED, update.getEntity(), getIdFrom(update))); + throw new IncorrectUpdateSemanticsDataAccessException( + String.format(UPDATE_FAILED, update.getEntity(), getIdFrom(update))); } } @@ -358,7 +375,8 @@ private void updateWithVersion(DbAction.UpdateRoot update) { */ private static class StagedValues { - static final List> aggregators = Arrays.asList(SetAggregator.INSTANCE, MapAggregator.INSTANCE, ListAggregator.INSTANCE, SingleElementAggregator.INSTANCE); + static final List> aggregators = Arrays.asList(SetAggregator.INSTANCE, + MapAggregator.INSTANCE, ListAggregator.INSTANCE, SingleElementAggregator.INSTANCE); Map> values = new HashMap<>(); @@ -366,12 +384,12 @@ private static class StagedValues { * Adds a value that needs to be set in an entity higher up in the tree of entities in the aggregate. If the * attribute to be set is multivalued this method expects only a single element. * - * @param action The action responsible for persisting the entity that needs the added value set. Must not be - * {@literal null}. - * @param path The path to the property in which to set the value. Must not be {@literal null}. + * @param action The action responsible for persisting the entity that needs the added value set. Must not be + * {@literal null}. + * @param path The path to the property in which to set the value. Must not be {@literal null}. * @param qualifier If {@code path} is a qualified multivalued properties this parameter contains the qualifier. May - * be {@literal null}. - * @param value The value to be set. Must not be {@literal null}. + * be {@literal null}. + * @param value The value to be set. Must not be {@literal null}. */ void stage(DbAction action, PersistentPropertyPath path, @Nullable Object qualifier, Object value) { @@ -384,9 +402,11 @@ StagedValue gather(DbAction action, PersistentPropertyPath path, @Nullabl MultiValueAggregator aggregator = getAggregatorFor(path); - Map valuesForPath = this.values.computeIfAbsent(action, dbAction -> new HashMap<>()); + Map valuesForPath = this.values.computeIfAbsent(action, + dbAction -> new HashMap<>()); - StagedValue stagedValue = valuesForPath.computeIfAbsent(path, persistentPropertyPath -> new StagedValue(aggregator.createEmptyInstance())); + StagedValue stagedValue = valuesForPath.computeIfAbsent(path, + persistentPropertyPath -> new StagedValue(aggregator.createEmptyInstance())); T currentValue = (T) stagedValue.value; stagedValue.value = aggregator.add(currentValue, qualifier, value); @@ -426,8 +446,7 @@ void forEachPath(DbAction dbAction, BiConsumer map) { * @param identifier the identifier to append. * @return the {@link Identifier} containing all existing keys and the key part for {@code name}, {@code value}, and a * {@link Class target type}. - * @since 3.5 + * @since 4.0 */ public Identifier withPart(Identifier identifier) { @@ -207,7 +207,6 @@ public Object get(SqlIdentifier columnName) { return null; } - /** * A single value of an Identifier consisting of the column name, the value and the target type which is to be used to * store the element in the database. diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilder.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilder.java index 7a3ac58a2a..24213662ff 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilder.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilder.java @@ -42,20 +42,24 @@ public static JdbcIdentifierBuilder empty() { /** * Creates ParentKeys with backreference for the given path and value of the parents id. */ - // gets called during insert. value contains the id from an insert of the parent - public static JdbcIdentifierBuilder forBackReferences(JdbcConverter converter, AggregatePath path, Function valueProvider) { + public static JdbcIdentifierBuilder forBackReferences(JdbcConverter converter, AggregatePath path, + Function valueProvider) { return new JdbcIdentifierBuilder(forBackReference(converter, path, Identifier.empty(), valueProvider)); } /** - * @param converter used for determining the column types to be used for different properties. Must not be {@literal null}. - * @param path the path for which needs to back reference an id. Must not be {@literal null}. - * @param defaultIdentifier Identifier to be used as a default when no backreference can be constructed. Must not be {@literal null}. - * @param valueProvider provides values for the {@link Identifier} based on an {@link AggregatePath}. Must not be {@literal null}. + * @param converter used for determining the column types to be used for different properties. Must not be + * {@literal null}. + * @param path the path for which needs to back reference an id. Must not be {@literal null}. + * @param defaultIdentifier Identifier to be used as a default when no backreference can be constructed. Must not be + * {@literal null}. + * @param valueProvider provides values for the {@link Identifier} based on an {@link AggregatePath}. Must not be + * {@literal null}. * @return Guaranteed not to be {@literal null}. */ - public static Identifier forBackReference(JdbcConverter converter, AggregatePath path, Identifier defaultIdentifier, Function valueProvider) { + public static Identifier forBackReference(JdbcConverter converter, AggregatePath path, Identifier defaultIdentifier, + Function valueProvider) { Identifier identifierToUse = defaultIdentifier; @@ -69,19 +73,17 @@ public static Identifier forBackReference(JdbcConverter converter, AggregatePath identifierToUse = infos.reduce(Identifier.empty(), (ap, ci) -> { RelationalPersistentProperty property = ap.getRequiredLeafProperty(); - return Identifier.of(ci.name(), valueProvider.apply(ap), - converter.getColumnType(property)); + return Identifier.of(ci.name(), valueProvider.apply(ap), converter.getColumnType(property)); }, Identifier::withPart); } return identifierToUse; } - /** * Adds a qualifier to the identifier to build. A qualifier is a map key or a list index. * - * @param path path to the map that gets qualified by {@code value}. Must not be {@literal null}. + * @param path path to the map that gets qualified by {@code value}. Must not be {@literal null}. * @param value map key or list index qualifying the map identified by {@code path}. Must not be {@literal null}. * @return this builder. Guaranteed to be not {@literal null}. */ @@ -91,8 +93,7 @@ public JdbcIdentifierBuilder withQualifier(AggregatePath path, Object value) { Assert.notNull(value, "Value must not be null"); AggregatePath.TableInfo tableInfo = path.getTableInfo(); - identifier = identifier.withPart(tableInfo.qualifierColumnInfo().name(), value, - tableInfo.qualifierColumnType()); + identifier = identifier.withPart(tableInfo.qualifierColumnInfo().name(), value, tableInfo.qualifierColumnType()); return this; } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverter.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverter.java index 707a860909..b1d74f1876 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverter.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverter.java @@ -80,7 +80,7 @@ public class MappingJdbcConverter extends MappingRelationalConverter implements * {@link #MappingJdbcConverter(RelationalMappingContext, RelationResolver, CustomConversions, JdbcTypeFactory)} * (MappingContext, RelationResolver, JdbcTypeFactory)} to convert arrays and large objects into JDBC-specific types. * - * @param context must not be {@literal null}. + * @param context must not be {@literal null}. * @param relationResolver used to fetch additional relations from the database. Must not be {@literal null}. */ public MappingJdbcConverter(RelationalMappingContext context, RelationResolver relationResolver) { @@ -98,12 +98,12 @@ public MappingJdbcConverter(RelationalMappingContext context, RelationResolver r /** * Creates a new {@link MappingJdbcConverter} given {@link MappingContext}. * - * @param context must not be {@literal null}. + * @param context must not be {@literal null}. * @param relationResolver used to fetch additional relations from the database. Must not be {@literal null}. - * @param typeFactory must not be {@literal null} + * @param typeFactory must not be {@literal null} */ public MappingJdbcConverter(RelationalMappingContext context, RelationResolver relationResolver, - CustomConversions conversions, JdbcTypeFactory typeFactory) { + CustomConversions conversions, JdbcTypeFactory typeFactory) { super(context, conversions); @@ -285,7 +285,7 @@ public R readAndResolve(TypeInformation type, RowDocument source, Identif @Override protected RelationalPropertyValueProvider newValueProvider(RowDocumentAccessor documentAccessor, - ValueExpressionEvaluator evaluator, ConversionContext context) { + ValueExpressionEvaluator evaluator, ConversionContext context) { if (context instanceof ResolvingConversionContext rcc) { @@ -314,7 +314,7 @@ class ResolvingRelationalPropertyValueProvider implements RelationalPropertyValu private final Identifier identifier; private ResolvingRelationalPropertyValueProvider(AggregatePathValueProvider delegate, RowDocumentAccessor accessor, - ResolvingConversionContext context, Identifier identifier) { + ResolvingConversionContext context, Identifier identifier) { AggregatePath path = context.aggregatePath(); @@ -323,7 +323,7 @@ private ResolvingRelationalPropertyValueProvider(AggregatePathValueProvider dele this.context = context; this.identifier = path.isEntity() ? potentiallyAppendIdentifier(identifier, path.getRequiredLeafEntity(), - property -> delegate.getValue(path.append(property))) + property -> delegate.getValue(path.append(property))) : identifier; } @@ -331,7 +331,7 @@ private ResolvingRelationalPropertyValueProvider(AggregatePathValueProvider dele * Conditionally append the identifier if the entity has an identifier property. */ static Identifier potentiallyAppendIdentifier(Identifier base, RelationalPersistentEntity entity, - Function getter) { + Function getter) { if (entity.hasIdProperty()) { @@ -361,7 +361,8 @@ public T getPropertyValue(RelationalPersistentProperty property) { if (property.isCollectionLike() || property.isMap()) { - Identifier identifier = JdbcIdentifierBuilder.forBackReference(MappingJdbcConverter.this, aggregatePath, this.identifier, getWrappedValueProvider(delegate::getValue, aggregatePath)); + Identifier identifier = JdbcIdentifierBuilder.forBackReference(MappingJdbcConverter.this, aggregatePath, + this.identifier, getWrappedValueProvider(delegate::getValue, aggregatePath)); Iterable allByPath = relationResolver.findAllByPath(identifier, aggregatePath.getRequiredPersistentPropertyPath()); @@ -445,11 +446,12 @@ public RelationalPropertyValueProvider withContext(ConversionContext context) { return context == this.context ? this : new ResolvingRelationalPropertyValueProvider(delegate.withContext(context), accessor, - (ResolvingConversionContext) context, identifier); + (ResolvingConversionContext) context, identifier); } } - private static Function getWrappedValueProvider(Function valueProvider, AggregatePath aggregatePath) { + private static Function getWrappedValueProvider(Function valueProvider, + AggregatePath aggregatePath) { AggregatePath idDefiningParentPath = aggregatePath.getIdDefiningParentPath(); @@ -464,7 +466,6 @@ private static Function getWrappedValueProvider(Function< return ap -> valueProvider.apply(idPath.append(ap)); } - /** * Marker object to indicate that the property value provider should resolve relations. * @@ -473,7 +474,7 @@ private static Function getWrappedValueProvider(Function< * @param identifier */ private record ResolvingConversionContext(ConversionContext delegate, AggregatePath aggregatePath, - Identifier identifier) implements ConversionContext { + Identifier identifier) implements ConversionContext { @Override public S convert(Object source, TypeInformation typeHint) { diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlContext.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlContext.java index c511e58dda..586da2c22f 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlContext.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlContext.java @@ -66,6 +66,7 @@ Column getColumn(AggregatePath path) { * * @param path must not be null. * @return a {@literal Column} that is part of the effective primary key for the given path. + * @since 4.0 */ Column getAnyReverseColumn(AggregatePath path) { 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 7e573c3f63..dfc4d81ce5 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 @@ -113,7 +113,15 @@ public class SqlGenerator { this.dialect = dialect; } - + /** + * Create a basic select structure with all the necessary joins + * + * @param table the table to base the select on + * @param pathFilter a filter for excluding paths from the select. All paths for which the filter returns + * {@literal true} will be skipped when determining columns to select. + * @return A select structure suitable for constructing more specialized selects by adding conditions. + * @since 4.0 + */ public SelectBuilder.SelectWhere createSelectBuilder(Table table, Predicate pathFilter) { return createSelectBuilder(table, pathFilter, Collections.emptyList()); } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGeneratorSource.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGeneratorSource.java index 9f5389e8c1..5f5d9de361 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGeneratorSource.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGeneratorSource.java @@ -19,7 +19,6 @@ import org.springframework.data.relational.core.dialect.Dialect; import org.springframework.data.relational.core.mapping.RelationalMappingContext; -import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.util.Assert; import org.springframework.util.ConcurrentReferenceHashMap; diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlParametersFactory.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlParametersFactory.java index 37ddde10ae..0fdf3d5be0 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlParametersFactory.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlParametersFactory.java @@ -62,19 +62,21 @@ public SqlParametersFactory(RelationalMappingContext context, JdbcConverter conv /** * Creates the parameters for a SQL insert operation. * - * @param instance the entity to be inserted. Must not be {@code null}. - * @param domainType the type of the instance. Must not be {@code null}. - * @param identifier information about data that needs to be considered for the insert but which is not part of the - * entity. Namely references back to a parent entity and key/index columns for entities that are stored in a - * {@link Map} or {@link List}. + * @param instance the entity to be inserted. Must not be {@code null}. + * @param domainType the type of the instance. Must not be {@code null}. + * @param identifier information about data that needs to be considered for the insert but which is not part of the + * entity. Namely references back to a parent entity and key/index columns for entities that are stored in a + * {@link Map} or {@link List}. * @param idValueSource the {@link IdValueSource} for the insert. * @return the {@link SqlIdentifierParameterSource} for the insert. Guaranteed to not be {@code null}. * @since 2.4 */ - SqlIdentifierParameterSource forInsert(T instance, Class domainType, Identifier identifier, IdValueSource idValueSource) { + SqlIdentifierParameterSource forInsert(T instance, Class domainType, Identifier identifier, + IdValueSource idValueSource) { RelationalPersistentEntity persistentEntity = getRequiredPersistentEntity(domainType); - SqlIdentifierParameterSource parameterSource = getParameterSource(instance, persistentEntity, "", PersistentProperty::isIdProperty); + SqlIdentifierParameterSource parameterSource = getParameterSource(instance, persistentEntity, "", + PersistentProperty::isIdProperty); identifier.forEach((name, value, type) -> addConvertedPropertyValue(parameterSource, name, value, type)); @@ -96,20 +98,21 @@ SqlIdentifierParameterSource forInsert(T instance, Class domainType, Iden /** * Creates the parameters for a SQL update operation. * - * @param instance the entity to be updated. Must not be {@code null}. + * @param instance the entity to be updated. Must not be {@code null}. * @param domainType the type of the instance. Must not be {@code null}. * @return the {@link SqlIdentifierParameterSource} for the update. Guaranteed to not be {@code null}. * @since 2.4 */ SqlIdentifierParameterSource forUpdate(T instance, Class domainType) { - return getParameterSource(instance, getRequiredPersistentEntity(domainType), "", RelationalPersistentProperty::isInsertOnly); + return getParameterSource(instance, getRequiredPersistentEntity(domainType), "", + RelationalPersistentProperty::isInsertOnly); } /** * Creates the parameters for a SQL query by id. * - * @param id the entity id. Must not be {@code null}. + * @param id the entity id. Must not be {@code null}. * @param domainType the type of the instance. Must not be {@code null}. * @return the {@link SqlIdentifierParameterSource} for the query. Guaranteed to not be {@code null}. * @since 2.4 @@ -123,8 +126,7 @@ SqlIdentifierParameterSource forQueryById(Object id, Class domainType) { RelationalPersistentEntity complexId = context.getPersistentEntity(singleIdProperty); - Function valueExtractor = complexId == null - ? ap -> id + Function valueExtractor = complexId == null ? ap -> id : ap -> complexId.getPropertyPathAccessor(id).getProperty(ap.getRequiredPersistentPropertyPath()); context.getAggregatePath(entity).getTableInfo().idColumnInfos() // @@ -140,7 +142,7 @@ SqlIdentifierParameterSource forQueryById(Object id, Class domainType) { /** * Creates the parameters for a SQL query by ids. * - * @param ids the entity ids. Must not be {@code null}. + * @param ids the entity ids. Must not be {@code null}. * @param domainType the type of the instance. Must not be {@code null}. * @return the {@link SqlIdentifierParameterSource} for the query. Guaranteed to not be {@code null}. * @since 2.4 @@ -154,8 +156,7 @@ SqlIdentifierParameterSource forQueryByIds(Iterable ids, Class domainT RelationalPersistentEntity complexId = context.getPersistentEntity(singleIdProperty); AggregatePath.ColumnInfos idColumnInfos = context.getAggregatePath(entity).getTableInfo().idColumnInfos(); - BiFunction valueExtractor = complexId == null - ? (id, ap) -> id + BiFunction valueExtractor = complexId == null ? (id, ap) -> id : (id, ap) -> complexId.getPropertyPathAccessor(id).getProperty(ap.getRequiredPersistentPropertyPath()); List parameterValues = new ArrayList<>(); @@ -184,22 +185,27 @@ SqlIdentifierParameterSource forQueryByIdentifier(Identifier identifier) { SqlIdentifierParameterSource parameterSource = new SqlIdentifierParameterSource(); - identifier.toMap().forEach((name, value) -> addConvertedPropertyValue(parameterSource, name, value, value.getClass())); + identifier.toMap() + .forEach((name, value) -> addConvertedPropertyValue(parameterSource, name, value, value.getClass())); return parameterSource; } - private void addConvertedPropertyValue(SqlIdentifierParameterSource parameterSource, RelationalPersistentProperty property, @Nullable Object value, SqlIdentifier name) { + private void addConvertedPropertyValue(SqlIdentifierParameterSource parameterSource, + RelationalPersistentProperty property, @Nullable Object value, SqlIdentifier name) { - addConvertedValue(parameterSource, value, name, converter.getColumnType(property), converter.getTargetSqlType(property)); + addConvertedValue(parameterSource, value, name, converter.getColumnType(property), + converter.getTargetSqlType(property)); } - private void addConvertedPropertyValue(SqlIdentifierParameterSource parameterSource, SqlIdentifier name, Object value, Class javaType) { + private void addConvertedPropertyValue(SqlIdentifierParameterSource parameterSource, SqlIdentifier name, Object value, + Class javaType) { addConvertedValue(parameterSource, value, name, javaType, JdbcUtil.targetSqlTypeFor(javaType)); } - private void addConvertedValue(SqlIdentifierParameterSource parameterSource, @Nullable Object value, SqlIdentifier paramName, Class javaType, SQLType sqlType) { + private void addConvertedValue(SqlIdentifierParameterSource parameterSource, @Nullable Object value, + SqlIdentifier paramName, Class javaType, SQLType sqlType) { JdbcValue jdbcValue = converter.writeJdbcValue( // value, // @@ -213,7 +219,8 @@ private void addConvertedValue(SqlIdentifierParameterSource parameterSource, @Nu jdbcValue.getJdbcType().getVendorTypeNumber()); } - private void addConvertedPropertyValuesAsList(SqlIdentifierParameterSource parameterSource, RelationalPersistentProperty property, Iterable values) { + private void addConvertedPropertyValuesAsList(SqlIdentifierParameterSource parameterSource, + RelationalPersistentProperty property, Iterable values) { List convertedIds = new ArrayList<>(); JdbcValue jdbcValue = null; @@ -239,11 +246,14 @@ private RelationalPersistentEntity getRequiredPersistentEntity(Class d return (RelationalPersistentEntity) context.getRequiredPersistentEntity(domainType); } - private SqlIdentifierParameterSource getParameterSource(@Nullable S instance, RelationalPersistentEntity persistentEntity, String prefix, Predicate skipProperty) { + private SqlIdentifierParameterSource getParameterSource(@Nullable S instance, + RelationalPersistentEntity persistentEntity, String prefix, + Predicate skipProperty) { SqlIdentifierParameterSource parameters = new SqlIdentifierParameterSource(); - PersistentPropertyAccessor propertyAccessor = instance != null ? persistentEntity.getPropertyAccessor(instance) : NoValuePropertyAccessor.instance(); + PersistentPropertyAccessor propertyAccessor = instance != null ? persistentEntity.getPropertyAccessor(instance) + : NoValuePropertyAccessor.instance(); persistentEntity.doWithAll(property -> { @@ -258,7 +268,8 @@ private SqlIdentifierParameterSource getParameterSource(@Nullable S insta Object value = propertyAccessor.getProperty(property); RelationalPersistentEntity embeddedEntity = context.getPersistentEntity(property.getTypeInformation()); - SqlIdentifierParameterSource additionalParameters = getParameterSource((T) value, (RelationalPersistentEntity) embeddedEntity, prefix + property.getEmbeddedPrefix(), skipProperty); + SqlIdentifierParameterSource additionalParameters = getParameterSource((T) value, + (RelationalPersistentEntity) embeddedEntity, prefix + property.getEmbeddedPrefix(), skipProperty); parameters.addAll(additionalParameters); } else { diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcDeleteQueryCreator.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcDeleteQueryCreator.java index d669cf3e20..f81cc62260 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcDeleteQueryCreator.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcDeleteQueryCreator.java @@ -29,9 +29,17 @@ import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.core.query.Criteria; -import org.springframework.data.relational.core.sql.*; +import org.springframework.data.relational.core.sql.Column; +import org.springframework.data.relational.core.sql.Condition; +import org.springframework.data.relational.core.sql.Conditions; +import org.springframework.data.relational.core.sql.Delete; import org.springframework.data.relational.core.sql.DeleteBuilder.DeleteWhere; +import org.springframework.data.relational.core.sql.Expression; +import org.springframework.data.relational.core.sql.Expressions; +import org.springframework.data.relational.core.sql.Select; import org.springframework.data.relational.core.sql.SelectBuilder.SelectWhere; +import org.springframework.data.relational.core.sql.StatementBuilder; +import org.springframework.data.relational.core.sql.Table; import org.springframework.data.relational.core.sql.render.SqlRenderer; import org.springframework.data.relational.repository.query.RelationalEntityMetadata; import org.springframework.data.relational.repository.query.RelationalParameterAccessor; @@ -125,11 +133,11 @@ private void deleteRelations(List deleteChain, RelationalPersistentEntit AggregatePath aggregatePath = context.getAggregatePath(path); - if (aggregatePath.isEmbedded()){ + if (aggregatePath.isEmbedded()) { continue; } - if (aggregatePath.isEntity() ) { + if (aggregatePath.isEntity()) { SqlContext sqlContext = new SqlContext(); @@ -141,7 +149,8 @@ private void deleteRelations(List deleteChain, RelationalPersistentEntit Condition inCondition = Conditions.in(expression, parentSelect); - List parentIdColumns = aggregatePath.getIdDefiningParentPath().getTableInfo().idColumnInfos().toColumnList(table); + List parentIdColumns = aggregatePath.getIdDefiningParentPath().getTableInfo().idColumnInfos() + .toColumnList(table); Select select = StatementBuilder.select( // parentIdColumns // 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 7ed485532d..fa7202a4a8 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 @@ -31,7 +31,12 @@ import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.core.query.Criteria; -import org.springframework.data.relational.core.sql.*; +import org.springframework.data.relational.core.sql.Column; +import org.springframework.data.relational.core.sql.Expressions; +import org.springframework.data.relational.core.sql.Functions; +import org.springframework.data.relational.core.sql.Select; +import org.springframework.data.relational.core.sql.SelectBuilder; +import org.springframework.data.relational.core.sql.Table; import org.springframework.data.relational.core.sql.render.SqlRenderer; import org.springframework.data.relational.repository.Lock; import org.springframework.data.relational.repository.query.RelationalEntityMetadata; @@ -106,6 +111,7 @@ class JdbcQueryCreator extends RelationalQueryCreator { * @param lockMode lock mode to be used for the query. * @param sqlGeneratorSource the source providing SqlGenerator instances for generating SQL. Must not be * {@literal null} + * @since 4.0 */ JdbcQueryCreator(RelationalMappingContext context, PartTree tree, JdbcConverter converter, Dialect dialect, RelationalEntityMetadata entityMetadata, RelationalParameterAccessor accessor, boolean isSliceQuery, @@ -263,8 +269,8 @@ private SelectBuilder.SelectJoin selectBuilder(Table table) { Predicate filter = ap -> returnedType.needsCustomConstruction() && !returnedType.getInputProperties().contains(ap.getRequiredBaseProperty().getName()); - return (SelectBuilder.SelectJoin) sqlGeneratorSource.getSqlGenerator(entity.getType()).createSelectBuilder(table, filter - ); + return (SelectBuilder.SelectJoin) sqlGeneratorSource.getSqlGenerator(entity.getType()).createSelectBuilder(table, + filter); } } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/CompositeIdAggregateTemplateHsqlIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/CompositeIdAggregateTemplateHsqlIntegrationTests.java index e053bc091f..9354f9423a 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/CompositeIdAggregateTemplateHsqlIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/CompositeIdAggregateTemplateHsqlIntegrationTests.java @@ -18,9 +18,7 @@ import static org.assertj.core.api.Assertions.*; import java.util.List; -import java.util.Optional; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; @@ -244,8 +242,9 @@ void projectByCompositeIdParts() { new EmbeddedPk(23L, "x"), "alpha" // )); - Query projectingQuery = Query.empty().columns( "embeddedPk.two", "name"); - SimpleEntityWithEmbeddedPk projected = template.findOne(projectingQuery, SimpleEntityWithEmbeddedPk.class).orElseThrow(); + Query projectingQuery = Query.empty().columns("embeddedPk.two", "name"); + SimpleEntityWithEmbeddedPk projected = template.findOne(projectingQuery, SimpleEntityWithEmbeddedPk.class) + .orElseThrow(); // Projection still does a full select, otherwise one would be null. // See https://github.com/spring-projects/spring-data-relational/issues/1821 diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextImmutableUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextImmutableUnitTests.java index 13215e22bb..ee7a75eddc 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextImmutableUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextImmutableUnitTests.java @@ -121,7 +121,7 @@ public void idGenerationOfChildInList() { } @Test - // GH-537 + // GH-537 void populatesIdsIfNecessaryForAllRootsThatWereProcessed() { DummyEntity root1 = new DummyEntity().withId(123L); @@ -152,7 +152,7 @@ void populatesIdsIfNecessaryForAllRootsThatWereProcessed() { } DbAction.Insert createInsert(DbAction.WithEntity parent, String propertyName, Object value, - @Nullable Object key) { + @Nullable Object key) { return new DbAction.Insert<>(value, getPersistentPropertyPath(propertyName), parent, key == null ? emptyMap() : singletonMap(toPath(propertyName), key), IdValueSource.GENERATED); @@ -167,7 +167,8 @@ PersistentPropertyPath getPersistentPropertyPath(S } Identifier createBackRef(long value) { - return JdbcIdentifierBuilder.forBackReferences(converter, toAggregatePath("content"), JdbcAggregateChangeExecutionContext.getValueProvider(value, toAggregatePath("content"), converter)).build(); + return JdbcIdentifierBuilder.forBackReferences(converter, toAggregatePath("content"), + JdbcAggregateChangeExecutionContext.getValueProvider(value, toAggregatePath("content"), converter)).build(); } PersistentPropertyPath toPath(String path) { @@ -181,10 +182,8 @@ PersistentPropertyPath toPath(String path) { private static final class DummyEntity { - @Id - private final Long id; - @Version - private final long version; + @Id private final Long id; + @Version private final long version; private final Content content; @@ -222,14 +221,16 @@ public List getList() { } public boolean equals(final Object o) { - if (o == this) return true; + if (o == this) + return true; if (!(o instanceof final DummyEntity other)) return false; final Object this$id = this.getId(); final Object other$id = other.getId(); if (!Objects.equals(this$id, other$id)) return false; - if (this.getVersion() != other.getVersion()) return false; + if (this.getVersion() != other.getVersion()) + return false; final Object this$content = this.getContent(); final Object other$content = other.getContent(); if (!Objects.equals(this$content, other$content)) @@ -254,7 +255,8 @@ public int hashCode() { } public String toString() { - return "JdbcAggregateChangeExecutorContextImmutableUnitTests.DummyEntity(id=" + this.getId() + ", version=" + this.getVersion() + ", content=" + this.getContent() + ", list=" + this.getList() + ")"; + return "JdbcAggregateChangeExecutorContextImmutableUnitTests.DummyEntity(id=" + this.getId() + ", version=" + + this.getVersion() + ", content=" + this.getContent() + ", list=" + this.getList() + ")"; } public DummyEntity withId(Long id) { @@ -275,8 +277,7 @@ public DummyEntity withList(List list) { } private static final class Content { - @Id - private final Long id; + @Id private final Long id; Content() { id = null; @@ -291,7 +292,8 @@ public Long getId() { } public boolean equals(final Object o) { - if (o == this) return true; + if (o == this) + return true; if (!(o instanceof final Content other)) return false; final Object this$id = this.getId(); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextUnitTests.java index 9596111125..e6bf1cb5c5 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextUnitTests.java @@ -258,7 +258,8 @@ PersistentPropertyPath getPersistentPropertyPath(S } Identifier createBackRef(long value) { - return forBackReferences(converter, toAggregatePath("content"), getValueProvider(value, toAggregatePath("content"), converter)).build(); + return forBackReferences(converter, toAggregatePath("content"), + getValueProvider(value, toAggregatePath("content"), converter)).build(); } PersistentPropertyPath toPath(String path) { diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilderUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilderUnitTests.java index e2d3f1cbae..f6a619af12 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilderUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilderUnitTests.java @@ -50,7 +50,9 @@ class WithSimpleId { @Test // DATAJDBC-326 void parametersWithPropertyKeysUseTheParentPropertyJdbcType() { - Identifier identifier = JdbcIdentifierBuilder.forBackReferences(converter, getPath("child"), getValueProvider("eins", getPath("child"), converter)).build(); + Identifier identifier = JdbcIdentifierBuilder + .forBackReferences(converter, getPath("child"), getValueProvider("eins", getPath("child"), converter)) + .build(); assertThat(identifier.getParts()) // .extracting("name", "value", "targetType") // @@ -99,7 +101,8 @@ void qualifiersForLists() { void backreferenceAcrossEmbeddable() { Identifier identifier = JdbcIdentifierBuilder // - .forBackReferences(converter, getPath("embeddable.child"), getValueProvider("parent-eins", getPath("embeddable.child"), converter)) // + .forBackReferences(converter, getPath("embeddable.child"), + getValueProvider("parent-eins", getPath("embeddable.child"), converter)) // .build(); assertThat(identifier.getParts()) // @@ -113,7 +116,8 @@ void backreferenceAcrossEmbeddable() { void backreferenceAcrossNoId() { Identifier identifier = JdbcIdentifierBuilder // - .forBackReferences(converter, getPath("noId.child"), getValueProvider("parent-eins", getPath("noId.child"), converter)) // + .forBackReferences(converter, getPath("noId.child"), + getValueProvider("parent-eins", getPath("noId.child"), converter)) // .build(); assertThat(identifier.getParts()) // @@ -133,7 +137,8 @@ private AggregatePath getPath(String dotPath) { */ static Function getValueProvider(Object idValue, AggregatePath path, JdbcConverter converter) { - RelationalPersistentEntity entity = converter.getMappingContext().getPersistentEntity(path.getIdDefiningParentPath().getRequiredIdProperty().getType()); + RelationalPersistentEntity entity = converter.getMappingContext() + .getPersistentEntity(path.getIdDefiningParentPath().getRequiredIdProperty().getType()); Function valueProvider = ap -> { if (entity == null) { diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorEmbeddedUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorEmbeddedUnitTests.java index 6054e87ceb..4a5973c86e 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorEmbeddedUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorEmbeddedUnitTests.java @@ -295,10 +295,8 @@ void findOneWithReference() { softly.assertThat(sql).startsWith("SELECT") // .contains(" LEFT OUTER JOIN other_entity other ") // .contains(" ON ") // - .contains( - " other.with_embedded_id_and_reference_one = with_embedded_id_and_reference.one ") // - .contains( - " other.with_embedded_id_and_reference_two = with_embedded_id_and_reference.two ") // + .contains(" other.with_embedded_id_and_reference_one = with_embedded_id_and_reference.one ") // + .contains(" other.with_embedded_id_and_reference_two = with_embedded_id_and_reference.two ") // .contains(" WHERE ") // .contains("with_embedded_id_and_reference.one = :one") // .contains("with_embedded_id_and_reference.two = :two"); diff --git a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java index e3b33e4547..1fecd89e8a 100644 --- a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java +++ b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java @@ -377,7 +377,8 @@ private Query getIdQuery(Object id) { idEntity.doWithProperties(new PropertyHandler() { @Override public void doWithPersistentProperty(RelationalPersistentProperty persistentProperty) { - criteriaHolder[0] = criteriaHolder [0].and(persistentProperty.getName()).is(accessor.getProperty(persistentProperty)); + criteriaHolder[0] = criteriaHolder[0].and(persistentProperty.getName()) + .is(accessor.getProperty(persistentProperty)); } }); criteria = criteriaHolder[0]; diff --git a/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/CompositeIdRepositoryIntegrationTests.java b/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/CompositeIdRepositoryIntegrationTests.java index eeedcf1355..9e868577fb 100644 --- a/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/CompositeIdRepositoryIntegrationTests.java +++ b/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/CompositeIdRepositoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2025 the original author or authors. + * Copyright 2025 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. @@ -15,7 +15,10 @@ */ package org.springframework.data.r2dbc.repository; +import static org.assertj.core.api.Assertions.*; + import io.r2dbc.spi.ConnectionFactory; +import reactor.test.StepVerifier; import javax.sql.DataSource; @@ -36,9 +39,6 @@ import org.springframework.data.repository.reactive.ReactiveCrudRepository; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.context.junit.jupiter.SpringExtension; -import reactor.test.StepVerifier; - -import static org.assertj.core.api.Assertions.*; /** * Integration tests for repositories of entities with a composite id. @@ -103,7 +103,7 @@ protected ConnectionFactory createConnectionFactory() { void findAllById() { repository.findById(new CompositeId(42, "HBAR")) // .as(StepVerifier::create) // - .consumeNextWith(actual ->{ + .consumeNextWith(actual -> { assertThat(actual.name).isEqualTo("Walter"); }).verifyComplete(); } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/AggregatePath.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/AggregatePath.java index 6fba2086f8..a6edd906cc 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/AggregatePath.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/AggregatePath.java @@ -33,7 +33,6 @@ import org.springframework.data.relational.core.sql.Column; import org.springframework.data.relational.core.sql.SqlIdentifier; import org.springframework.data.relational.core.sql.Table; -import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -71,7 +70,7 @@ public interface AggregatePath extends Iterable, Comparable stream() { * The path resulting from removing the first element of the {@link AggregatePath}. * * @return {@literal null} for any {@link AggregatePath} having less than two elements. - * @since 3.5.0 + * @since 4.0 */ @Nullable AggregatePath getTail(); @@ -266,6 +265,7 @@ default Stream stream() { * @param basePath the path to be removed. * @return an AggregatePath that ends like the original {@literal AggregatePath} but has {@literal basePath} removed * from the beginning. + * @since 4.0 */ @Nullable AggregatePath subtract(@Nullable AggregatePath basePath); @@ -279,9 +279,10 @@ default Stream stream() { * @param other the {@code AggregatePath} to compare to. Must not be {@literal null}. * @return a negative integer, zero, or a positive integer as this object's path is less than, equal to, or greater * than the specified object's path. + * @since 4.0 */ @Override - default int compareTo(@NonNull AggregatePath other) { + default int compareTo(AggregatePath other) { return toDotPath().compareTo(other.toDotPath()); } @@ -475,7 +476,7 @@ static ColumnInfo of(AggregatePath path) { * ids and references to such ids. * * @author Jens Schauder - * @since 3.5 + * @since 4.0 */ class ColumnInfos { diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/ColumInfosBuilder.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/ColumInfosBuilder.java index 9df469eb7b..2e5c290325 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/ColumInfosBuilder.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/ColumInfosBuilder.java @@ -15,16 +15,16 @@ */ package org.springframework.data.relational.core.mapping; -import org.springframework.data.relational.core.sql.SqlIdentifier; - import java.util.Map; import java.util.TreeMap; +import org.springframework.data.relational.core.sql.SqlIdentifier; + /** * A builder for {@link AggregatePath.ColumnInfos} instances. * * @author Jens Schauder - * @since 3.5 + * @since 4.0 */ class ColumInfosBuilder { @@ -43,8 +43,8 @@ class ColumInfosBuilder { /** * Adds a {@link AggregatePath.ColumnInfo} to the {@link AggregatePath.ColumnInfos} under construction. * - * @param path referencing the {@literal ColumnInfo}. - * @param name of the column. + * @param path referencing the {@literal ColumnInfo}. + * @param name of the column. * @param alias alias for the column. */ void add(AggregatePath path, SqlIdentifier name, SqlIdentifier alias) { @@ -55,8 +55,8 @@ void add(AggregatePath path, SqlIdentifier name, SqlIdentifier alias) { * Adds a {@link AggregatePath.ColumnInfo} to the {@link AggregatePath.ColumnInfos} under construction. * * @param property referencing the {@literal ColumnInfo}. - * @param name of the column. - * @param alias alias for the column. + * @param name of the column. + * @param alias alias for the column. */ void add(RelationalPersistentProperty property, SqlIdentifier name, SqlIdentifier alias) { add(basePath.append(property), name, alias); @@ -65,7 +65,7 @@ void add(RelationalPersistentProperty property, SqlIdentifier name, SqlIdentifie /** * Adds a {@link AggregatePath.ColumnInfo} to the {@link AggregatePath.ColumnInfos} under construction. * - * @param path the path referencing the {@literal ColumnInfo} + * @param path the path referencing the {@literal ColumnInfo} * @param columnInfo the {@literal ColumnInfo} added. */ void add(AggregatePath path, AggregatePath.ColumnInfo columnInfo) { diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DefaultAggregatePath.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DefaultAggregatePath.java index 4246d92ead..dd264dbcca 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DefaultAggregatePath.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DefaultAggregatePath.java @@ -21,7 +21,6 @@ import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.util.Lazy; -import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ConcurrentLruCache; diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntity.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntity.java index 7edf18b4d7..7cc9fdc9ba 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntity.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntity.java @@ -50,7 +50,8 @@ default SqlIdentifier getQualifiedTableName() { * Returns the column representing the identifier. * * @return will never be {@literal null}. - * @deprecated because an entity may have multiple id columns. Use {@code AggregatePath.getTableInfo().getIdColumnInfos()} instead. + * @deprecated because an entity may have multiple id columns. Use + * {@code AggregatePath.getTableInfo().getIdColumnInfos()} instead. */ @Deprecated(forRemoval = true) SqlIdentifier getIdColumn(); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AnalyticFunction.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AnalyticFunction.java index 687591bd5e..fb4edc9a9e 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AnalyticFunction.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AnalyticFunction.java @@ -62,7 +62,7 @@ public AnalyticFunction partitionBy(Expression... partitionBy) { * @param partitionBy Typically, column but other expressions are fine to. * @return a new {@literal AnalyticFunction} is partitioned by the given expressions, overwriting any expression * previously present. - * @since 3.5 + * @since 4.0 */ public AnalyticFunction partitionBy(Collection partitionBy) { return partitionBy(partitionBy.toArray(new Expression[0])); @@ -85,7 +85,7 @@ public AnalyticFunction orderBy(OrderByField... orderBy) { * @param orderBy Typically, column but other expressions are fine to. * @return a new {@literal AnalyticFunction} is ordered by the given expressions, overwriting any expression * previously present. - * @since 3.5 + * @since 4.0 */ public AnalyticFunction orderBy(Collection orderBy) { return orderBy(orderBy.toArray(new Expression[0])); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Expressions.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Expressions.java index cf6c7c76d5..42176e1e55 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Expressions.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Expressions.java @@ -66,14 +66,14 @@ public static Expression cast(Expression expression, String targetType) { /** * Creates an {@link Expression} based on the provided list of {@link Column}s. *

- * If the list contains only a single column, this method returns that column directly - * as the resulting {@link Expression}. Otherwise, it creates and returns a - * {@link TupleExpression} that represents multiple columns as a single expression. + * If the list contains only a single column, this method returns that column directly as the resulting + * {@link Expression}. Otherwise, it creates and returns a {@link TupleExpression} that represents multiple columns as + * a single expression. * - * @param columns the list of {@link Column}s to include in the expression; - * must not be {@literal null}. - * @return an {@link Expression} corresponding to the input columns: either a single column or a {@link TupleExpression} for multiple columns. - * @since 3.5 + * @param columns the list of {@link Column}s to include in the expression; must not be {@literal null}. + * @return an {@link Expression} corresponding to the input columns: either a single column or a + * {@link TupleExpression} for multiple columns. + * @since 4.0 */ public static Expression of(List columns) { @@ -84,8 +84,7 @@ public static Expression of(List columns) { } // Utility constructor. - private Expressions() { - } + private Expressions() {} static public class SimpleExpression extends AbstractSegment implements Expression { diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/TupleExpression.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/TupleExpression.java index aa0e89bebc..82ac2fe7f8 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/TupleExpression.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/TupleExpression.java @@ -28,7 +28,7 @@ * * * @author Jens Schauder - * @since 3.5 + * @since 4.0 */ public class TupleExpression extends AbstractSegment implements Expression { diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/TupleVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/TupleVisitor.java index fef8d8f688..d03fce9d3f 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/TupleVisitor.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/TupleVisitor.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2024 the original author or authors. + * Copyright 2025 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. @@ -22,7 +22,7 @@ * Visitor for rendering tuple expressions. * * @author Jens Schauder - * @since 3.5 + * @since 4.0 */ class TupleVisitor extends TypedSingleConditionRenderSupport implements PartRenderer { diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/AggregatePathAssertions.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/AggregatePathAssertions.java index aec43445ca..33a195d5b6 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/AggregatePathAssertions.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/AggregatePathAssertions.java @@ -17,9 +17,19 @@ import org.assertj.core.api.AbstractAssert; +/** + * Custom AssertJ assertions for {@link AggregatePath} instances + * + * @author Jens Schauder + * @since 4.0 + */ public class AggregatePathAssertions extends AbstractAssert { - // Constructor for initializing with an AggregatePath instance + /** + * Constructor taking the actual {@link AggregatePath} to assert over. + * + * @param actual + */ public AggregatePathAssertions(AggregatePath actual) { super(actual, AggregatePathAssertions.class); } @@ -32,7 +42,8 @@ public static AggregatePathAssertions assertThat(AggregatePath actual) { } /** - * Example custom assertion method: Asserts that the AggregatePath has a specific property. + * Assertion method comparing the path of the actual AggregatePath with the provided String representation of a path + * in dot notation. Note that the assertion does not test the root entity type of the AggregatePath. */ public AggregatePathAssertions hasPath(String expectedPath) { isNotNull(); @@ -43,6 +54,9 @@ public AggregatePathAssertions hasPath(String expectedPath) { return this; } + /** + * assertion testing if the actual path is a root path. + */ public AggregatePathAssertions isRoot() { isNotNull(); @@ -52,6 +66,9 @@ public AggregatePathAssertions isRoot() { return this; } + /** + * assertion testing if the actual path is NOT a root path. + */ public AggregatePathAssertions isNotRoot() { isNotNull(); @@ -60,17 +77,4 @@ public AggregatePathAssertions isNotRoot() { } return this; } - - /** - * Example custom assertion method: Validates the depth of the path. - */ - public AggregatePathAssertions hasLength(int expectedLength) { - isNotNull(); - - if (actual.getLength() != expectedLength) { - failWithMessage("Expected path length to be <%d> but was <%d>", expectedLength, actual.getLength()); - } - return this; - } - -} \ No newline at end of file +} diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/AggregatePathSoftAssertions.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/AggregatePathSoftAssertions.java index 1734552905..3b59af40ba 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/AggregatePathSoftAssertions.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/AggregatePathSoftAssertions.java @@ -15,12 +15,22 @@ */ package org.springframework.data.relational.core.mapping; +import java.util.function.Consumer; + import org.assertj.core.api.SoftAssertions; import org.assertj.core.api.SoftAssertionsProvider; -import java.util.function.Consumer; - +/** + * Soft assertions for {@link AggregatePath} instances. + * + * @author Jens Schauder + * @since 4.0 + */ public class AggregatePathSoftAssertions extends SoftAssertions { + + /** + * Entry point for assertions. The default {@literal assertThat} can't be used, since it collides with {@link SoftAssertions#assertThat(Iterable)} + */ public AggregatePathAssertions assertAggregatePath(AggregatePath actual) { return proxy(AggregatePathAssertions.class, AggregatePath.class, actual); } diff --git a/src/main/antora/modules/ROOT/pages/jdbc/mapping.adoc b/src/main/antora/modules/ROOT/pages/jdbc/mapping.adoc index c41b6cd42b..02d4b12cfd 100644 --- a/src/main/antora/modules/ROOT/pages/jdbc/mapping.adoc +++ b/src/main/antora/modules/ROOT/pages/jdbc/mapping.adoc @@ -65,7 +65,8 @@ The table of the referenced entity is expected to have an additional column with * `Map` is considered a qualified one-to-many relationship. The table of the referenced entity is expected to have two additional columns: One named based on the referencing entity for the foreign key (see <>) and one with the same name and an additional `_key` suffix for the map key. -* `List` is mapped as a `Map`. The same additional columns are expected and the names used can be customized in the same way. +* `List` is mapped as a `Map`. +The same additional columns are expected and the names used can be customized in the same way. + For `List`, `Set`, and `Map` naming of the back reference can be controlled by implementing `NamingStrategy.getReverseColumnName(RelationalPersistentEntity owner)` and `NamingStrategy.getKeyColumn(RelationalPersistentProperty property)`, respectively. Alternatively you may annotate the attribute with `@MappedCollection(idColumn="your_column_name", keyColumn="your_key_column_name")`. diff --git a/src/main/antora/modules/ROOT/partials/mapping-annotations.adoc b/src/main/antora/modules/ROOT/partials/mapping-annotations.adoc index 4f5c4bac54..0fb12a5706 100644 --- a/src/main/antora/modules/ROOT/partials/mapping-annotations.adoc +++ b/src/main/antora/modules/ROOT/partials/mapping-annotations.adoc @@ -1,11 +1,15 @@ The `RelationalConverter` can use metadata to drive the mapping of objects to rows. The following annotations are available: -* `@Embedded`: an entity with this annotation will be mapped to the table of the parent entity, instead of a separate table. Allows to specify if the resulting columns should have a common prefix. +* `@Embedded`: an entity with this annotation will be mapped to the table of the parent entity, instead of a separate table. +Allows to specify if the resulting columns should have a common prefix. If all columns resulting from such an entity are `null` either the annotated entity will be `null` or _empty_, i.e. all of its properties will be `null`, depending on the value of `@Embedded.onEmpty()` May be combined with `@Id` to form a composite id. -* `@Id`: Applied at the field level to mark the primary key. It may be combined with `@Embedded` to form a composite id. -* `@InsertOnlyProperty`: Marks a property as only to be written during insert. Such a property on an aggregate root will only be written once and never updated. Note that on a nested entity, all save operations result in an insert therefore this annotation has no effect on properties of nested entities. +* `@Id`: Applied at the field level to mark the primary key. +It may be combined with `@Embedded` to form a composite id. +* `@InsertOnlyProperty`: Marks a property as only to be written during insert. +Such a property on an aggregate root will only be written once and never updated. +Note that on a nested entity, all save operations result in an insert therefore this annotation has no effect on properties of nested entities. * `@MappedCollection`: Allows for configuration how a collection, or a single nested entity gets mapped. `idColumn` specifies the column used for referencing the parent entities primary key. `keyColumn` specifies the column used to store the index of a `List` or the key of a `Map`. * `@Sequence`: specify a database sequence for generating values for the annotated property. * `@Table`: Applied at the class level to indicate this class is a candidate for mapping to the database. diff --git a/src/main/antora/modules/ROOT/partials/mapping.adoc b/src/main/antora/modules/ROOT/partials/mapping.adoc index 8295714e4c..16e0c5b833 100644 --- a/src/main/antora/modules/ROOT/partials/mapping.adoc +++ b/src/main/antora/modules/ROOT/partials/mapping.adoc @@ -88,7 +88,6 @@ endif::[] You may use xref:value-expressions.adoc[Spring Data's SpEL support] to dynamically create column names. Once generated the names will be cached, so it is dynamic per mapping context only. - ifdef::embedded-entities[] [[entity-persistence.embedded-entities]] @@ -185,6 +184,7 @@ CREATE TABLE PERSON_WITH_COMPOSITE_ID ( ---- + <1> Entities may be represented as records without any special consideration <2> `pk` is marked as id and embedded <3> the two columns from the embedded `Name` entity make up the primary key in the database.