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 ebd3103251..3813e1695c 100644 --- a/pom.xml +++ b/pom.xml @@ -1,11 +1,14 @@ - + 4.0.0 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 @@ -53,7 +56,6 @@ 1.3.0 1.37 - 0.4.0.BUILD-SNAPSHOT 2017 @@ -165,87 +167,12 @@ jmh - - com.github.mp911de.microbenchmark-runner - microbenchmark-runner-junit5 - ${mbr.version} - test - - - org.openjdk.jmh - jmh-core - ${jmh.version} - test - org.openjdk.jmh jmh-generator-annprocess - ${jmh.version} test - - - - org.codehaus.mojo - build-helper-maven-plugin - 3.3.0 - - - add-source - generate-sources - - add-test-source - - - - src/jmh/java - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - true - - - - - org.apache.maven.plugins - maven-failsafe-plugin - - true - - - - org.codehaus.mojo - exec-maven-plugin - 3.1.0 - - - run-benchmarks - pre-integration-test - - exec - - - test - java - - -classpath - - org.openjdk.jmh.Main - .* - - - - - - - jitpack.io 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-jdbc/src/jmh/java/org/springframework/data/jdbc/BenchmarkSettings.java b/spring-data-jdbc/src/jmh/java/org/springframework/data/jdbc/BenchmarkSettings.java new file mode 100644 index 0000000000..2e056565d4 --- /dev/null +++ b/spring-data-jdbc/src/jmh/java/org/springframework/data/jdbc/BenchmarkSettings.java @@ -0,0 +1,40 @@ +/* + * Copyright 2023-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; + +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Warmup; + +/** + * Global benchmark settings. + * + * @author Mark Paluch + */ +@Warmup(iterations = 5, time = 1000, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 5, time = 1000, timeUnit = TimeUnit.MILLISECONDS) +@Fork(value = 1, warmups = 0) +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.SECONDS) +public abstract class BenchmarkSettings { + +} diff --git a/spring-data-jdbc/src/jmh/java/org/springframework/data/jdbc/CompositeIdBenchmarks.java b/spring-data-jdbc/src/jmh/java/org/springframework/data/jdbc/CompositeIdBenchmarks.java new file mode 100644 index 0000000000..f84792b20f --- /dev/null +++ b/spring-data-jdbc/src/jmh/java/org/springframework/data/jdbc/CompositeIdBenchmarks.java @@ -0,0 +1,245 @@ +/* + * 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.jdbc; + +import static org.assertj.core.api.Assertions.*; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; + +import javax.sql.DataSource; + +import org.junit.platform.commons.annotation.Testable; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.annotation.Id; +import org.springframework.data.jdbc.core.JdbcAggregateTemplate; +import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration; +import org.springframework.data.relational.core.mapping.Embedded; +import org.springframework.data.relational.core.mapping.Table; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.datasource.ConnectionHolder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * Benchmarks for Composite Ids in Spring Data JDBC. + * + * @author Mark Paluch + */ +@Testable +public class CompositeIdBenchmarks extends BenchmarkSettings { + + @Configuration + static class BenchmarkConfiguration extends AbstractJdbcConfiguration { + + @Bean + NamedParameterJdbcTemplate namedParameterJdbcTemplate(DataSource dataSource) { + return new NamedParameterJdbcTemplate(dataSource); + } + + @Bean + DataSource dataSource() { + + return new EmbeddedDatabaseBuilder() // + .generateUniqueName(true) // + .setType(EmbeddedDatabaseType.HSQL) // + .setScriptEncoding("UTF-8") // + .ignoreFailedDrops(true) // + .addScript( + "classpath:/org.springframework.data.jdbc.core/CompositeIdAggregateTemplateHsqlIntegrationTests-hsql.sql") // + .build(); + } + } + + @State(Scope.Benchmark) + public static class BenchmarkState { + + AnnotationConfigApplicationContext context; + JdbcAggregateTemplate template; + NamedParameterJdbcTemplate named; + AtomicLong l = new AtomicLong(); + SimpleEntity alpha; + + @Setup + public void setup() throws SQLException { + + context = new AnnotationConfigApplicationContext(); + context.register(BenchmarkConfiguration.class); + context.refresh(); + context.start(); + + template = context.getBean(JdbcAggregateTemplate.class); + named = context.getBean(NamedParameterJdbcTemplate.class); + DataSource dataSource = context.getBean(DataSource.class); + + Connection connection = dataSource.getConnection(); + ConnectionHolder holder = new ConnectionHolder(connection, true); + holder.setSynchronizedWithTransaction(true); + TransactionSynchronizationManager.bindResource(dataSource, holder); + + alpha = template.insert(new SimpleEntity(new WrappedPk(l.incrementAndGet()), "alpha")); + } + + @TearDown + public void cleanup() { + context.close(); + } + } + + @Benchmark + public Object namedTemplate(BenchmarkState state) { + return state.named.query("SELECT * FROM SIMPLE_ENTITY WHERE id = :id", Map.of("id", state.alpha.wrappedPk.id), + (rs, rowNum) -> 1); + } + + @Benchmark + public Object jdbcTemplate(BenchmarkState state) { + return state.named.getJdbcOperations().query("SELECT * FROM SIMPLE_ENTITY WHERE id = " + state.alpha.wrappedPk.id, + (rs, rowNum) -> 1); + } + + @Benchmark + public Object baselineInsert(BenchmarkState state) { + return state.template.insert(new BaselineEntity(state.l.incrementAndGet(), "alpha")); + } + + @Benchmark + public Object loadBaselineEntity(BenchmarkState state) { + return state.template.findById(state.alpha.wrappedPk, BaselineEntity.class); + } + + @Benchmark + public Object insert(BenchmarkState state) { + return state.template.insert(new SimpleEntity(new WrappedPk(state.l.incrementAndGet()), "alpha")); + } + + @Benchmark + public Object loadSimpleEntity(BenchmarkState state) { + return state.template.findById(state.alpha.wrappedPk, SimpleEntity.class); + } + + @Benchmark + public Object saveAndLoadEntityWithList(BenchmarkState state) { + + WithList entity = state.template.insert(new WithList(new WrappedPk(state.l.incrementAndGet()), "alpha", + List.of(new Child("Romulus"), new Child("Remus")))); + + assertThat(entity.wrappedPk).isNotNull() // + .extracting(WrappedPk::id).isNotNull(); + + return state.template.findById(entity.wrappedPk, WithList.class); + } + + @Benchmark + public Object saveAndLoadSimpleEntityWithEmbeddedPk(BenchmarkState state) { + + SimpleEntityWithEmbeddedPk entity = state.template + .insert(new SimpleEntityWithEmbeddedPk(new EmbeddedPk(state.l.incrementAndGet(), "x"), "alpha")); + + return state.template.findById(entity.embeddedPk, SimpleEntityWithEmbeddedPk.class); + } + + @Benchmark + public void deleteSingleSimpleEntityWithEmbeddedPk(BenchmarkState state) { + + List entities = (List) state.template + .insertAll(List.of(new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "x"), "alpha"))); + + state.template.delete(entities.get(0)); + } + + @Benchmark + public void deleteMultipleSimpleEntityWithEmbeddedPk(BenchmarkState state) { + + List entities = (List) state.template + .insertAll(List.of(new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "x"), "alpha"), + new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "y"), "beta"))); + + state.template.deleteAll(List.of(entities.get(1), entities.get(0))); + } + + @Benchmark + public void updateSingleSimpleEntityWithEmbeddedPk(BenchmarkState state) { + + List entities = (List) state.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"); + state.template.save(updated); + + state.template.deleteAll(SimpleEntityWithEmbeddedPk.class); + } + + private record WrappedPk(Long id) { + } + + @Table("SIMPLE_ENTITY") + private record BaselineEntity( // + @Id Long id, // + String name // + ) { + } + + 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) { + } + +} 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..edb2faee97 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 @@ -15,8 +15,18 @@ */ package org.springframework.data.jdbc.core; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.function.BiConsumer; +import java.util.function.Function; import java.util.stream.Collectors; import org.springframework.dao.IncorrectUpdateSemanticsDataAccessException; @@ -175,8 +185,9 @@ private Identifier getParentKeys(DbAction.WithDependingOn action, JdbcConvert Object id = getParentId(action); + AggregatePath aggregatePath = context.getAggregatePath(action.getPropertyPath()); JdbcIdentifierBuilder identifier = JdbcIdentifierBuilder // - .forBackReferences(converter, context.getAggregatePath(action.getPropertyPath()), id); + .forBackReferences(converter, aggregatePath, getIdMapper(id, aggregatePath, converter)); for (Map.Entry, Object> qualifier : action.getQualifiers() .entrySet()) { @@ -186,6 +197,20 @@ private Identifier getParentKeys(DbAction.WithDependingOn action, JdbcConvert return identifier.build(); } + static Function getIdMapper(Object idValue, AggregatePath path, JdbcConverter converter) { + + RelationalPersistentProperty idProperty = path.getIdDefiningParentPath().getRequiredIdProperty(); + RelationalPersistentEntity entity = converter.getMappingContext() + .getPersistentEntity(idProperty); + + if (entity == null) { + return aggregatePath -> idValue; + } + + PersistentPropertyPathAccessor propertyPathAccessor = entity.getPropertyPathAccessor(idValue); + return aggregatePath -> propertyPathAccessor.getProperty(aggregatePath.getSubPathBasedOn(idProperty.getActualType()).getRequiredPersistentPropertyPath()); + } + private Object getParentId(DbAction.WithDependingOn action) { DbAction.WithEntity idOwningAction = getIdOwningAction(action, @@ -267,12 +292,10 @@ 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); } } } @@ -359,8 +382,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<>(); diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BindParameterNameSanitizer.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BindParameterNameSanitizer.java index 64213bd939..09dbcefe4a 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BindParameterNameSanitizer.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BindParameterNameSanitizer.java @@ -29,6 +29,14 @@ abstract class BindParameterNameSanitizer { private static final Pattern parameterPattern = Pattern.compile("\\W"); static String sanitize(String rawName) { - return parameterPattern.matcher(rawName).replaceAll(""); + + for (int i = 0; i < rawName.length(); i++) { + char c = rawName.charAt(i); + if ((c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c < '0' || c > '9') && c != '_') { + return parameterPattern.matcher(rawName).replaceAll(""); + } + } + + return rawName; } } 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 ff0b67f44e..f0febcad79 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 @@ -121,6 +121,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(), // @@ -171,7 +172,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); } @@ -192,7 +193,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); @@ -212,8 +213,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); } @@ -273,7 +273,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, getRowMapper(domainType)); @@ -359,7 +359,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/Identifier.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/Identifier.java index 5f9284a54b..711ba330c8 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 4.0 + */ + 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. 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/JdbcColumnTypes.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcColumnTypes.java index fa74b3b94f..0c678ef975 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcColumnTypes.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcColumnTypes.java @@ -24,21 +24,28 @@ import java.util.Map; import org.springframework.util.ClassUtils; +import org.springframework.util.ConcurrentLruCache; /** * Utility that determines the necessary type conversions between Java types used in the domain model and types * compatible with JDBC drivers. * * @author Jens Schauder + * @author Mark Paluch * @since 2.0 */ public enum JdbcColumnTypes { INSTANCE { + private final ConcurrentLruCache, Class> cache = new ConcurrentLruCache<>(64, this::doResolve); + @SuppressWarnings({ "unchecked", "rawtypes" }) public Class resolvePrimitiveType(Class type) { + return cache.get(type); + } + private Class doResolve(Class type) { return javaToDbType.entrySet().stream() // .filter(e -> e.getKey().isAssignableFrom(type)) // .map(e -> (Class) e.getValue()) // 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..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 @@ -15,7 +15,10 @@ */ package org.springframework.data.jdbc.core.convert; +import java.util.function.Function; + import org.springframework.data.relational.core.mapping.AggregatePath; +import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.util.Assert; /** @@ -39,15 +42,42 @@ public static JdbcIdentifierBuilder empty() { /** * Creates ParentKeys with backreference for the given path and value of the parents id. */ - public static JdbcIdentifierBuilder forBackReferences(JdbcConverter converter, AggregatePath path, Object value) { + 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}. + * @return Guaranteed not to be {@literal null}. + */ + public static Identifier forBackReference(JdbcConverter converter, AggregatePath path, Identifier defaultIdentifier, + Function valueProvider) { + + Identifier identifierToUse = defaultIdentifier; + + AggregatePath idDefiningParentPath = path.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()) { + + AggregatePath.ColumnInfos infos = path.getTableInfo().backReferenceColumnInfos(); + identifierToUse = infos.reduce(Identifier.empty(), (ap, ci) -> { - Identifier identifier = Identifier.of( // - path.getTableInfo().reverseColumnInfo().name(), // - value, // - converter.getColumnType(path.getIdDefiningParentPath().getRequiredIdProperty()) // - ); + RelationalPersistentProperty property = ap.getRequiredLeafProperty(); + return Identifier.of(ci.name(), valueProvider.apply(ap), converter.getColumnType(property)); + }, Identifier::withPart); + } - return new JdbcIdentifierBuilder(identifier); + return identifierToUse; } /** @@ -62,8 +92,8 @@ 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..2c3feffdb6 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,10 @@ public T getPropertyValue(RelationalPersistentProperty property) { if (property.isCollectionLike() || property.isMap()) { - Identifier identifierToUse = this.identifier; - AggregatePath idDefiningParentPath = aggregatePath.getIdDefiningParentPath(); + Identifier identifier = JdbcIdentifierBuilder.forBackReference(MappingJdbcConverter.this, aggregatePath, + this.identifier, getWrappedValueProvider(delegate::getValue, 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()) { @@ -423,7 +409,7 @@ public boolean hasValue(RelationalPersistentProperty property) { return delegate.hasValue(toUse); } - return delegate.hasValue(aggregatePath.getTableInfo().reverseColumnInfo().alias()); + return delegate.hasValue(aggregatePath.getTableInfo().backReferenceColumnInfos().any().alias()); } return delegate.hasValue(aggregatePath); @@ -449,7 +435,7 @@ public boolean hasNonEmptyValue(RelationalPersistentProperty property) { return delegate.hasValue(toUse); } - return delegate.hasValue(aggregatePath.getTableInfo().reverseColumnInfo().alias()); + return delegate.hasValue(aggregatePath.getTableInfo().backReferenceColumnInfos().any().alias()); } return delegate.hasNonEmptyValue(aggregatePath); @@ -460,7 +446,35 @@ 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( + "AggregatePath %s does not define an identifier property".formatted(idDefiningParentPath)); + }; + } + + RelationalPersistentProperty idProperty = idDefiningParentPath.getRequiredIdProperty(); + AggregatePath idPath = idProperty.isEntity() ? idDefiningParentPath.append(idProperty) : idDefiningParentPath; + + return ap -> valueProvider.apply(smartAppend(idPath, ap)); + } + + private static AggregatePath smartAppend(AggregatePath base, AggregatePath suffix) { + + RelationalPersistentEntity owner = suffix.getRequiredBaseProperty().getOwner(); + if (owner.equals(base.getRequiredLeafEntity())) { + return base.append(suffix); + } else { + return smartAppend(base, suffix.getTail()); } } @@ -472,7 +486,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/RowDocumentExtractorSupport.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/RowDocumentExtractorSupport.java index 2d27a453ac..5509157847 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/RowDocumentExtractorSupport.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/RowDocumentExtractorSupport.java @@ -26,6 +26,7 @@ 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.RelationalPredicates; import org.springframework.data.relational.core.sql.SqlIdentifier; import org.springframework.data.relational.domain.RowDocument; import org.springframework.lang.Nullable; @@ -235,7 +236,7 @@ private void readEntity(RS row, RowDocument document, AggregatePath basePath, AggregatePath path = basePath.append(property); - if (property.isEntity() && !property.isEmbedded() && (property.isCollectionLike() || property.isQualified())) { + if (RelationalPredicates.isRelation(property) && (property.isCollectionLike() || property.isQualified())) { readerState.put(property, new ContainerSink<>(aggregateContext, property, path)); continue; 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..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 @@ -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,21 @@ 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. + * @since 4.0 + */ + Column getAnyReverseColumn(AggregatePath path) { + + 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 7ac637e8c3..09a62f0667 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 @@ -16,7 +16,9 @@ package org.springframework.data.jdbc.core.convert; import java.util.*; +import java.util.function.BiFunction; import java.util.function.Function; +import java.util.function.Predicate; import java.util.stream.Collectors; import org.springframework.data.domain.Pageable; @@ -33,7 +35,6 @@ 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.jdbc.core.namedparam.MapSqlParameterSource; @@ -59,12 +60,10 @@ * @author Viktor Ardelean * @author Kurt Niemi */ -class SqlGenerator { +public 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; @@ -110,13 +108,25 @@ 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; } + /** + * 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()); + } + /** * When deleting entities there is a fundamental difference between deleting *
    @@ -156,44 +166,55 @@ 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<>(); + + // TODO: cannot we simply pass on the columnInfos? + 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().toColumnList(subSelectTable); + 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 Expressions.of(new ArrayList<>(columnsMap.values())); } private BindMarker getBindMarker(SqlIdentifier columnName) { @@ -439,7 +460,7 @@ String createDeleteAllSql(@Nullable PersistentPropertyPath path) { - return createDeleteByPathAndCriteria(mappingContext.getAggregatePath(path), - filterColumn -> filterColumn.isEqualTo(getBindMarker(ROOT_ID_PARAMETER))); + // TODO: When deleting by path, why do we expect the where-value to be id and not named after the path? + // See SqlGeneratorEmbeddedUnitTests.deleteByPath + return createDeleteByPathAndCriteria(mappingContext.getAggregatePath(path), this::equalityCondition); } /** @@ -462,17 +484,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), this::inCondition); + } + + /** + * Constructs a where condition. The where condition will be of the form {@literal IN :bind-marker} + */ + private Condition inCondition(Map columnMap) { + + Collection columns = columnMap.values(); + + return Conditions.in(columns.size() == 1 ? columns.iterator().next() : TupleExpression.create(columns), + getBindMarker(IDS_SQL_PARAMETER)); + } + + /** + * Constructs a where-condition. The where condition will be of the form + * {@literal = :bind-marker-a AND = :bind-marker-b ...} + */ + private Condition equalityCondition(Map columnMap) { + + Assert.isTrue(!columnMap.isEmpty(), "Column map must not be empty"); + + AggregatePath.ColumnInfos idColumnInfos = mappingContext.getAggregatePath(entity).getTableInfo().idColumnInfos(); + + return createPredicate(columnMap, (aggregatePath, column) -> { + return column.isEqualTo(getBindMarker(idColumnInfos.get(aggregatePath).name())); + }); + } - return createDeleteByPathAndCriteria(mappingContext.getAggregatePath(path), - filterColumn -> filterColumn.in(getBindMarker(IDS_SQL_PARAMETER))); + /** + * 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 Condition isNotNullCondition(Map columnMap) { + return createPredicate(columnMap, (aggregatePath, column) -> column.isNotNull()); + } + + /** + * 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 static Condition createPredicate(Map columnMap, + BiFunction conditionFunction) { + + Condition result = null; + for (Map.Entry entry : columnMap.entrySet()) { + + Condition singleCondition = conditionFunction.apply(entry.getKey(), entry.getValue()); + result = result == null ? singleCondition : result.and(singleCondition); + } + Assert.state(result != null, "We need at least one condition"); + return result; } private String createFindOneSql() { + return render(selectBuilder().where(equalityIdWhereCondition()).build()); + } - Select select = selectBuilder().where(getIdColumn().isEqualTo(getBindMarker(ID_SQL_PARAMETER))) // - .build(); + private Condition equalityIdWhereCondition() { + return equalityIdWhereCondition(getIdColumns()); + } - return render(select); + private Condition equalityIdWhereCondition(Iterable columns) { + + Assert.isTrue(columns.iterator().hasNext(), "Identifier columns must not be empty"); + + Condition aggregate = null; + for (Column column : columns) { + + 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 +567,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 +581,7 @@ private String createAcquireLockAll(LockMode lockMode) { Table table = this.getTable(); Select select = StatementBuilder // - .select(getIdColumn()) // + .select(getSingleNonNullColumn()) // .from(table) // .lock(lockMode) // .build(); @@ -512,7 +599,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<>(); @@ -520,15 +611,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); } @@ -538,14 +633,18 @@ 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; + baseSelect = baseSelect.leftOuterJoin(join.joinTable).on(join.condition); + } + return baseSelect; } private SelectBuilder.SelectOrdered selectBuilder(Collection keyColumns, Sort sort, @@ -605,7 +704,7 @@ Column getColumn(AggregatePath path) { return null; } - return sqlContext.getReverseColumn(path); + return sqlContext.getAnyReverseColumn(path); } return sqlContext.getColumn(path); @@ -619,32 +718,44 @@ Join getJoin(AggregatePath path) { } Table currentTable = sqlContext.getTable(path); + AggregatePath.ColumnInfos backRefColumnInfos = path.getTableInfo().backReferenceColumnInfos(); AggregatePath idDefiningParentPath = path.getIdDefiningParentPath(); Table parentTable = sqlContext.getTable(idDefiningParentPath); + AggregatePath.ColumnInfos idColumnInfos = idDefiningParentPath.getTableInfo().idColumnInfos(); - return new Join( // - currentTable, // - currentTable.column(path.getTableInfo().reverseColumnInfo().name()), // - parentTable.column(idDefiningParentPath.getTableInfo().idColumnName()) // - ); + Condition joinCondition = backRefColumnInfos.reduce(Conditions.unrestricted(), (aggregatePath, columnInfo) -> { + + return currentTable.column(columnInfo.name()) + .isEqualTo(parentTable.column(idColumnInfos.get(aggregatePath).name())); + }, Condition::and); + + return new Join(currentTable, joinCondition); } 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 +826,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 +849,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 +867,20 @@ private String createDeleteByPathAndCriteria(AggregatePath path, Function columns = new TreeMap<>(); + AggregatePath.ColumnInfos columnInfos = path.getTableInfo().backReferenceColumnInfos(); + + // TODO: cannot we simply pass on the columnInfos? + 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 +893,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 +919,26 @@ 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() { + + // getColumn() is slightly different from the code in any(…). Why? + // AggregatePath.ColumnInfo columnInfo = path.getColumnInfo(); + // return getTable(path).column(columnInfo.name()).as(columnInfo.alias()); + + AggregatePath.ColumnInfos columnInfos = mappingContext.getAggregatePath(entity).getTableInfo().idColumnInfos(); + return columnInfos.any((ap, ci) -> sqlContext.getColumn(ap)); + } + + private List getIdColumns() { + + AggregatePath.ColumnInfos columnInfos = mappingContext.getAggregatePath(entity).getTableInfo().idColumnInfos(); + + // sqlcontext.getColumn (vs sqlContext.getTable + return columnInfos + .toColumnList((aggregatePath, columnInfo) -> sqlContext.getColumn(aggregatePath)); } private Column getVersionColumn() { @@ -961,7 +1095,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 +1105,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 +1131,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 +1141,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 +1185,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, 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..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 @@ -56,7 +56,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/SqlIdentifierParameterSource.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlIdentifierParameterSource.java index 78ff82deb2..209aa7108d 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlIdentifierParameterSource.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlIdentifierParameterSource.java @@ -35,8 +35,8 @@ */ class SqlIdentifierParameterSource extends AbstractSqlParameterSource { - private final Set identifiers = new HashSet<>(); - private final Map namesToValues = new HashMap<>(); + private final Set identifiers = new HashSet<>(16, 1f); + private final Map namesToValues = new HashMap<>(16, 1f); @Override public boolean hasValue(String paramName) { @@ -73,7 +73,7 @@ void addAll(SqlIdentifierParameterSource others) { for (SqlIdentifier identifier : others.getIdentifiers()) { - String name = BindParameterNameSanitizer.sanitize( identifier.getReference()); + String name = BindParameterNameSanitizer.sanitize(identifier.getReference()); addValue(identifier, others.getValue(name), others.getSqlType(name)); } } 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..5e893d26fa 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 @@ -17,22 +17,25 @@ import java.sql.SQLType; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.function.BiFunction; import java.util.function.Predicate; import org.springframework.data.jdbc.core.mapping.JdbcValue; 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; +import org.springframework.data.relational.core.mapping.RelationalPredicates; import org.springframework.data.relational.core.sql.SqlIdentifier; -import org.springframework.jdbc.support.JdbcUtils; import org.springframework.lang.Nullable; -import org.springframework.util.Assert; /** * Creates the {@link SqlIdentifierParameterSource} for various SQL operations, dialect identifier processing rules and @@ -41,9 +44,11 @@ * @author Jens Schauder * @author Chirag Tailor * @author Mikhail Polivakha + * @author Mark Paluch * @since 2.4 */ public class SqlParametersFactory { + private final RelationalMappingContext context; private final JdbcConverter converter; @@ -78,9 +83,17 @@ 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(); + + // fullPath: because we use the result with a PropertyPathAccessor + columnInfos.forEachLong((ap, __) -> { + Object idValue = propertyPathAccessor.getProperty(ap.getRequiredPersistentPropertyPath()); + RelationalPersistentProperty idProperty = ap.getRequiredLeafProperty(); + addConvertedPropertyValue(parameterSource, idProperty, idValue, idProperty.getColumnName()); + }); + } return parameterSource; } @@ -104,21 +117,25 @@ 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(); + return doWithIdentifiers(domainType, (columns, idProperty, complexId) -> { - addConvertedPropertyValue( // - parameterSource, // - getRequiredPersistentEntity(domainType).getRequiredIdProperty(), // - id, // - name // - ); - return parameterSource; + SqlIdentifierParameterSource parameterSource = new SqlIdentifierParameterSource(); + BiFunction valueExtractor = getIdMapper(complexId); + + columns.forEach((ap, ci) -> addConvertedPropertyValue( // + parameterSource, // + ap.getRequiredLeafProperty(), // + valueExtractor.apply(id, ap), // + ci.name() // + )); + + return parameterSource; + }); } /** @@ -131,12 +148,44 @@ SqlIdentifierParameterSource forQueryById(Object id, Class domainType, Sq */ SqlIdentifierParameterSource forQueryByIds(Iterable ids, Class domainType) { - SqlIdentifierParameterSource parameterSource = new SqlIdentifierParameterSource(); + return doWithIdentifiers(domainType, (columns, idProperty, complexId) -> { - addConvertedPropertyValuesAsList(parameterSource, getRequiredPersistentEntity(domainType).getRequiredIdProperty(), - ids); + SqlIdentifierParameterSource parameterSource = new SqlIdentifierParameterSource(); - return parameterSource; + BiFunction valueExtractor = getIdMapper(complexId); + + List parameterValues = new ArrayList<>(ids instanceof Collection c ? c.size() : 16); + for (Object id : ids) { + + Object[] tupleList = new Object[columns.size()]; + + int i = 0; + for (AggregatePath path : columns.paths()) { + tupleList[i++] = valueExtractor.apply(id, path); + } + + parameterValues.add(tupleList); + } + + parameterSource.addValue(SqlGenerator.IDS_SQL_PARAMETER, parameterValues); + return parameterSource; + }); + } + + private T doWithIdentifiers(Class domainType, IdentifierCallback callback) { + + RelationalPersistentEntity entity = context.getRequiredPersistentEntity(domainType); + RelationalPersistentProperty idProperty = entity.getRequiredIdProperty(); + RelationalPersistentEntity complexId = context.getPersistentEntity(idProperty); + AggregatePath.ColumnInfos columns = context.getAggregatePath(entity).getTableInfo().idColumnInfos(); + + return callback.doWithIdentifiers(columns, idProperty, complexId); + } + + interface IdentifierCallback { + + T doWithIdentifiers(AggregatePath.ColumnInfos columns, RelationalPersistentProperty idProperty, + RelationalPersistentEntity complexId); } /** @@ -156,19 +205,17 @@ 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 BiFunction getIdMapper(@Nullable RelationalPersistentEntity complexId) { + + if (complexId == null) { + return (id, aggregatePath) -> id; } + + return (id, aggregatePath) -> { + + PersistentPropertyAccessor accessor = complexId.getPropertyAccessor(id); + return accessor.getProperty(aggregatePath.getRequiredLeafProperty()); + }; } private void addConvertedPropertyValue(SqlIdentifierParameterSource parameterSource, @@ -199,28 +246,6 @@ private void addConvertedValue(SqlIdentifierParameterSource parameterSource, @Nu jdbcValue.getJdbcType().getVendorTypeNumber()); } - private void addConvertedPropertyValuesAsList(SqlIdentifierParameterSource parameterSource, - RelationalPersistentProperty property, Iterable values) { - - List convertedIds = new ArrayList<>(); - JdbcValue jdbcValue = null; - for (Object id : values) { - - Class columnType = converter.getColumnType(property); - SQLType sqlType = converter.getTargetSqlType(property); - - jdbcValue = converter.writeJdbcValue(id, columnType, sqlType); - convertedIds.add(jdbcValue.getValue()); - } - - Assert.state(jdbcValue != null, "JdbcValue must be not null at this point; Please report this as a bug"); - - SQLType jdbcType = jdbcValue.getJdbcType(); - int typeNumber = jdbcType == null ? JdbcUtils.TYPE_UNKNOWN : jdbcType.getVendorTypeNumber(); - - parameterSource.addValue(SqlGenerator.IDS_SQL_PARAMETER, convertedIds, typeNumber); - } - @SuppressWarnings("unchecked") private RelationalPersistentEntity getRequiredPersistentEntity(Class domainType) { return (RelationalPersistentEntity) context.getRequiredPersistentEntity(domainType); @@ -235,12 +260,14 @@ private SqlIdentifierParameterSource getParameterSource(@Nullable S insta PersistentPropertyAccessor propertyAccessor = instance != null ? persistentEntity.getPropertyAccessor(instance) : NoValuePropertyAccessor.instance(); + persistentEntity.doWithAll(property -> { if (skipProperty.test(property) || !property.isWritable()) { return; } - if (property.isEntity() && !property.isEmbedded()) { + + if (RelationalPredicates.isRelation(property)) { return; } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/Tables.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/Tables.java index c8d28cc309..e77cdd6884 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/Tables.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/mapping/schema/Tables.java @@ -33,6 +33,7 @@ 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.RelationalPredicates; import org.springframework.data.relational.core.sql.SqlIdentifier; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -68,7 +69,7 @@ public static Tables from(Stream> persis for (RelationalPersistentProperty property : entity) { - if (property.isEntity() && !property.isEmbedded()) { + if (RelationalPredicates.isRelation(property)) { foreignKeyMetadataList.add(createForeignKeyMetadata(entity, property, context, sqlTypeMapping)); continue; } 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..d3968b18a0 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,11 @@ import java.util.ArrayList; import java.util.List; -import java.util.stream.Stream; +import java.util.function.Consumer; 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,10 +30,13 @@ 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.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; @@ -44,13 +46,14 @@ import org.springframework.data.relational.repository.query.RelationalParameterAccessor; import org.springframework.data.relational.repository.query.RelationalQueryCreator; import org.springframework.data.repository.query.parser.PartTree; +import org.springframework.data.util.Predicates; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.lang.Nullable; 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,18 +99,18 @@ protected List complete(@Nullable Criteria criteria, Sort sor Table table = Table.create(entityMetadata.getTableName()); MapSqlParameterSource parameterSource = new MapSqlParameterSource(); - SqlContext sqlContext = new SqlContext(entity); - Condition condition = criteria == null ? null : queryMapper.getMappedObject(parameterSource, criteria, table, entity); + List idColumns = context.getAggregatePath(entity).getTableInfo().idColumnInfos().toColumnList(table); + // 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 List deleteChain = new ArrayList<>(); - deleteRelations(deleteChain, entity, select); + deleteRelations(entity, select, deleteChain::add); // crate delete query DeleteWhere deleteBuilder = StatementBuilder.delete(table); @@ -125,34 +128,39 @@ protected List complete(@Nullable Criteria criteria, Sort sor return queries; } - private void deleteRelations(List deleteChain, RelationalPersistentEntity entity, Select parentSelect) { + private void deleteRelations(RelationalPersistentEntity entity, Select parentSelect, + Consumer deleteConsumer) { for (PersistentPropertyPath path : context - .findPersistentPropertyPaths(entity.getType(), p -> true)) { + .findPersistentPropertyPaths(entity.getType(), Predicates.isTrue())) { AggregatePath aggregatePath = context.getAggregatePath(path); - // prevent duplication on recursive call - if (path.getLength() > 1 && !aggregatePath.getParentPath().isEmbedded()) { + if (aggregatePath.isEmbedded() || !aggregatePath.isEntity()) { continue; } - if (aggregatePath.isEntity() && !aggregatePath.isEmbedded()) { + SqlContext sqlContext = new SqlContext(); - SqlContext sqlContext = new SqlContext(aggregatePath.getLeafEntity()); + // MariaDB prior to 11.6 does not support aliases for delete statements + Table table = sqlContext.getUnaliasedTable(aggregatePath); - Condition inCondition = Conditions - .in(sqlContext.getTable().column(aggregatePath.getTableInfo().reverseColumnInfo().name()), parentSelect); + List reverseColumns = aggregatePath.getTableInfo().backReferenceColumnInfos().toColumnList(table); + Expression expression = Expressions.of(reverseColumns); - Select select = StatementBuilder.select( // - sqlContext.getTable().column(aggregatePath.getIdDefiningParentPath().getTableInfo().idColumnName()) // - ).from(sqlContext.getTable()) // - .where(inCondition) // - .build(); - deleteRelations(deleteChain, aggregatePath.getLeafEntity(), select); + Condition inCondition = Conditions.in(expression, parentSelect); - deleteChain.add(StatementBuilder.delete(sqlContext.getTable()).where(inCondition).build()); - } + List parentIdColumns = aggregatePath.getIdDefiningParentPath().getTableInfo().idColumnInfos() + .toColumnList(table); + + Select select = StatementBuilder.select( // + parentIdColumns // + ).from(table) // + .where(inCondition) // + .build(); + deleteRelations(aggregatePath.getLeafEntity(), select, deleteConsumer); + + deleteConsumer.accept(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..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 @@ -15,15 +15,14 @@ */ package org.springframework.data.jdbc.repository.query; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; 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; @@ -33,12 +32,10 @@ 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.render.SqlRenderer; import org.springframework.data.relational.repository.Lock; @@ -73,6 +70,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}, @@ -86,16 +84,45 @@ 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} + * @since 4.0 + */ + 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; @@ -107,6 +134,7 @@ class JdbcQueryCreator extends RelationalQueryCreator { this.isSliceQuery = isSliceQuery; this.returnedType = returnedType; this.lockMode = lockMode; + this.sqlGeneratorSource = sqlGeneratorSource; } /** @@ -222,7 +250,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); @@ -235,139 +264,13 @@ SelectBuilder.SelectLimitOffset createSelectClause(RelationalPersistentEntity private SelectBuilder.SelectJoin selectBuilder(Table table) { - List columnExpressions = new ArrayList<>(); RelationalPersistentEntity entity = entityMetadata.getTableEntity(); - SqlContext sqlContext = new SqlContext(entity); - - 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) { - baseSelect = baseSelect.leftOuterJoin(join.joinTable).on(join.joinColumn).equals(join.parentId); - } - - return baseSelect; - } - - /** - * Create a {@link Column} for {@link AggregatePath}. - * - * @param sqlContext - * @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()) { - - // 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() // - ) { - return null; - } - - return sqlContext.getReverseColumn(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); + Predicate filter = ap -> returnedType.needsCustomConstruction() + && !returnedType.getInputProperties().contains(ap.getRequiredBaseProperty().getName()); - return new Join( // - currentTable, // - currentTable.column(path.getTableInfo().reverseColumnInfo().name()), // - parentTable.column(idDefiningParentPath.getTableInfo().idColumnName()) // - ); + return (SelectBuilder.SelectJoin) sqlGeneratorSource.getSqlGenerator(entity.getType()).createSelectBuilder(table, + filter); } - /** - * Value object representing a {@code JOIN} association. - */ - static private 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; - } - - @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 + '}'; - } - } } 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..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 @@ -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().backReferenceColumnInfos().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/main/java/org/springframework/data/jdbc/support/JdbcUtil.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/support/JdbcUtil.java index 5fec40f1a9..c091191bb5 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/support/JdbcUtil.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/support/JdbcUtil.java @@ -27,7 +27,7 @@ import java.util.Map; import org.springframework.jdbc.support.JdbcUtils; -import org.springframework.util.Assert; +import org.springframework.util.ConcurrentLruCache; /** * Contains methods dealing with the quirks of JDBC, independent of any Entity, Aggregate or Repository abstraction. @@ -58,7 +58,10 @@ public String toString() { return getName(); } }; + private static final Map, SQLType> sqlTypeMappings = new HashMap<>(); + private static ConcurrentLruCache, SQLType> sqlTypeCache = new ConcurrentLruCache<>(64, + JdbcUtil::doGetSqlType); static { @@ -97,9 +100,10 @@ private JdbcUtil() { * @return a matching {@link SQLType} or {@link #TYPE_UNKNOWN}. */ public static SQLType targetSqlTypeFor(Class type) { + return sqlTypeCache.get(type); + } - Assert.notNull(type, "Type must not be null"); - + private static SQLType doGetSqlType(Class type) { return sqlTypeMappings.keySet().stream() // .filter(k -> k.isAssignableFrom(type)) // .findFirst() // 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..2d464b1bf9 --- /dev/null +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/CompositeIdAggregateTemplateHsqlIntegrationTests.java @@ -0,0 +1,307 @@ +/* + * 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.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; +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.data.relational.core.query.Query; +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) +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); + } + + @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) { + } + + private record SimpleEntity( // + @Id 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 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/JdbcAggregateChangeExecutorContextImmutableUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextImmutableUnitTests.java index 78d05c03dc..bf0b41a044 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); @@ -166,7 +167,8 @@ PersistentPropertyPath getPersistentPropertyPath(S } Identifier createBackRef(long value) { - return JdbcIdentifierBuilder.forBackReferences(converter, toAggregatePath("content"), value).build(); + return JdbcIdentifierBuilder.forBackReferences(converter, toAggregatePath("content"), + JdbcAggregateChangeExecutionContext.getIdMapper(value, toAggregatePath("content"), converter)).build(); } PersistentPropertyPath toPath(String path) { @@ -180,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; @@ -221,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)) @@ -253,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) { @@ -274,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; @@ -290,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 eef22d5c94..afb0f224c0 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,8 @@ PersistentPropertyPath getPersistentPropertyPath(S } Identifier createBackRef(long value) { - return forBackReferences(converter, toAggregatePath("content"), value).build(); + return forBackReferences(converter, toAggregatePath("content"), + getIdMapper(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 5873ce23a1..5c0daa2e0b 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,109 +21,193 @@ 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}. * * @author Jens Schauder */ -public class JdbcIdentifierBuilderUnitTests { +class JdbcIdentifierBuilderUnitTests { JdbcMappingContext context = new JdbcMappingContext(); JdbcConverter converter = new MappingJdbcConverter(context, (identifier, path) -> { throw new UnsupportedOperationException(); }); - @Test // DATAJDBC-326 - public void parametersWithPropertyKeysUseTheParentPropertyJdbcType() { - - Identifier identifier = JdbcIdentifierBuilder.forBackReferences(converter, getPath("child"), "eins").build(); + @Nested + class WithSimpleId { + @Test // DATAJDBC-326 + void parametersWithPropertyKeysUseTheParentPropertyJdbcType() { + + Identifier identifier = JdbcIdentifierBuilder + .forBackReferences(converter, getPath("child"), getValueProvider("eins", getPath("child"), converter)) + .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, getValueProvider("parent-eins", path, converter)) // + .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, getValueProvider("parent-eins", path, converter)) // + .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"), + getValueProvider("parent-eins", getPath("embeddable.child"), converter)) // + .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"), + getValueProvider("parent-eins", getPath("noId.child"), converter)) // + .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); + } + } - assertThat(identifier.getParts()) // - .extracting("name", "value", "targetType") // - .containsExactly( // - tuple(quoted("DUMMY_ENTITY"), "eins", UUID.class) // - ); + /** + * 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 { + + ap = ap.getTail(); + PersistentPropertyPathAccessor propertyPathAccessor = entity.getPropertyPathAccessor(idValue); + return propertyPathAccessor.getProperty(ap.getRequiredPersistentPropertyPath()); + } + }; + return valueProvider; } - @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, getValueProvider(exampleId, path, converter)) // + .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..9a011a8802 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 @@ -17,20 +17,21 @@ import static java.util.Collections.*; import static org.assertj.core.api.Assertions.*; -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,10 +42,11 @@ * * @author Bastian Wilhelm * @author Mark Paluch + * @author Jens Schauder */ class SqlGeneratorEmbeddedUnitTests { - private final RelationalMappingContext context = new JdbcMappingContext(); + private RelationalMappingContext context = new JdbcMappingContext(); private JdbcConverter converter = new MappingJdbcConverter(context, (identifier, path) -> { throw new UnsupportedOperationException(); }); @@ -63,133 +65,252 @@ SqlGenerator createSqlGenerator(Class type) { @Test // DATAJDBC-111 void findOne() { - final String sql = sqlGenerator.getFindOne(); - - assertSoftly(softly -> { - - softly.assertThat(sql).startsWith("SELECT") // - .contains("dummy_entity.id1 AS id1") // - .contains("dummy_entity.test AS test") // - .contains("dummy_entity.attr1 AS attr1") // - .contains("dummy_entity.attr2 AS attr2") // - .contains("dummy_entity.prefix2_attr1 AS prefix2_attr1") // - .contains("dummy_entity.prefix2_attr2 AS prefix2_attr2") // - .contains("dummy_entity.prefix_test AS prefix_test") // - .contains("dummy_entity.prefix_attr1 AS prefix_attr1") // - .contains("dummy_entity.prefix_attr2 AS prefix_attr2") // - .contains("dummy_entity.prefix_prefix2_attr1 AS prefix_prefix2_attr1") // - .contains("dummy_entity.prefix_prefix2_attr2 AS prefix_prefix2_attr2") // - .contains("WHERE dummy_entity.id1 = :id") // - .doesNotContain("JOIN").doesNotContain("embeddable"); // - }); + String sql = sqlGenerator.getFindOne(); + + assertThat(sql).startsWith("SELECT") // + .contains("dummy_entity.id1 AS id1") // + .contains("dummy_entity.test AS test") // + .contains("dummy_entity.attr1 AS attr1") // + .contains("dummy_entity.attr2 AS attr2") // + .contains("dummy_entity.prefix2_attr1 AS prefix2_attr1") // + .contains("dummy_entity.prefix2_attr2 AS prefix2_attr2") // + .contains("dummy_entity.prefix_test AS prefix_test") // + .contains("dummy_entity.prefix_attr1 AS prefix_attr1") // + .contains("dummy_entity.prefix_attr2 AS prefix_attr2") // + .contains("dummy_entity.prefix_prefix2_attr1 AS prefix_prefix2_attr1") // + .contains("dummy_entity.prefix_prefix2_attr2 AS prefix_prefix2_attr2") // + .contains("WHERE dummy_entity.id1 = :id") // + .doesNotContain("JOIN").doesNotContain("embeddable"); // + } + + @Test // GH-574 + void findOneWrappedId() { + + SqlGenerator sqlGenerator = createSqlGenerator(WithWrappedId.class); + + String sql = sqlGenerator.getFindOne(); + + assertThat(sql).startsWith("SELECT") // + .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(WithEmbeddedId.class); + + String sql = sqlGenerator.getFindOne(); + + assertThat(sql).startsWith("SELECT") // + .contains("with_embedded_id.name AS name") // + .contains("with_embedded_id.one") // + .contains("with_embedded_id.two") // + .contains(" WHERE ") // + .contains("with_embedded_id.one = :one") // + .contains("with_embedded_id.two = :two"); + } + + @Test // GH-574 + void deleteByIdEmbeddedId() { + + SqlGenerator sqlGenerator = createSqlGenerator(WithEmbeddedId.class); + + String sql = sqlGenerator.getDeleteById(); + + assertThat(sql).startsWith("DELETE") // + .contains(" WHERE ") // + .contains("with_embedded_id.one = :one") // + .contains("with_embedded_id.two = :two"); + } + + @Test // GH-574 + void deleteByIdInEmbeddedId() { + + SqlGenerator sqlGenerator = createSqlGenerator(WithEmbeddedId.class); + + String sql = sqlGenerator.getDeleteByIdIn(); + + assertThat(sql).startsWith("DELETE") // + .contains(" WHERE ") // + .contains("(with_embedded_id.one, with_embedded_id.two) IN (:ids)"); + } + + @Test // GH-574 + void deleteByPathEmbeddedId() { + + SqlGenerator sqlGenerator = createSqlGenerator(WithEmbeddedId.class); + PersistentPropertyPath path = PersistentPropertyPathTestUtils.getPath("other", + WithEmbeddedIdAndReference.class, context); + + String sql = sqlGenerator.createDeleteByPath(path); + + assertThat(sql).startsWith("DELETE FROM other_entity WHERE") // + .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(WithEmbeddedId.class); + PersistentPropertyPath path = PersistentPropertyPathTestUtils.getPath("other", + WithEmbeddedIdAndReference.class, context); + + String sql = sqlGenerator.createDeleteInByPath(path); + + assertThat(sql).startsWith("DELETE FROM other_entity WHERE") // + .contains(" WHERE ") // + .contains( + "(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(WithEmbeddedId.class); + + String sql = sqlGenerator.getUpdate(); + + assertThat(sql).startsWith("UPDATE") // + .contains(" WHERE ") // + .contains("with_embedded_id.one = :one") // + .contains("with_embedded_id.two = :two"); + } + + @Test // GH-574 + void existsByIdEmbeddedId() { + + SqlGenerator sqlGenerator = createSqlGenerator(WithEmbeddedId.class); + + String sql = sqlGenerator.getExists(); + + assertThat(sql).startsWith("SELECT COUNT") // + .contains(" WHERE ") // + .contains("with_embedded_id.one = :one") // + .contains("with_embedded_id.two = :two"); } @Test // DATAJDBC-111 void findAll() { - final String sql = sqlGenerator.getFindAll(); - - assertSoftly(softly -> { - - softly.assertThat(sql).startsWith("SELECT") // - .contains("dummy_entity.id1 AS id1") // - .contains("dummy_entity.test AS test") // - .contains("dummy_entity.attr1 AS attr1") // - .contains("dummy_entity.attr2 AS attr2") // - .contains("dummy_entity.prefix2_attr1 AS prefix2_attr1") // - .contains("dummy_entity.prefix2_attr2 AS prefix2_attr2") // - .contains("dummy_entity.prefix_test AS prefix_test") // - .contains("dummy_entity.prefix_attr1 AS prefix_attr1") // - .contains("dummy_entity.prefix_attr2 AS prefix_attr2") // - .contains("dummy_entity.prefix_prefix2_attr1 AS prefix_prefix2_attr1") // - .contains("dummy_entity.prefix_prefix2_attr2 AS prefix_prefix2_attr2") // - .doesNotContain("JOIN") // - .doesNotContain("embeddable"); - }); + String sql = sqlGenerator.getFindAll(); + + assertThat(sql).startsWith("SELECT") // + .contains("dummy_entity.id1 AS id1") // + .contains("dummy_entity.test AS test") // + .contains("dummy_entity.attr1 AS attr1") // + .contains("dummy_entity.attr2 AS attr2") // + .contains("dummy_entity.prefix2_attr1 AS prefix2_attr1") // + .contains("dummy_entity.prefix2_attr2 AS prefix2_attr2") // + .contains("dummy_entity.prefix_test AS prefix_test") // + .contains("dummy_entity.prefix_attr1 AS prefix_attr1") // + .contains("dummy_entity.prefix_attr2 AS prefix_attr2") // + .contains("dummy_entity.prefix_prefix2_attr1 AS prefix_prefix2_attr1") // + .contains("dummy_entity.prefix_prefix2_attr2 AS prefix_prefix2_attr2") // + .doesNotContain("JOIN") // + .doesNotContain("embeddable"); } @Test // DATAJDBC-111 void findAllInList() { - final String sql = sqlGenerator.getFindAllInList(); - - assertSoftly(softly -> { - - softly.assertThat(sql).startsWith("SELECT") // - .contains("dummy_entity.id1 AS id1") // - .contains("dummy_entity.test AS test") // - .contains("dummy_entity.attr1 AS attr1") // - .contains("dummy_entity.attr2 AS attr2").contains("dummy_entity.prefix2_attr1 AS prefix2_attr1") // - .contains("dummy_entity.prefix2_attr2 AS prefix2_attr2") // - .contains("dummy_entity.prefix_test AS prefix_test") // - .contains("dummy_entity.prefix_attr1 AS prefix_attr1") // - .contains("dummy_entity.prefix_attr2 AS prefix_attr2") // - .contains("dummy_entity.prefix_prefix2_attr1 AS prefix_prefix2_attr1") // - .contains("dummy_entity.prefix_prefix2_attr2 AS prefix_prefix2_attr2") // - .contains("WHERE dummy_entity.id1 IN (:ids)") // - .doesNotContain("JOIN") // - .doesNotContain("embeddable"); - }); + + String sql = sqlGenerator.getFindAllInList(); + + assertThat(sql).startsWith("SELECT") // + .contains("dummy_entity.id1 AS id1") // + .contains("dummy_entity.test AS test") // + .contains("dummy_entity.attr1 AS attr1") // + .contains("dummy_entity.attr2 AS attr2").contains("dummy_entity.prefix2_attr1 AS prefix2_attr1") // + .contains("dummy_entity.prefix2_attr2 AS prefix2_attr2") // + .contains("dummy_entity.prefix_test AS prefix_test") // + .contains("dummy_entity.prefix_attr1 AS prefix_attr1") // + .contains("dummy_entity.prefix_attr2 AS prefix_attr2") // + .contains("dummy_entity.prefix_prefix2_attr1 AS prefix_prefix2_attr1") // + .contains("dummy_entity.prefix_prefix2_attr2 AS prefix_prefix2_attr2") // + .contains("WHERE dummy_entity.id1 IN (:ids)") // + .doesNotContain("JOIN") // + .doesNotContain("embeddable"); + } + + @Test // GH-574 + void findAllInListEmbeddedId() { + + SqlGenerator sqlGenerator = createSqlGenerator(WithEmbeddedId.class); + + String sql = sqlGenerator.getFindAllInList(); + + assertThat(sql).startsWith("SELECT") // + .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(WithEmbeddedIdAndReference.class); + + String sql = sqlGenerator.getFindOne(); + + 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(" WHERE ") // + .contains("with_embedded_id_and_reference.one = :one") // + .contains("with_embedded_id_and_reference.two = :two"); } @Test // DATAJDBC-111 void insert() { - final String sql = sqlGenerator.getInsert(emptySet()); - - assertSoftly(softly -> { - - softly.assertThat(sql) // - .startsWith("INSERT INTO") // - .contains("dummy_entity") // - .contains(":test") // - .contains(":attr1") // - .contains(":attr2") // - .contains(":prefix2_attr1") // - .contains(":prefix2_attr2") // - .contains(":prefix_test") // - .contains(":prefix_attr1") // - .contains(":prefix_attr2") // - .contains(":prefix_prefix2_attr1") // - .contains(":prefix_prefix2_attr2"); - }); + String sql = sqlGenerator.getInsert(emptySet()); + + assertThat(sql) // + .startsWith("INSERT INTO") // + .contains("dummy_entity") // + .contains(":test") // + .contains(":attr1") // + .contains(":attr2") // + .contains(":prefix2_attr1") // + .contains(":prefix2_attr2") // + .contains(":prefix_test") // + .contains(":prefix_attr1") // + .contains(":prefix_attr2") // + .contains(":prefix_prefix2_attr1") // + .contains(":prefix_prefix2_attr2"); } @Test // DATAJDBC-111 void update() { - final String sql = sqlGenerator.getUpdate(); - - assertSoftly(softly -> { - - softly.assertThat(sql) // - .startsWith("UPDATE") // - .contains("dummy_entity") // - .contains("test = :test") // - .contains("attr1 = :attr1") // - .contains("attr2 = :attr2") // - .contains("prefix2_attr1 = :prefix2_attr1") // - .contains("prefix2_attr2 = :prefix2_attr2") // - .contains("prefix_test = :prefix_test") // - .contains("prefix_attr1 = :prefix_attr1") // - .contains("prefix_attr2 = :prefix_attr2") // - .contains("prefix_prefix2_attr1 = :prefix_prefix2_attr1") // - .contains("prefix_prefix2_attr2 = :prefix_prefix2_attr2"); - }); + String sql = sqlGenerator.getUpdate(); + + assertThat(sql) // + .startsWith("UPDATE") // + .contains("dummy_entity") // + .contains("test = :test") // + .contains("attr1 = :attr1") // + .contains("attr2 = :attr2") // + .contains("prefix2_attr1 = :prefix2_attr1") // + .contains("prefix2_attr2 = :prefix2_attr2") // + .contains("prefix_test = :prefix_test") // + .contains("prefix_attr1 = :prefix_attr1") // + .contains("prefix_attr2 = :prefix_attr2") // + .contains("prefix_prefix2_attr1 = :prefix_prefix2_attr1") // + .contains("prefix_prefix2_attr2 = :prefix_prefix2_attr2"); } @Test // DATAJDBC-340 - @Disabled // this is just broken right now void deleteByPath() { - final String sql = sqlGenerator + sqlGenerator = createSqlGenerator(DummyEntity2.class); + + 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 @@ -275,14 +396,9 @@ 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")); - }); + assertThat(join.joinTable().getName()).isEqualTo(SqlIdentifier.unquoted("other_entity")); + assertThat(join.condition()) + .isEqualTo(SqlGeneratorUnitTests.equalsCondition("dummy_entity2", "id", join.joinTable(), "dummy_entity2")); } @Test // DATAJDBC-340 @@ -301,6 +417,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 +432,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 +450,47 @@ static class DummyEntity { @Embedded(onEmpty = OnEmpty.USE_NULL) CascadedEmbedded embeddable; } + record WrappedId(Long id) { + } + + static class WithWrappedId { + + @Id + @Embedded(onEmpty = OnEmpty.USE_NULL) WrappedId wrappedId; + + String name; + } + + record EmbeddedId(Long one, String two) { + } + + static class WithEmbeddedId { + + @Id + @Embedded(onEmpty = OnEmpty.USE_NULL) EmbeddedId embeddedId; + + String name; + + } + + static class WithEmbeddedIdAndReference { + + @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 +512,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 cc264cbe62..2c13bdfc52 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 @@ -56,6 +56,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; @@ -91,6 +92,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); @@ -159,7 +176,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 @@ -176,7 +193,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 @@ -226,7 +243,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 @@ -381,7 +398,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 @@ -660,7 +678,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" // ); } @@ -679,7 +697,7 @@ void deletingLongChain() { "WHERE chain2.chain3 IN (" + // "SELECT chain3.x_three " + // "FROM chain3 " + // - "WHERE chain3.chain4 = :rootId" + // + "WHERE chain3.chain4 = :x_four" + // ")))"); } @@ -688,7 +706,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 @@ -704,7 +722,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" // + "))"); } @@ -720,11 +738,10 @@ 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.condition()).isEqualTo(equalsCondition(SqlIdentifier.quoted("DUMMY_ENTITY"), + SqlIdentifier.quoted("id1"), join.joinTable(), SqlIdentifier.quoted("DUMMY_ENTITY"))); + }); } @@ -751,13 +768,11 @@ void joinForSecondLevelReference() { SqlGenerator.Join join = generateJoin("ref.further", DummyEntity.class); assertSoftly(softly -> { + softly.assertThat(join.joinTable().getName()).isEqualTo(SqlIdentifier.quoted("SECOND_LEVEL_REFERENCED_ENTITY")); + softly.assertThat(join.condition()) + .isEqualTo(equalsCondition(Table.create("REFERENCED_ENTITY").as(SqlIdentifier.quoted("ref")), + SqlIdentifier.quoted("X_L1ID"), join.joinTable(), SqlIdentifier.quoted("REFERENCED_ENTITY"))); - 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")); }); } @@ -765,19 +780,15 @@ 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.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-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 a941d1830c..ef297a193e 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 @@ -40,6 +40,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; @@ -66,8 +67,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(); @@ -779,7 +780,8 @@ interface UserRepository extends Repository { @Table("users") static class User { - @Id Long id; + @Id + @Nullable UserId id; String firstName; String lastName; Date dateOfBirth; @@ -787,7 +789,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; @@ -795,6 +797,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-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-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..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 @@ -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,29 @@ 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/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); 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..9e868577fb --- /dev/null +++ b/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/CompositeIdRepositoryIntegrationTests.java @@ -0,0 +1,122 @@ +/* + * 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.r2dbc.repository; + +import static org.assertj.core.api.Assertions.*; + +import io.r2dbc.spi.ConnectionFactory; +import reactor.test.StepVerifier; + +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; + +/** + * 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) { + } + +} 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 diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MappingRelationalConverter.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MappingRelationalConverter.java index 395c64e677..82e7e133c3 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MappingRelationalConverter.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MappingRelationalConverter.java @@ -44,7 +44,16 @@ import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.PersistentPropertyPathAccessor; import org.springframework.data.mapping.context.MappingContext; -import org.springframework.data.mapping.model.*; +import org.springframework.data.mapping.model.CachingValueExpressionEvaluatorFactory; +import org.springframework.data.mapping.model.ConvertingPropertyAccessor; +import org.springframework.data.mapping.model.EntityInstantiator; +import org.springframework.data.mapping.model.ParameterValueProvider; +import org.springframework.data.mapping.model.PersistentEntityParameterValueProvider; +import org.springframework.data.mapping.model.PropertyValueProvider; +import org.springframework.data.mapping.model.SimpleTypeHolder; +import org.springframework.data.mapping.model.SpELContext; +import org.springframework.data.mapping.model.ValueExpressionEvaluator; +import org.springframework.data.mapping.model.ValueExpressionParameterValueProvider; import org.springframework.data.projection.EntityProjection; import org.springframework.data.projection.EntityProjectionIntrospector; import org.springframework.data.projection.EntityProjectionIntrospector.ProjectionPredicate; @@ -578,6 +587,10 @@ private Object readEmbedded(ConversionContext conversionContext, RelationalPrope private boolean shouldReadEmbeddable(ConversionContext context, RelationalPersistentProperty property, RelationalPersistentEntity unwrappedEntity, RelationalPropertyValueProvider propertyValueProvider) { + if (property.isIdProperty() && !property.isAnnotationPresent(Embedded.class)) { + return true; + } + OnEmpty onEmpty = property.getRequiredAnnotation(Embedded.class).onEmpty(); if (onEmpty.equals(OnEmpty.USE_EMPTY)) { diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityDeleteWriter.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityDeleteWriter.java index 2a68bc061e..cc706f7cb5 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityDeleteWriter.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalEntityDeleteWriter.java @@ -24,6 +24,7 @@ import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.data.relational.core.mapping.RelationalPredicates; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -124,7 +125,7 @@ private List> deleteReferencedEntities(Object id, AggregateChange private void forAllTableRepresentingPaths(Class entityType, Consumer> pathConsumer) { - context.findPersistentPropertyPaths(entityType, property -> property.isEntity() && !property.isEmbedded()) // + context.findPersistentPropertyPaths(entityType, RelationalPredicates.isRelation()) // .filter(path -> context.getAggregatePath(path).isWritable()) // .forEach(pathConsumer); } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/WritingContext.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/WritingContext.java index 84be15dfb2..6602e72841 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/WritingContext.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/WritingContext.java @@ -27,6 +27,7 @@ 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.RelationalPredicates; import org.springframework.data.util.Pair; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -61,7 +62,7 @@ class WritingContext { this.aggregateChange = aggregateChange; this.rootIdValueSource = IdValueSource.forInstance(root, context.getRequiredPersistentEntity(aggregateChange.getEntityType())); - this.paths = context.findPersistentPropertyPaths(entityType, (p) -> p.isEntity() && !p.isEmbedded()) // + this.paths = context.findPersistentPropertyPaths(entityType, RelationalPredicates::isRelation) // .filter(ppp -> context.getAggregatePath(ppp).isWritable()).toList(); } 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..bb4ab12469 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,25 +16,40 @@ package org.springframework.data.relational.core.mapping; +import java.util.ArrayList; +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.BinaryOperator; 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.Column; import org.springframework.data.relational.core.sql.SqlIdentifier; +import org.springframework.data.relational.core.sql.Table; 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 { +public interface AggregatePath extends Iterable, Comparable { /** * Returns the path that has the same beginning but is one segment shorter than this path. @@ -52,6 +67,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 4.0 + */ + AggregatePath append(AggregatePath path); + /** * @return {@literal true} if this is a root path for the underlying type. */ @@ -222,47 +246,69 @@ default Stream stream() { /** * Returns the longest ancestor path that has an {@link org.springframework.data.annotation.Id} property. * - * @return A path that starts just as this path but is shorter. Guaranteed to be not {@literal null}. TODO: throws - * NoSuchElementException: No value present for empty paths + * @return a path that starts just as this path but is shorter. Guaranteed to be not {@literal null}. + * @throws IllegalStateException if the current path is not a {@link #isRoot() root} path or if there is no identifier + * associated. */ AggregatePath getIdDefiningParentPath(); - 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, - - ColumnInfo reverseColumnInfo, + /** + * 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 4.0 + */ + @Nullable + AggregatePath getTail(); - /* - * 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. + * @since 4.0 + */ + @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. + * @since 4.0 + */ + @Override + default int compareTo(AggregatePath other) { + return toDotPath().compareTo(other.toDotPath()); + } - /* - * The column name of the id column of the ancestor path that represents an actual table. - */ - SqlIdentifier idColumnName, + AggregatePath getSubPathBasedOn(Class baseType); - /* - * 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) { + /** + * 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) { @@ -273,18 +319,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 backReferenceColumnInfos = computeBackReferenceColumnInfos(path); ColumnInfo qualifierColumnInfo = null; if (!path.isRoot()) { @@ -300,27 +335,128 @@ static TableInfo of(AggregatePath path) { qualifierColumnType = path.getRequiredLeafProperty().getQualifierColumnType(); } - SqlIdentifier idColumnName = leafEntity.hasIdProperty() ? leafEntity.getIdColumn() : null; + ColumnInfos idColumnInfos = computeIdColumnInfos(tableOwner, leafEntity); + + return new TableInfo(qualifiedTableName, tableAlias, backReferenceColumnInfos, qualifierColumnInfo, + qualifierColumnType, idColumnInfos); + + } - SqlIdentifier effectiveIdColumnName = tableOwner.isRoot() ? idColumnName : reverseColumnInfo.name(); + private static ColumnInfos computeIdColumnInfos(AggregatePath tableOwner, + RelationalPersistentEntity leafEntity) { - return new TableInfo(qualifiedTableName, tableAlias, reverseColumnInfo, qualifierColumnInfo, qualifierColumnType, - idColumnName, effectiveIdColumnName); + ColumnInfos idColumnInfos = ColumnInfos.empty(); + 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 computeBackReferenceColumnInfos(AggregatePath path) { - /* 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) { + AggregatePath tableOwner = AggregatePathTraversal.getTableOwningPath(path); + + if (tableOwner.isRoot()) { + return ColumnInfos.empty(); + } + + AggregatePath idDefiningParentPath = tableOwner.getIdDefiningParentPath(); + RelationalPersistentProperty idProperty = idDefiningParentPath.getRequiredLeafEntity().getIdProperty(); + + AggregatePath basePath = idProperty != null && idProperty.isEntity() ? idDefiningParentPath.append(idProperty) + : idDefiningParentPath; + ColumInfosBuilder ciBuilder = new ColumInfosBuilder(basePath); + + if (idProperty != null && idProperty.isEntity()) { + + 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); + }); + + } else { + + RelationalPersistentProperty leafProperty = tableOwner.getRequiredLeafProperty(); + SqlIdentifier reverseColumnName = leafProperty + .getReverseColumnName(idDefiningParentPath.getRequiredLeafEntity()); + SqlIdentifier alias = AggregatePathTableUtils.prefixWithTableAlias(path, reverseColumnName); + + 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 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 backReferenceColumnInfos.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. + * @since 3.2 + */ + 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 +474,195 @@ 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}. 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 + * @author Mark Paluch + * @since 4.0 + */ + class ColumnInfos { + + private final Map columnInfos; + private final Map> columnCache; + + /** + * Creates a new ColumnInfos instances based on the arguments. + * + * @param columnInfos A map, mapping {@literal AggregatePath} instances to the respective {@literal ColumnInfo} + */ + ColumnInfos(Map columnInfos) { + + this.columnInfos = new TreeMap<>(); + this.columnInfos.putAll(columnInfos); + this.columnCache = new HashMap<>(columnInfos.size(), 1f); + } + + /** + * An empty {@literal ColumnInfos} instance with a fixed base path. Useful as a base when collecting + * {@link ColumnInfo} instances into an {@literal ColumnInfos} instance. + * + * @return an empty instance save the {@literal basePath}. + */ + public static ColumnInfos empty() { + return new ColumnInfos(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(); + Assert.state(values.size() == 1, "ColumnInfo is not 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(); + } + + /** + * 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 -> toColumnList((__, ci) -> t.column(ci.name))); + } + + /** + * Creates a {@link List} of {@link Column} instances by applying the provided mapping function to each + * {@link AggregatePath} and {@link ColumnInfo}. + * + * @param mappingFunction function to map {@link AggregatePath} and {@link ColumnInfo} to a {@link Column}. + * @return a list of {@link Column}s. + * @since 4.0 + */ + public List toColumnList(BiFunction mappingFunction) { + + List result = new ArrayList<>(columnInfos.size()); + columnInfos.forEach((ap, ci) -> result.add(mappingFunction.apply(ap, ci))); + return result; + } + + /** + * 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. + * @param type of the result. + * @return result of the function. + * @since 4.0 + */ + 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; + } + + /** + * 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 consumer for each pair of {@link AggregatePath} and {@literal ColumnInfo}. + * + * @param consumer the function to call. + */ + public void forEachLong(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); + } + + /** + * Return an {@link Iterable} of {@link AggregatePath} associated with this column infos. + * + * @return + */ + public Iterable paths() { + return columnInfos.keySet(); + } + + /** + * 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(); + } + + /** + * Number of {@literal ColumnInfo} elements in this instance. + * + * @return the size of the collection of {@literal ColumnInfo}. + */ + public int size() { + return columnInfos.size(); + } + } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/AggregatePathTraversal.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/AggregatePathTraversal.java index 3a5aa3f4a2..3dd353d252 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/AggregatePathTraversal.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/AggregatePathTraversal.java @@ -15,14 +15,22 @@ */ package org.springframework.data.relational.core.mapping; -import java.util.NoSuchElementException; import java.util.function.Predicate; /** + * Traversal methods for {@link AggregatePath} to find paths that define the ID or own the table. + * * @author Mark Paluch + * @since 3.2 */ public class AggregatePathTraversal { + /** + * Get the path that defines the identifier of the aggregate. + * + * @param aggregatePath + * @return + */ public static AggregatePath getIdDefiningPath(AggregatePath aggregatePath) { Predicate idDefiningPathFilter = ap -> !ap.equals(aggregatePath) @@ -30,18 +38,25 @@ public static AggregatePath getIdDefiningPath(AggregatePath aggregatePath) { AggregatePath result = aggregatePath.filter(idDefiningPathFilter); if (result == null) { - throw new NoSuchElementException(); + throw new IllegalStateException( + "No identifier associated within this aggregate path: %s".formatted(aggregatePath)); } return result; } + /** + * Get the path that owns the table of the aggregate. + * + * @param aggregatePath + * @return + */ public static AggregatePath getTableOwningPath(AggregatePath aggregatePath) { Predicate tableOwningPathFilter = ap -> ap.isEntity() && !ap.isEmbedded(); AggregatePath result = aggregatePath.filter(tableOwningPathFilter); if (result == null) { - throw new NoSuchElementException(); + throw new IllegalStateException("No table associated within this aggregate path: %s".formatted(aggregatePath)); } return result; } 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/BasicRelationalPersistentProperty.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java index 0538a98103..395174a645 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java @@ -250,7 +250,7 @@ public boolean isOrdered() { @Override public boolean isEmbedded() { - return isEmbedded; + return isEmbedded || (isIdProperty() && isEntity()); } @Override @@ -263,7 +263,8 @@ public boolean shouldCreateEmptyEmbedded() { Embedded findAnnotation = findAnnotation(Embedded.class); - return findAnnotation != null && OnEmpty.USE_EMPTY.equals(findAnnotation.onEmpty()); + return (findAnnotation != null && OnEmpty.USE_EMPTY.equals(findAnnotation.onEmpty())) + || (isIdProperty() && isEntity()); } @Override 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..9f230a492c --- /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 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 4.0 + */ +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, 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(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 b0bcc78cb2..42f25a2475 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 @@ -97,6 +97,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,14 +208,64 @@ public AggregatePath getIdDefiningParentPath() { return AggregatePathTraversal.getIdDefiningPath(this); } - /** - * Finds and returns the longest path with ich identical or an ancestor to the current path and maps directly to a - * table. - * - * @return a path. Guaranteed to be not {@literal null}. - */ - private AggregatePath getTableOwningAncestor() { - return AggregatePathTraversal.getTableOwningPath(this); + @Override + @Nullable + 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 subtract(@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.subtract(basePath.getTail()); + } + + throw new IllegalStateException("Can't subtract [%s] from [%s]".formatted(basePath, this)); + } + + @Override + public AggregatePath getSubPathBasedOn(Class baseType) { + + if (isRoot()) { + if (rootType.getType() != baseType) { + throw new IllegalStateException("No matching path found for [%s]".formatted(baseType)); + } + return this; + } + + RelationalPersistentEntity owner = getRequiredBaseProperty().getOwner(); + if (owner.getType() == baseType) { + return this; + } + + return getTail().getSubPathBasedOn(baseType); } /** @@ -240,7 +304,6 @@ public int hashCode() { return Objects.hash(context, rootType, path); } - @Override public String toString() { return "AggregatePath[" 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..b8c6315d69 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,8 +148,8 @@ protected RelationalPersistentProperty createPersistentProperty(Property propert } /** - * @since 3.2 * @return iff single query loading is enabled. + * @since 3.2 * @see #setSingleQueryLoadingEnabled(boolean) */ public boolean isSingleQueryLoadingEnabled() { @@ -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..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,10 @@ 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(forRemoval = true) SqlIdentifier getIdColumn(); } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPredicates.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPredicates.java new file mode 100644 index 0000000000..c308a4a49d --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPredicates.java @@ -0,0 +1,47 @@ +/* + * 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 java.util.function.Predicate; + +/** + * Collection of relational predicates. + * + * @author Mark Paluch + * @since 4.0 + */ +public class RelationalPredicates { + + /** + * Predicate to determine whether a property is a relation (i.e. it is an entity, not an identifier property, and not + * an embedded property). + * + * @return a predicate that tests if the given property is a relation. + */ + public static Predicate isRelation() { + return RelationalPredicates::isRelation; + } + + /** + * Determine whether a property is a relation (i.e. it is an entity, not an identifier property, and not an embedded + * property). + * + * @return {@literal true} if the property is a relation; {@literal false} otherwise. + */ + public static boolean isRelation(RelationalPersistentProperty property) { + return !property.isIdProperty() && property.isEntity() && !property.isEmbedded(); + } +} 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..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 @@ -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 4.0 + */ + 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 4.0 + */ + 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..2013e04450 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 @@ -45,6 +45,16 @@ public static Condition just(String sql) { return new ConstantCondition(sql); } + /** + * Empty {@link Condition} that can be used to indicate that no condition is present. + * + * @return empty (unrestricted) condition. + * @since 4.0 + */ + public static Condition unrestricted() { + return Unrestricted.INSTANCE; + } + /** * Creates a nested {@link Condition} that is enclosed with parentheses. Useful to combine {@code AND} and {@code OR} * statements. @@ -247,7 +257,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"); @@ -317,4 +327,5 @@ public static In notIn(Column column, Select subselect) { // Utility constructor. private Conditions() {} + } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Disjunct.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Disjunct.java new file mode 100644 index 0000000000..8283cd1ed2 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Disjunct.java @@ -0,0 +1,38 @@ +/* + * 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.sql; + +/** + * Disjunct condition that does not match any rows using {@code 1 = 0} + * + * @author Mark Paluch + * @since 4.0 + */ +enum Disjunct implements Condition { + + INSTANCE; + + @Override + public Condition and(Condition other) { + return INSTANCE; + } + + @Override + public Condition not() { + return Condition.super.not(); + } + +} 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..db6a348ec5 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,6 +15,8 @@ */ package org.springframework.data.relational.core.sql; +import java.util.List; + /** * Factory for common {@link Expression}s. * @@ -61,6 +63,26 @@ 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 4.0 + */ + public static Expression of(List columns) { + + if (columns.size() == 1) { + return columns.get(0); + } + return new TupleExpression(columns); + } + // Utility constructor. private Expressions() {} 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..30fac0924f --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/TupleExpression.java @@ -0,0 +1,72 @@ +/* + * 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.sql; + +import static java.util.stream.Collectors.*; + +import java.util.Collection; +import java.util.List; + +/** + * A tuple as used for {@code IN} predicates. For example: + * + *

    + *   WHERE (one, two) IN (select x, y from some_table)
    + * 
    + * + * @author Jens Schauder + * @since 4.0 + */ +public class TupleExpression extends AbstractSegment implements Expression { + + private final Collection expressions; + + private static Segment[] children(Collection expressions) { + return expressions.toArray(new Segment[0]); + } + + TupleExpression(Collection expressions) { + + super(children(expressions)); + + this.expressions = expressions; + } + + /** + * Creates a {@link TupleExpression} from the given expressions. + * + * @param expressions must not be {@literal null} or empty. + * @return the new {@link TupleExpression}. + */ + public static TupleExpression create(Expression... expressions) { + return new TupleExpression(List.of(expressions)); + } + + /** + * Creates a {@link TupleExpression} from the given expressions. + * + * @param expressions must not be {@literal null} or empty. + * @return the new {@link TupleExpression}. + */ + public static TupleExpression create(Collection expressions) { + return new TupleExpression(expressions); + } + + @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/Unrestricted.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Unrestricted.java new file mode 100644 index 0000000000..d66e955bd6 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Unrestricted.java @@ -0,0 +1,42 @@ +/* + * 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.sql; + +/** + * Unrestricted condition. Any condition combined with this condition will yield the other condition. + * + * @author Mark Paluch + * @since 4.0 + */ +enum Unrestricted implements Condition { + + INSTANCE; + + @Override + public Condition and(Condition other) { + return other; + } + + @Override + public Condition or(Condition other) { + return other; + } + + @Override + public Condition not() { + return Disjunct.INSTANCE; + } +} 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/JoinVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/JoinVisitor.java index 94c36b11be..e73061d9c3 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/JoinVisitor.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/JoinVisitor.java @@ -77,8 +77,12 @@ Delegation leaveNested(Visitable segment) { if (hasSeenCondition) { - joinClause.append(" ON "); - joinClause.append(conditionVisitor.getRenderedPart()); + CharSequence renderedPart = conditionVisitor.getRenderedPart(); + + if (!renderedPart.isEmpty()) { + joinClause.append(" ON "); + joinClause.append(renderedPart); + } hasSeenCondition = false; } 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..d03fce9d3f --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/TupleVisitor.java @@ -0,0 +1,72 @@ +/* + * 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.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 4.0 + */ +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..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 @@ -167,9 +167,10 @@ 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().backReferenceColumnInfos().toColumnList(table) // + ).as(rowCountAlias); columns.add(count); String backReferenceAlias = null; @@ -178,7 +179,8 @@ 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().backReferenceColumnInfos().unique().name()).as(backReferenceAlias)); keyAlias = aliases.getKeyAlias(basePath); Expression keyExpression = basePath.isQualified() @@ -238,9 +240,10 @@ private String getIdentifierProperty(List paths) { private static AnalyticFunction createRowNumberExpression(AggregatePath basePath, Table table, String rowNumberAlias) { + AggregatePath.ColumnInfos reverseColumnInfos = basePath.getTableInfo().backReferenceColumnInfos(); return AnalyticFunction.create("row_number") // - .partitionBy(table.column(basePath.getTableInfo().reverseColumnInfo().name())) // - .orderBy(table.column(basePath.getTableInfo().reverseColumnInfo().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..33a195d5b6 --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/AggregatePathAssertions.java @@ -0,0 +1,80 @@ +/* + * 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; + +/** + * Custom AssertJ assertions for {@link AggregatePath} instances + * + * @author Jens Schauder + * @since 4.0 + */ +public class AggregatePathAssertions extends AbstractAssert { + + /** + * Constructor taking the actual {@link AggregatePath} to assert over. + * + * @param actual + */ + 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); + } + + /** + * 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(); + + 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; + } + + /** + * assertion testing if the actual path is a root path. + */ + public AggregatePathAssertions isRoot() { + isNotNull(); + + if (!actual.isRoot()) { + failWithMessage("Expected AggregatePath to be root path, but it was not"); + } + return this; + } + + /** + * assertion testing if the actual path is NOT a root path. + */ + public AggregatePathAssertions isNotRoot() { + isNotNull(); + + if (actual.isRoot()) { + failWithMessage("Expected AggregatePath not to be root path, but it was."); + } + return this; + } +} 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..3b59af40ba --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/AggregatePathSoftAssertions.java @@ -0,0 +1,41 @@ +/* + * 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 java.util.function.Consumer; + +import org.assertj.core.api.SoftAssertions; +import org.assertj.core.api.SoftAssertionsProvider; + +/** + * 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); + } + + 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 new file mode 100644 index 0000000000..17583ed10e --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/ColumnInfosUnitTests.java @@ -0,0 +1,98 @@ +/* + * 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 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; +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(); + + @Test // GH-574 + void emptyColumnInfos() { + + AggregatePath.ColumnInfos columnInfos = AggregatePath.ColumnInfos.empty(); + + assertThat(columnInfos.isEmpty()).isTrue(); + assertThrows(NoSuchElementException.class, columnInfos::any); + assertThrows(IllegalStateException.class, columnInfos::unique); + assertThat(columnInfos.toColumnList(TABLE)).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.toColumnList(TABLE)).containsExactly(TABLE.column(ID)); + } + + @Test // GH-574 + void multiElementColumnInfos() { + + 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.toColumnList(TABLE)) // + .containsExactly( // + TABLE.column(SqlIdentifier.quoted("ONE")), // + TABLE.column(SqlIdentifier.quoted("TWO")) // + ); + + List collector = new ArrayList<>(); + columnInfos.forEach((ap, ci) -> collector.add(ap.toDotPath() + "+" + ci.name())); + assertThat(collector).containsExactly("id.one+\"ONE\"", "id.two+\"TWO\""); + } + + 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 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 c173d0294f..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 @@ -22,13 +22,17 @@ 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; 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}. @@ -46,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 @@ -54,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(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.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); }); @@ -75,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); @@ -92,16 +96,16 @@ void getRequiredLeafEntity() { @Test // GH-1525 void idDefiningPath() { - assertSoftly(softly -> { + AggregatePathSoftAssertions.assertAggregatePathsSoftly(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.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"); }); } @@ -121,13 +125,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 +144,19 @@ void reverseColumnName() { }); } + @Test // GH-574 + void reverseColumnNames() { + + assertSoftly(softly -> { + 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")); + + }); + } + @Test // GH-1525 void getQualifierColumn() { @@ -169,12 +186,11 @@ void getQualifierColumnType() { @Test // GH-1525 void extendBy() { + AggregatePathSoftAssertions.assertAggregatePathsSoftly(softly -> { - assertSoftly(softly -> { - - softly.assertThat(path().append(entity.getRequiredPersistentProperty("withId"))).isEqualTo(path("withId")); - softly.assertThat(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"); }); } @@ -229,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(); + + // embedded path into Second, held by + // List (so the parent is + // multi-valued but not third2). + softly.assertThat(path("secondList.third.value").isMultiValued()).isTrue(); softly.assertThat(path("secondList").isMultiValued()).isTrue(); }); } @@ -306,13 +322,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 +432,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() { @@ -453,42 +455,86 @@ void getRequiredPersistentPropertyPath() { } @Test // GH-1525 - void getEffectiveIdColumnName() { + 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() { + + AggregatePathSoftAssertions.assertAggregatePathsSoftly(softly -> { + + 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() { + + 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.assertAggregatePath(path("second.third").append(value)).hasPath("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"); - assertThat(path().getLength()).isEqualTo(1); - assertThat(path().stream().collect(Collectors.toList())).hasSize(1); + sorted.add(aw); + sorted.add(ast); + sorted.add(as); + sorted.add(alpha); - assertThat(path("second.third2").getLength()).isEqualTo(3); - assertThat(path("second.third2").stream().collect(Collectors.toList())).hasSize(3); + 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 +548,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..f673190aad --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/TupleExpressionUnitTests.java @@ -0,0 +1,52 @@ +/* + * 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.sql; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +/** + * Unit tests for construction of {@link TupleExpression}. + * + * @author Jens Schauder + */ +class TupleExpressionUnitTests { + + @Test // GH-574 + void singleExpressionDoesNotGetWrapped() { + + Column testColumn = Column.create("name", Table.create("employee")); + + Expression wrapped = Expressions.of(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 = Expressions.of(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..2a662a2919 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() { @@ -328,6 +138,17 @@ void shouldRenderOrderByAlias() { .isEqualTo("SELECT emp.name AS my_emp_name FROM employee emp ORDER BY my_emp_name ASC"); } + @Test // GH-574 + void shouldNotRenderEmptyCondition() { + + Table table = SQL.table("foo"); + Column bar = table.column("bar"); + + Select select = Select.builder().select(bar).from(table).where(Conditions.unrestricted()).build(); + + assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT foo.bar FROM foo"); + } + @Test // DATAJDBC-309 void shouldRenderIsNull() { @@ -424,7 +245,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 +255,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 +529,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 +544,239 @@ 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 // GH-574 + void shouldRenderSimpleJoinWithUnrestrictedCondition() { + + 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.unrestricted()) // + .build(); + + assertThat(SqlRenderer.toString(select)) + .isEqualTo("SELECT employee.id, department.name FROM employee " + "JOIN department"); + } + + @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 +794,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..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")`. @@ -106,6 +107,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-annotations.adoc b/src/main/antora/modules/ROOT/partials/mapping-annotations.adoc index e98d076c5d..b61072851a 100644 --- a/src/main/antora/modules/ROOT/partials/mapping-annotations.adoc +++ b/src/main/antora/modules/ROOT/partials/mapping-annotations.adoc @@ -1,7 +1,17 @@ The `RelationalConverter` can use metadata to drive the mapping of objects to rows. The following annotations are available: +* `@Embedded`: a property 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. diff --git a/src/main/antora/modules/ROOT/partials/mapping.adoc b/src/main/antora/modules/ROOT/partials/mapping.adoc index 7e864516e2..e6cacbaada 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]] @@ -149,6 +148,50 @@ 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 + +The identifier property can be annotated with `@Embedded` allowing to use composite ids. +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 table creation depends on the used database. +==== + [[entity-persistence.read-only-properties]] == Read Only Properties