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 bdbdc9267aa..8b35c598bea 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilder.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilder.java @@ -15,7 +15,13 @@ */ package org.springframework.data.jdbc.core.convert; +import org.springframework.beans.factory.config.ConstructorArgumentValues; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.SimplePropertyHandler; import org.springframework.data.relational.core.mapping.AggregatePath; +import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; +import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -41,10 +47,26 @@ public static JdbcIdentifierBuilder empty() { */ public static JdbcIdentifierBuilder forBackReferences(JdbcConverter converter, AggregatePath path, Object value) { + RelationalPersistentProperty idProperty = path.getIdDefiningParentPath().getRequiredIdProperty(); + + if (value != null && idProperty.isEntity() && idProperty.isEmbedded()) { + // TODO: Fix for more than one property + RelationalPersistentEntity propertyType = converter.getMappingContext().getRequiredPersistentEntity(idProperty.getType()); + PersistentPropertyAccessor propertyAccessor = propertyType.getPropertyAccessor(value); + Object[] bucket = new Object[1]; + propertyType.doWithProperties((SimplePropertyHandler) p -> { + if (bucket[0] != null) { + throw new IllegalStateException("Can't handle embededs with more than one property"); + } + bucket[0] = propertyAccessor.getProperty(p); + }); + value = bucket[0]; + } + Identifier identifier = Identifier.of( // path.getTableInfo().reverseColumnInfo().name(), // value, // - converter.getColumnType(path.getIdDefiningParentPath().getRequiredIdProperty()) // + converter.getColumnType(idProperty) // ); return new JdbcIdentifierBuilder(identifier); 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 2b3ad379456..0aa19a23f0e 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 @@ -20,6 +20,7 @@ import java.sql.SQLException; import java.sql.SQLType; import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.function.Function; @@ -80,7 +81,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 +99,12 @@ public MappingJdbcConverter(RelationalMappingContext context, RelationResolver r /** * Creates a new {@link MappingJdbcConverter} given {@link MappingContext}. * - * @param context must not be {@literal null}. + * @param context must not be {@literal null}. * @param relationResolver used to fetch additional relations from the database. Must not be {@literal null}. - * @param typeFactory must not be {@literal null} + * @param typeFactory must not be {@literal null} */ public MappingJdbcConverter(RelationalMappingContext context, RelationResolver relationResolver, - CustomConversions conversions, JdbcTypeFactory typeFactory) { + CustomConversions conversions, JdbcTypeFactory typeFactory) { super(context, conversions); @@ -285,7 +286,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 +315,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 +324,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 +332,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()) { @@ -368,9 +369,16 @@ public T getPropertyValue(RelationalPersistentProperty property) { // 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); + List idPaths = getMappingContext().getIdPaths(idDefiningParentPath.getRequiredLeafEntity()); + RelationalPersistentProperty identifier = null; + Object value = null; + for (AggregatePath idPath : idPaths) { + + // TODO this hack only works for single values. + + identifier = idPath.getRequiredLeafProperty(); + value = delegate.getValue(idDefiningParentPath.append(idPath)); + } Assert.state(value != null, "Identifier value must not be null at this point"); @@ -460,7 +468,7 @@ public RelationalPropertyValueProvider withContext(ConversionContext context) { return context == this.context ? this : new ResolvingRelationalPropertyValueProvider(delegate.withContext(context), accessor, - (ResolvingConversionContext) context, identifier); + (ResolvingConversionContext) context, identifier); } } @@ -472,7 +480,7 @@ public RelationalPropertyValueProvider withContext(ConversionContext context) { * @param identifier */ private record ResolvingConversionContext(ConversionContext delegate, AggregatePath aggregatePath, - Identifier identifier) implements ConversionContext { + Identifier identifier) implements ConversionContext { @Override public S convert(Object source, TypeInformation typeHint) { diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java index 4be21ccf34e..1c913ec8105 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 @@ -15,6 +15,7 @@ */ package org.springframework.data.jdbc.core.convert; +import java.time.LocalTime; import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; @@ -37,6 +38,7 @@ import org.springframework.data.relational.core.sql.render.SqlRenderer; import org.springframework.data.util.Lazy; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -451,7 +453,7 @@ String createDeleteAllSql(@Nullable PersistentPropertyPath path) { return createDeleteByPathAndCriteria(mappingContext.getAggregatePath(path), - filterColumn -> filterColumn.isEqualTo(getBindMarker(ROOT_ID_PARAMETER))); + filterColumn -> filterColumn.isEqualTo(getBindMarker(entity.getIdColumn()))); } /** @@ -469,10 +471,27 @@ String createDeleteInByPath(PersistentPropertyPath private String createFindOneSql() { - Select select = selectBuilder().where(getIdColumn().isEqualTo(getBindMarker(ID_SQL_PARAMETER))) // - .build(); + SelectBuilder.SelectWhereAndOr select = null; - return render(select); + return render(selectBuilder().where(singleIdWhereCondition()).build()); + } + + private Condition singleIdWhereCondition() { + + Condition aggregate = null; + for (Column column : getIdColumns()) { + Comparison condition = column.isEqualTo(getBindMarker(column.getName())); + + if (aggregate == null) { + aggregate = condition; + } else { + aggregate = aggregate.and(condition); + } + } + + Assert.state(aggregate != null, "We need at least one id column"); + + return aggregate; } private String createAcquireLockById(LockMode lockMode) { @@ -632,19 +651,26 @@ Join getJoin(AggregatePath path) { private String createFindAllInListSql() { - Select select = selectBuilder().where(getIdColumn().in(getBindMarker(IDS_SQL_PARAMETER))).build(); + In condition = multiIdWhereClause(); + Select select = selectBuilder().where(condition).build(); return render(select); } - private String createExistsSql() { + private In multiIdWhereClause() { + List idColumns = getIdColumns(); + TupleExpression tuple = TupleExpression.create(idColumns); + return Conditions.in(tuple, getBindMarker(IDS_SQL_PARAMETER)); + } + + private String createExistsSql() { Table table = getTable(); Select select = StatementBuilder // .select(Functions.count(getIdColumn())) // .from(table) // - .where(getIdColumn().isEqualTo(getBindMarker(ID_SQL_PARAMETER))) // + .where(singleIdWhereCondition()) // .build(); return render(select); @@ -715,7 +741,7 @@ private UpdateBuilder.UpdateWhereAndOr createBaseUpdate() { return Update.builder() // .table(table) // .set(assignments) // - .where(getIdColumn().isEqualTo(getBindMarker(entity.getIdColumn()))); + .where(singleIdWhereCondition()); } private String createDeleteByIdSql() { @@ -738,13 +764,13 @@ private String createDeleteByIdAndVersionSql() { private DeleteBuilder.DeleteWhereAndOr createBaseDeleteById(Table table) { return Delete.builder().from(table) // - .where(getIdColumn().isEqualTo(getBindMarker(ID_SQL_PARAMETER))); + .where(singleIdWhereCondition()); } private DeleteBuilder.DeleteWhereAndOr createBaseDeleteByIdIn(Table table) { return Delete.builder().from(table) // - .where(getIdColumn().in(getBindMarker(IDS_SQL_PARAMETER))); + .where(multiIdWhereClause()); } private String createDeleteByPathAndCriteria(AggregatePath path, Function rootCondition) { @@ -804,7 +830,24 @@ private Table getTable() { } private Column getIdColumn() { - return sqlContext.getIdColumn(); + + List idPaths = mappingContext.getIdPaths(entity); + + AggregatePath idAggregatePath = idPaths.get(0);// TODO: hack for single column + + return sqlContext.getColumn(idAggregatePath); + } + + private List getIdColumns() { + + List idPaths = mappingContext.getIdPaths(entity); + + List result = new ArrayList<>(idPaths.size()); + + for (AggregatePath idPath : idPaths) { + result.add(sqlContext.getColumn(idPath)); + } + return result; } private Column getVersionColumn() { 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 94f90de501f..0a096121d8f 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlParametersFactory.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlParametersFactory.java @@ -25,7 +25,9 @@ import org.springframework.data.jdbc.support.JdbcUtil; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.PersistentPropertyPathAccessor; import org.springframework.data.relational.core.conversion.IdValueSource; +import org.springframework.data.relational.core.mapping.AggregatePath; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; @@ -78,9 +80,13 @@ 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); + for (AggregatePath idPath : context.getIdPaths(persistentEntity)) { + + Object idValue = propertyPathAccessor.getProperty(idPath.getRequiredPersistentPropertyPath()); + RelationalPersistentProperty idProperty = idPath.getRequiredLeafProperty(); + addConvertedPropertyValue(parameterSource, idProperty, idValue, idProperty.getColumnName()); + } } return parameterSource; } @@ -112,12 +118,38 @@ SqlIdentifierParameterSource forQueryById(Object id, Class domainType, Sq SqlIdentifierParameterSource parameterSource = new SqlIdentifierParameterSource(); - addConvertedPropertyValue( // - parameterSource, // - getRequiredPersistentEntity(domainType).getRequiredIdProperty(), // - id, // - name // - ); + RelationalPersistentEntity entity = getRequiredPersistentEntity(domainType); + + RelationalPersistentProperty singleIdProperty = entity.getRequiredIdProperty(); + + if (singleIdProperty.isEntity()) { + + RelationalPersistentEntity complexId = context.getPersistentEntity(singleIdProperty); + PersistentPropertyPathAccessor accessor = complexId.getPropertyPathAccessor(id); + + List idPaths = context.getIdPaths(entity); + + for (AggregatePath idPath : idPaths) { + AggregatePath idElementPath = idPath.getTail(); + Object idValue = accessor.getProperty(idElementPath.getRequiredPersistentPropertyPath()); + + addConvertedPropertyValue( // + parameterSource, // + idElementPath.getRequiredLeafProperty(), // + idValue, // + idElementPath.getColumnInfo().name() // + ); + } + + } else { + + addConvertedPropertyValue( // + parameterSource, // + singleIdProperty, // + id, // + singleIdProperty.getColumnName() // + ); + } return parameterSource; } @@ -133,9 +165,35 @@ SqlIdentifierParameterSource forQueryByIds(Iterable ids, Class domainT SqlIdentifierParameterSource parameterSource = new SqlIdentifierParameterSource(); - addConvertedPropertyValuesAsList(parameterSource, getRequiredPersistentEntity(domainType).getRequiredIdProperty(), - ids); + RelationalPersistentEntity entity = context.getPersistentEntity(domainType); + RelationalPersistentProperty singleIdProperty = entity.getRequiredIdProperty(); + + if (singleIdProperty.isEntity()) { + + RelationalPersistentEntity complexId = context.getPersistentEntity(singleIdProperty); + List idPaths = context.getIdPaths(entity); + + List parameterValues = new ArrayList<>(); + for (Object id : ids) { + + PersistentPropertyPathAccessor accessor = complexId.getPropertyPathAccessor(id); + + Object[] tuple = new Object[idPaths.size()]; + int index = 0; + for (AggregatePath idPath : idPaths) { + AggregatePath idElementPath = idPath.getTail(); + tuple[index] = accessor.getProperty(idElementPath.getRequiredPersistentPropertyPath()); + index++; + } + parameterValues.add(tuple); + } + + parameterSource.addValue(SqlGenerator.IDS_SQL_PARAMETER, parameterValues); + } else { + addConvertedPropertyValuesAsList(parameterSource, getRequiredPersistentEntity(domainType).getRequiredIdProperty(), + ids); + } return parameterSource; } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AbstractJdbcAggregateTemplateIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AbstractJdbcAggregateTemplateIntegrationTests.java index d1a085bd264..6aabeb2225d 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AbstractJdbcAggregateTemplateIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AbstractJdbcAggregateTemplateIntegrationTests.java @@ -28,7 +28,6 @@ import java.util.function.Function; import java.util.stream.IntStream; -import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; 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 00000000000..1108fa2af56 --- /dev/null +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/CompositeIdAggregateTemplateHsqlIntegrationTests.java @@ -0,0 +1,209 @@ +/* + * Copyright 2017-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.core; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.data.annotation.Id; +import org.springframework.data.jdbc.core.convert.DataAccessStrategy; +import org.springframework.data.jdbc.core.convert.JdbcConverter; +import org.springframework.data.jdbc.testing.DatabaseType; +import org.springframework.data.jdbc.testing.EnabledOnDatabase; +import org.springframework.data.jdbc.testing.IntegrationTest; +import org.springframework.data.jdbc.testing.TestConfiguration; +import org.springframework.data.relational.core.mapping.Embedded; +import org.springframework.data.relational.core.mapping.RelationalMappingContext; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; + +/** + * Integration tests for {@link JdbcAggregateTemplate} and it's handling of entities with embedded entities as keys. + * + * @author Jens Schauder + */ +@IntegrationTest +@EnabledOnDatabase(DatabaseType.HSQL) +public class CompositeIdAggregateTemplateHsqlIntegrationTests { + + @Autowired JdbcAggregateOperations template; + @Autowired private NamedParameterJdbcOperations namedParameterJdbcTemplate; + + @Test + // GH-574 + void saveAndLoadSimpleEntity() { + + SimpleEntity entity = template.insert(new SimpleEntity(new WrappedPk(23L), "alpha")); + + assertThat(entity.wrappedPk).isNotNull() // + .extracting(WrappedPk::id).isNotNull(); + + SimpleEntity reloaded = template.findById(entity.wrappedPk, SimpleEntity.class); + + assertThat(reloaded).isEqualTo(entity); + } + + @Test + // GH-574 + void saveAndLoadEntityWithList() { + + WithList entity = template + .insert(new WithList(new WrappedPk(23L), "alpha", List.of(new Child("Romulus"), new Child("Remus")))); + + assertThat(entity.wrappedPk).isNotNull() // + .extracting(WrappedPk::id).isNotNull(); + + WithList reloaded = template.findById(entity.wrappedPk, WithList.class); + + assertThat(reloaded).isEqualTo(entity); + } + + @Test // GH-574 + void saveAndLoadSimpleEntityWithEmbeddedPk() { + + SimpleEntityWithEmbeddedPk entity = template + .insert(new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "x"), "alpha")); + + SimpleEntityWithEmbeddedPk reloaded = template.findById(entity.embeddedPk, SimpleEntityWithEmbeddedPk.class); + + assertThat(reloaded).isEqualTo(entity); + } + + @Test // GH-574 + void saveAndLoadSimpleEntitiesWithEmbeddedPk() { + + List entities = (List) template + .insertAll(List.of(new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "x"), "alpha"), + new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "y"), "beta"), + new SimpleEntityWithEmbeddedPk(new EmbeddedPk(24L, "y"), "gamma"))); + + List firstTwoPks = entities.stream().limit(2).map(SimpleEntityWithEmbeddedPk::embeddedPk).toList(); + Iterable reloaded = template.findAllById(firstTwoPks, SimpleEntityWithEmbeddedPk.class); + + assertThat(reloaded).containsExactlyInAnyOrder(entities.get(0), entities.get(1)); + } + + @Test // GH-574 + void deleteSingleSimpleEntityWithEmbeddedPk() { + + List entities = (List) template + .insertAll(List.of(new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "x"), "alpha"), + new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "y"), "beta"), + new SimpleEntityWithEmbeddedPk(new EmbeddedPk(24L, "y"), "gamma"))); + + + template.delete(entities.get(1)); + + Iterable reloaded = template.findAll(SimpleEntityWithEmbeddedPk.class); + + assertThat(reloaded).containsExactlyInAnyOrder(entities.get(0), entities.get(2)); + } + + @Test // GH-574 + void deleteMultipleSimpleEntityWithEmbeddedPk() { + + List entities = (List) template + .insertAll(List.of(new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "x"), "alpha"), + new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "y"), "beta"), + new SimpleEntityWithEmbeddedPk(new EmbeddedPk(24L, "y"), "gamma"))); + + + template.deleteAll(List.of(entities.get(1), entities.get(0))); + + Iterable reloaded = template.findAll(SimpleEntityWithEmbeddedPk.class); + + assertThat(reloaded).containsExactly(entities.get(2)); + } + + @Test // GH-574 + void existsSingleSimpleEntityWithEmbeddedPk() { + + List entities = (List) template + .insertAll(List.of(new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "x"), "alpha"), + new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "y"), "beta"), + new SimpleEntityWithEmbeddedPk(new EmbeddedPk(24L, "y"), "gamma"))); + + + assertThat(template.existsById(entities.get(1).embeddedPk, SimpleEntityWithEmbeddedPk.class)).isTrue(); + assertThat(template.existsById(new EmbeddedPk(24L, "x"), SimpleEntityWithEmbeddedPk.class)).isFalse(); + + } + + @Test // GH-574 + void updateSingleSimpleEntityWithEmbeddedPk() { + + List entities = (List) template + .insertAll(List.of(new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "x"), "alpha"), + new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "y"), "beta"), + new SimpleEntityWithEmbeddedPk(new EmbeddedPk(24L, "y"), "gamma"))); + + + SimpleEntityWithEmbeddedPk updated = new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "x"), "ALPHA"); + template.save(updated); + + Iterable reloaded = template.findAll(SimpleEntityWithEmbeddedPk.class); + + assertThat(reloaded).containsExactlyInAnyOrder(updated, entities.get(1), entities.get(2)); + } + + private record WrappedPk(Long id) { + } + + private record SimpleEntity( // + @Id @Embedded(onEmpty = Embedded.OnEmpty.USE_NULL) WrappedPk wrappedPk, // + String name // + ) { + } + + private record Child(String name) { + } + + private record WithList( // + @Id @Embedded(onEmpty = Embedded.OnEmpty.USE_NULL) WrappedPk wrappedPk, // + String name, List children) { + } + + private record EmbeddedPk(Long one, String two) { + } + + private record SimpleEntityWithEmbeddedPk( // + @Id @Embedded(onEmpty = Embedded.OnEmpty.USE_NULL) EmbeddedPk embeddedPk, // + String name // + ) { + } + + @Configuration + @Import(TestConfiguration.class) + static class Config { + + @Bean + Class testClass() { + return CompositeIdAggregateTemplateHsqlIntegrationTests.class; + } + + @Bean + JdbcAggregateOperations operations(ApplicationEventPublisher publisher, RelationalMappingContext context, + DataAccessStrategy dataAccessStrategy, JdbcConverter converter) { + return new JdbcAggregateTemplate(publisher, context, converter, dataAccessStrategy); + } + } +} diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorContextBasedNamingStrategyUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorContextBasedNamingStrategyUnitTests.java index a4c30c02efa..af8909fd002 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 ea5afb8db86..bc501d80000 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 @@ -26,6 +26,7 @@ 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.relational.core.dialect.AnsiDialect; 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; @@ -41,6 +42,7 @@ * * @author Bastian Wilhelm * @author Mark Paluch + * @author Jens Schauder */ class SqlGeneratorEmbeddedUnitTests { @@ -84,6 +86,104 @@ void findOne() { }); } + @Test // GH-574 + void findOneWrappedId() { + + SqlGenerator sqlGenerator = createSqlGenerator(DummyEntityWithWrappedId.class); + + String sql = sqlGenerator.getFindOne(); + + assertSoftly(softly -> { + + softly.assertThat(sql).startsWith("SELECT") // + .contains("dummy_entity_with_wrapped_id.name AS name") // + .contains("dummy_entity_with_wrapped_id.id") // + .contains("WHERE dummy_entity_with_wrapped_id.id = :id"); + }); + } + + @Test // GH-574 + void findOneEmbeddedId() { + + SqlGenerator sqlGenerator = createSqlGenerator(DummyEntityWithEmbeddedId.class); + + String sql = sqlGenerator.getFindOne(); + + assertSoftly(softly -> { + + softly.assertThat(sql).startsWith("SELECT") // + .contains("dummy_entity_with_embedded_id.name AS name") // + .contains("dummy_entity_with_embedded_id.one") // + .contains("dummy_entity_with_embedded_id.two") // + .contains(" WHERE ") // + .contains("dummy_entity_with_embedded_id.one = :one") // + .contains("dummy_entity_with_embedded_id.two = :two"); + }); + } + + @Test // GH-574 + void deleteByIdEmbeddedId() { + + SqlGenerator sqlGenerator = createSqlGenerator(DummyEntityWithEmbeddedId.class); + + String sql = sqlGenerator.getDeleteById(); + + assertSoftly(softly -> { + + softly.assertThat(sql).startsWith("DELETE") // + .contains(" WHERE ") // + .contains("dummy_entity_with_embedded_id.one = :one") // + .contains("dummy_entity_with_embedded_id.two = :two"); + }); + } + + @Test // GH-574 + void deleteByIdInEmbeddedId() { + + SqlGenerator sqlGenerator = createSqlGenerator(DummyEntityWithEmbeddedId.class); + + String sql = sqlGenerator.getDeleteByIdIn(); + + assertSoftly(softly -> { + + softly.assertThat(sql).startsWith("DELETE") // + .contains(" WHERE ") // + .contains("(dummy_entity_with_embedded_id.one, dummy_entity_with_embedded_id.two) IN (:ids)"); + }); + } + + @Test // GH-574 + void updateWithEmbeddedId() { + + SqlGenerator sqlGenerator = createSqlGenerator(DummyEntityWithEmbeddedId.class); + + String sql = sqlGenerator.getUpdate(); + + assertSoftly(softly -> { + + softly.assertThat(sql).startsWith("UPDATE") // + .contains(" WHERE ") // + .contains("dummy_entity_with_embedded_id.one = :one") // + .contains("dummy_entity_with_embedded_id.two = :two"); + }); + } + + @Test // GH-574 + void existsByIdEmbeddedId() { + + SqlGenerator sqlGenerator = createSqlGenerator(DummyEntityWithEmbeddedId.class); + + String sql = sqlGenerator.getExists(); + + assertSoftly(softly -> { + + softly.assertThat(sql).startsWith("SELECT COUNT") // + .contains(" WHERE ") // + .contains("dummy_entity_with_embedded_id.one = :one") // + .contains("dummy_entity_with_embedded_id.two = :two"); + }); + } + @Test // DATAJDBC-111 void findAll() { final String sql = sqlGenerator.getFindAll(); @@ -109,7 +209,8 @@ void findAll() { @Test // DATAJDBC-111 void findAllInList() { - final String sql = sqlGenerator.getFindAllInList(); + + String sql = sqlGenerator.getFindAllInList(); assertSoftly(softly -> { @@ -124,12 +225,29 @@ void findAllInList() { .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)") // + .contains("WHERE (dummy_entity.id1) IN (:ids)") // .doesNotContain("JOIN") // .doesNotContain("embeddable"); }); } + @Test // GH-574 + void findAllInListEmbeddedId() { + + SqlGenerator sqlGenerator = createSqlGenerator(DummyEntityWithEmbeddedId.class); + + String sql = sqlGenerator.getFindAllInList(); + + assertSoftly(softly -> { + + softly.assertThat(sql).startsWith("SELECT") // + .contains("dummy_entity_with_embedded_id.name AS name") // + .contains("dummy_entity_with_embedded_id.one") // + .contains("dummy_entity_with_embedded_id.two") // + .contains(" WHERE (dummy_entity_with_embedded_id.one, dummy_entity_with_embedded_id.two) IN (:ids)"); + }); + } + @Test // DATAJDBC-111 void insert() { final String sql = sqlGenerator.getInsert(emptySet()); @@ -332,6 +450,29 @@ static class DummyEntity { @Embedded(onEmpty = OnEmpty.USE_NULL) CascadedEmbedded embeddable; } + record WrappedId(Long id) { + } + static class DummyEntityWithWrappedId { + + @Id + @Embedded(onEmpty = OnEmpty.USE_NULL) + WrappedId wrappedId; + + String name; + } + + record EmbeddedId(Long one, String two) { + } + + static class DummyEntityWithEmbeddedId { + + @Id + @Embedded(onEmpty = OnEmpty.USE_NULL) + EmbeddedId embeddedId; + + String name; + } + @SuppressWarnings("unused") static class CascadedEmbedded { String test; 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 d545be74e6c..055df3705b1 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 @@ -35,6 +35,7 @@ * * @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 f9b9c8b6fa7..268dd92f3d6 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java @@ -153,7 +153,7 @@ void cascadingDeleteFirstLevel() { String sql = sqlGenerator.createDeleteByPath(getPath("ref", DummyEntity.class)); - assertThat(sql).isEqualTo("DELETE FROM referenced_entity WHERE referenced_entity.dummy_entity = :rootId"); + assertThat(sql).isEqualTo("DELETE FROM referenced_entity WHERE referenced_entity.dummy_entity = :id1"); } @Test // GH-537 @@ -170,7 +170,7 @@ void cascadingDeleteByPathSecondLevel() { String sql = sqlGenerator.createDeleteByPath(getPath("ref.further", DummyEntity.class)); assertThat(sql).isEqualTo( - "DELETE FROM second_level_referenced_entity WHERE second_level_referenced_entity.referenced_entity IN (SELECT referenced_entity.x_l1id FROM referenced_entity WHERE referenced_entity.dummy_entity = :rootId)"); + "DELETE FROM second_level_referenced_entity WHERE second_level_referenced_entity.referenced_entity IN (SELECT referenced_entity.x_l1id FROM referenced_entity WHERE referenced_entity.dummy_entity = :id1)"); } @Test // GH-537 @@ -220,7 +220,7 @@ void deleteMapByPath() { String sql = sqlGenerator.createDeleteByPath(getPath("mappedElements", DummyEntity.class)); - assertThat(sql).isEqualTo("DELETE FROM element WHERE element.dummy_entity = :rootId"); + assertThat(sql).isEqualTo("DELETE FROM element WHERE element.dummy_entity = :id1"); } @Test // DATAJDBC-101 @@ -639,7 +639,7 @@ void readOnlyPropertyIncludedIntoQuery_when_generateFindAllInListSql() { + "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 IN (:ids)" // + + "WHERE (entity_with_read_only_property.x_id) IN (:ids)" // ); } @@ -654,7 +654,7 @@ void readOnlyPropertyIncludedIntoQuery_when_generateFindOneSql() { + "entity_with_read_only_property.x_name AS x_name, " // + "entity_with_read_only_property.x_read_only_value AS x_read_only_value " // + "FROM entity_with_read_only_property " // - + "WHERE entity_with_read_only_property.x_id = :id" // + + "WHERE entity_with_read_only_property.x_id = :x_id" // ); } @@ -673,7 +673,7 @@ void deletingLongChain() { "WHERE chain2.chain3 IN (" + // "SELECT chain3.x_three " + // "FROM chain3 " + // - "WHERE chain3.chain4 = :rootId" + // + "WHERE chain3.chain4 = :x_four" + // ")))"); } @@ -682,7 +682,7 @@ void deletingLongChainNoId() { assertThat(createSqlGenerator(NoIdChain4.class) .createDeleteByPath(getPath("chain3.chain2.chain1.chain0", NoIdChain4.class))) // - .isEqualTo("DELETE FROM no_id_chain0 WHERE no_id_chain0.no_id_chain4 = :rootId"); + .isEqualTo("DELETE FROM no_id_chain0 WHERE no_id_chain0.no_id_chain4 = :x_four"); } @Test // DATAJDBC-359 @@ -698,7 +698,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" // + "))"); } 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 87% 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 7fd0f6e9a8b..0e169142877 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; @@ -35,6 +36,7 @@ import org.springframework.data.relational.core.conversion.IdValueSource; import org.springframework.data.relational.core.dialect.AnsiDialect; 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; @@ -44,7 +46,7 @@ * * @author Chirag Tailor */ -class SqlParametersFactoryTest { +class SqlParametersFactoryUnitTests { RelationalMappingContext context = new JdbcMappingContext(); RelationResolver relationResolver = mock(RelationResolver.class); @@ -88,7 +90,7 @@ public void considersConfiguredWriteConverterForIdValueObjectsWhichReferencedInO } @Test - // DATAJDBC-146 + // DATAJDBC-146 void identifiersGetAddedAsParameters() { long id = 4711L; @@ -103,7 +105,7 @@ void identifiersGetAddedAsParameters() { } @Test - // DATAJDBC-146 + // DATAJDBC-146 void additionalIdentifierForIdDoesNotLeadToDuplicateParameters() { long id = 4711L; @@ -116,7 +118,7 @@ void additionalIdentifierForIdDoesNotLeadToDuplicateParameters() { } @Test - // DATAJDBC-235 + // DATAJDBC-235 void considersConfiguredWriteConverter() { SqlParametersFactory sqlParametersFactory = createSqlParametersFactoryWithConverters( @@ -131,7 +133,7 @@ void considersConfiguredWriteConverter() { } @Test - // DATAJDBC-412 + // DATAJDBC-412 void considersConfiguredWriteConverterForIdValueObjects_onWrite() { SqlParametersFactory sqlParametersFactory = createSqlParametersFactoryWithConverters( @@ -149,7 +151,7 @@ void considersConfiguredWriteConverterForIdValueObjects_onWrite() { } @Test - // GH-1405 + // GH-1405 void parameterNamesGetSanitized() { WithIllegalCharacters entity = new WithIllegalCharacters(23L, "aValue"); @@ -164,6 +166,23 @@ 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 { @@ -177,7 +196,8 @@ public String convert(IdValue source) { private static class WithValueObjectId { - @Id private final IdValue id; + @Id + private final IdValue id; String value; private WithValueObjectId(IdValue id) { @@ -255,7 +275,8 @@ public Boolean convert(String source) { private static class EntityWithBoolean { - @Id Long id; + @Id + Long id; boolean flag; public EntityWithBoolean(Long id, boolean flag) { @@ -267,7 +288,8 @@ public EntityWithBoolean(Long id, boolean flag) { // DATAJDBC-349 private static class DummyEntityRoot { - @Id private final IdValue id; + @Id + private final IdValue id; List dummyEntities = new ArrayList<>(); public DummyEntityRoot(IdValue id) { @@ -277,7 +299,8 @@ public DummyEntityRoot(IdValue id) { private static class DummyEntity { - @Id private final Long id; + @Id + private final Long id; public DummyEntity(Long id) { this.id = id; @@ -287,9 +310,11 @@ public DummyEntity(Long id) { private static class WithIllegalCharacters { @Column("i.d") - @Id Long id; + @Id + Long id; - @Column("val&ue") String value; + @Column("val&ue") + String value; public WithIllegalCharacters(Long id, String value) { this.id = id; @@ -301,6 +326,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/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 00000000000..a2ead94c72b --- /dev/null +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/CompositeIdAggregateTemplateHsqlIntegrationTests-hsql.sql @@ -0,0 +1,26 @@ +CREATE TABLE SIMPLE_ENTITY +( + ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, + NAME VARCHAR(100) +); + +CREATE TABLE WITH_LIST +( + ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, + NAME VARCHAR(100) +); + +CREATE TABLE CHILD +( + WITH_LIST BIGINT NOT NULL REFERENCES WITH_LIST (ID), + WITH_LIST_KEY INT NOT NULL, + NAME VARCHAR(100) +); + +CREATE TABLE SIMPLE_ENTITY_WITH_EMBEDDED_PK +( + 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 21e80a6c989..d6dde771495 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 @@ -419,4 +419,9 @@ CREATE TABLE THIRD SEC BIGINT NOT NULL, NAME VARCHAR(20) NOT NULL, FOREIGN KEY (SEC) REFERENCES SEC (ID) -); \ No newline at end of file +); +create table SINGLE_EMBEDDED_ID_ENTITY +( + ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, + NAME VARCHAR(100) +) \ No newline at end of file diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/AggregatePath.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/AggregatePath.java index 244ea452491..02ebb8a84b2 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 @@ -52,6 +52,16 @@ public interface AggregatePath extends Iterable { */ AggregatePath append(RelationalPersistentProperty property); + /** + * Creates a new path by extending the current path by the path passed as an argument. + * + * @param path must not be {@literal null}. + * @return Guaranteed to be not {@literal null}. + * + * @since 3.4 + */ + AggregatePath append(AggregatePath path); + /** * @return {@literal true} if this is a root path for the underlying type. */ @@ -227,6 +237,9 @@ default Stream stream() { */ AggregatePath getIdDefiningParentPath(); + @Nullable + AggregatePath getTail(); + record TableInfo( /* 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 3808b2ba3cf..fa7572fc59b 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 @@ -19,7 +19,9 @@ import java.util.NoSuchElementException; import java.util.Objects; +import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentPropertyPath; +import org.springframework.data.relational.core.conversion.RootAggregateChange; import org.springframework.data.util.Lazy; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -97,6 +99,20 @@ public AggregatePath append(RelationalPersistentProperty property) { return nestedCache.get(property); } + @Override + public AggregatePath append(AggregatePath path) { + + if (path.isRoot()) { + return this; + } + + RelationalPersistentProperty baseProperty = path.getRequiredBaseProperty(); + AggregatePath appended = append(baseProperty); + AggregatePath tail = path.getTail(); + return tail == null ? appended : appended.append(tail); + + } + private AggregatePath doGetAggegatePath(RelationalPersistentProperty property) { PersistentPropertyPath newPath = isRoot() // @@ -194,6 +210,25 @@ public AggregatePath getIdDefiningParentPath() { return AggregatePathTraversal.getIdDefiningPath(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; + } + /** * Finds and returns the longest path with ich identical or an ancestor to the current path and maps directly to a * table. 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 a5e7e4c83df..7bf86fab15c 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 @@ -15,6 +15,9 @@ */ package org.springframework.data.relational.core.mapping; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -138,7 +141,7 @@ protected RelationalPersistentEntity createPersistentEntity(TypeInformati @Override protected RelationalPersistentProperty createPersistentProperty(Property property, - RelationalPersistentEntity owner, SimpleTypeHolder simpleTypeHolder) { + RelationalPersistentEntity owner, SimpleTypeHolder simpleTypeHolder) { BasicRelationalPersistentProperty persistentProperty = new BasicRelationalPersistentProperty(property, owner, simpleTypeHolder, this.namingStrategy); @@ -148,9 +151,9 @@ protected RelationalPersistentProperty createPersistentProperty(Property propert } /** - * @since 3.2 * @return iff single query loading is enabled. * @see #setSingleQueryLoadingEnabled(boolean) + * @since 3.2 */ public boolean isSingleQueryLoadingEnabled() { return singleQueryLoadingEnabled; @@ -161,8 +164,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; @@ -210,8 +213,45 @@ public AggregatePath getAggregatePath(RelationalPersistentEntity type) { return aggregatePath; } + public List getIdPaths(RelationalPersistentEntity entity) { + + RelationalPersistentProperty idProperty = entity.getIdProperty(); + + if (idProperty == null) { + return Collections.emptyList(); + } else { + PersistentPropertyPath idPath = getPersistentPropertyPath(idProperty.getName(), entity.getTypeInformation()); + + if (!idProperty.isEmbedded()) { + + return List.of(getAggregatePath(idPath)); + } else { + + return gatherSubPaths(getAggregatePath(idPath)); + + } + } + } + + private List gatherSubPaths(AggregatePath path) { + + if (path.getRequiredLeafProperty().isEntity()) { + RelationalPersistentEntity leafEntity = path.getRequiredLeafEntity(); + + List result = new ArrayList<>(); + + leafEntity.doWithProperties( (RelationalPersistentProperty p) -> result.addAll(gatherSubPaths(path.append(p)))); + + return result; + + } else { + return List.of(path); + } + } + + private record AggregatePathCacheKey(RelationalPersistentEntity root, - @Nullable PersistentPropertyPath path) { + @Nullable PersistentPropertyPath path) { /** * Create a new AggregatePathCacheKey for a root entity. 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 00000000000..b4fd9c847a2 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/TupleExpression.java @@ -0,0 +1,44 @@ +package org.springframework.data.relational.core.sql; + +import static java.util.stream.Collectors.*; + +import java.util.List; + +/** + * A tuple as used in conditions like + * + *
+ *   WHERE (one, two) IN (select x, y from some_table)
+ * 
+ * + * @author Jens Schauder + * @since 3.4 + */ +public class TupleExpression extends AbstractSegment implements Expression { + + private final List expressions; + + private static Segment[] children(List expressions) { + return expressions.toArray(new Segment[0]); + } + + private TupleExpression(List expressions) { + + super(children(expressions)); + + this.expressions = expressions; + } + + public static TupleExpression create(Expression... expressions) { + return new TupleExpression(List.of(expressions)); + } + + public static TupleExpression create(List expressions) { + return new TupleExpression(expressions); + } + + @Override + public String toString() { + return "(" + expressions.stream().map(Expression::toString).collect(joining(", ")) + ")"; + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ExpressionVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ExpressionVisitor.java index 65843cd3400..c8b83224346 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 @@ -78,6 +78,13 @@ Delegation enterMatched(Expression segment) { return Delegation.delegateTo(visitor); } + if (segment instanceof TupleExpression) { + + TupleVisitor visitor = new TupleVisitor(context); + partRenderer = visitor; + return Delegation.delegateTo(visitor); + } + if (segment instanceof AnalyticFunction) { AnalyticFunctionVisitor visitor = new AnalyticFunctionVisitor(context); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/TupleVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/TupleVisitor.java new file mode 100644 index 00000000000..e6391586b6a --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/TupleVisitor.java @@ -0,0 +1,71 @@ +/* + * Copyright 2019-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.relational.core.sql.render; + +import org.springframework.data.relational.core.sql.SimpleFunction; +import org.springframework.data.relational.core.sql.TupleExpression; +import org.springframework.data.relational.core.sql.Visitable; + +/** + * @author Jens Schauder + * @since 3.4 + */ +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/test/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentEntityUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentEntityUnitTests.java index 83f56e80121..53d21cdda30 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentEntityUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentEntityUnitTests.java @@ -18,6 +18,7 @@ import static org.assertj.core.api.Assertions.*; import static org.springframework.data.relational.core.sql.SqlIdentifier.*; +import java.util.List; import java.util.Map; import org.junit.jupiter.api.Test; 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 837cec98328..cfa02c24fa1 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 @@ -41,7 +41,8 @@ class DefaultAggregatePathUnitTests { private RelationalPersistentEntity entity = context.getRequiredPersistentEntity(DummyEntity.class); - @Test // GH-1525 + @Test + // GH-1525 void isNotRootForNonRootPath() { AggregatePath path = context.getAggregatePath(context.getPersistentPropertyPath("entityId", DummyEntity.class)); @@ -49,7 +50,8 @@ void isNotRootForNonRootPath() { assertThat(path.isRoot()).isFalse(); } - @Test // GH-1525 + @Test + // GH-1525 void isRootForRootPath() { AggregatePath path = context.getAggregatePath(entity); @@ -57,7 +59,8 @@ void isRootForRootPath() { assertThat(path.isRoot()).isTrue(); } - @Test // GH-1525 + @Test + // GH-1525 void getParentPath() { assertSoftly(softly -> { @@ -70,7 +73,8 @@ void getParentPath() { }); } - @Test // GH-1525 + @Test + // GH-1525 void getRequiredLeafEntity() { assertSoftly(softly -> { @@ -89,7 +93,8 @@ void getRequiredLeafEntity() { }); } - @Test // GH-1525 + @Test + // GH-1525 void idDefiningPath() { assertSoftly(softly -> { @@ -105,7 +110,8 @@ void idDefiningPath() { }); } - @Test // GH-1525 + @Test + // GH-1525 void getRequiredIdProperty() { assertSoftly(softly -> { @@ -116,7 +122,8 @@ void getRequiredIdProperty() { }); } - @Test // GH-1525 + @Test + // GH-1525 void reverseColumnName() { assertSoftly(softly -> { @@ -140,7 +147,8 @@ void reverseColumnName() { }); } - @Test // GH-1525 + @Test + // GH-1525 void getQualifierColumn() { assertSoftly(softly -> { @@ -154,7 +162,8 @@ void getQualifierColumn() { }); } - @Test // GH-1525 + @Test + // GH-1525 void getQualifierColumnType() { assertSoftly(softly -> { @@ -167,7 +176,8 @@ void getQualifierColumnType() { }); } - @Test // GH-1525 + @Test + // GH-1525 void extendBy() { assertSoftly(softly -> { @@ -178,7 +188,8 @@ void extendBy() { }); } - @Test // GH-1525 + @Test + // GH-1525 void isWritable() { assertSoftly(softly -> { @@ -193,7 +204,8 @@ void isWritable() { }); } - @Test // GH-1525 + @Test + // GH-1525 void isEmbedded() { assertSoftly(softly -> { @@ -205,7 +217,8 @@ void isEmbedded() { }); } - @Test // GH-1525 + @Test + // GH-1525 void isEntity() { assertSoftly(softly -> { @@ -220,7 +233,8 @@ void isEntity() { }); } - @Test // GH-1525 + @Test + // GH-1525 void isMultiValued() { assertSoftly(softly -> { @@ -229,16 +243,17 @@ void isMultiValued() { softly.assertThat(path("second").isMultiValued()).isFalse(); softly.assertThat(path("second.third2").isMultiValued()).isFalse(); softly.assertThat(path("secondList.third2").isMultiValued()).isTrue(); // this seems wrong as third2 is an - // embedded path into Second, held by - // List (so the parent is - // multi-valued but not third2). + // embedded path into Second, held by + // List (so the parent is + // multi-valued but not third2). // TODO: This test fails because MultiValued considers parents. // softly.assertThat(path("secondList.third.value").isMultiValued()).isFalse(); softly.assertThat(path("secondList").isMultiValued()).isTrue(); }); } - @Test // GH-1525 + @Test + // GH-1525 void isQualified() { assertSoftly(softly -> { @@ -251,7 +266,8 @@ void isQualified() { }); } - @Test // GH-1525 + @Test + // GH-1525 void isMap() { assertSoftly(softly -> { @@ -266,7 +282,8 @@ void isMap() { }); } - @Test // GH-1525 + @Test + // GH-1525 void isCollectionLike() { assertSoftly(softly -> { @@ -281,7 +298,8 @@ void isCollectionLike() { }); } - @Test // GH-1525 + @Test + // GH-1525 void isOrdered() { assertSoftly(softly -> { @@ -296,7 +314,8 @@ void isOrdered() { }); } - @Test // GH-1525 + @Test + // GH-1525 void getTableAlias() { assertSoftly(softly -> { @@ -306,13 +325,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")); @@ -321,7 +340,8 @@ void getTableAlias() { }); } - @Test // GH-1525 + @Test + // GH-1525 void getTableName() { assertSoftly(softly -> { @@ -337,7 +357,8 @@ void getTableName() { }); } - @Test // GH-1525 + @Test + // GH-1525 void getColumnName() { assertSoftly(softly -> { @@ -351,7 +372,8 @@ void getColumnName() { }); } - @Test // GH-1525 + @Test + // GH-1525 void getColumnAlias() { assertSoftly(softly -> { @@ -367,7 +389,8 @@ void getColumnAlias() { }); } - @Test // GH-1525 + @Test + // GH-1525 void getReverseColumnAlias() { assertSoftly(softly -> { @@ -385,7 +408,8 @@ void getReverseColumnAlias() { }); } - @Test // GH-1525 + @Test + // GH-1525 void getRequiredLeafProperty() { assertSoftly(softly -> { @@ -401,7 +425,8 @@ void getRequiredLeafProperty() { }); } - @Test // GH-1525 + @Test + // GH-1525 void getBaseProperty() { assertSoftly(softly -> { @@ -416,7 +441,8 @@ void getBaseProperty() { }); } - @Test // GH-1525 + @Test + // GH-1525 void getIdColumnName() { assertSoftly(softly -> { @@ -430,7 +456,8 @@ void getIdColumnName() { }); } - @Test // GH-1525 + @Test + // GH-1525 void toDotPath() { assertSoftly(softly -> { @@ -440,7 +467,8 @@ void toDotPath() { }); } - @Test // GH-1525 + @Test + // GH-1525 void getRequiredPersistentPropertyPath() { assertSoftly(softly -> { @@ -452,7 +480,8 @@ void getRequiredPersistentPropertyPath() { }); } - @Test // GH-1525 + @Test + // GH-1525 void getEffectiveIdColumnName() { assertSoftly(softly -> { @@ -466,17 +495,46 @@ void getEffectiveIdColumnName() { }); } - @Test // GH-1525 + @Test + // GH-1525 void getLength() { - assertThat(path().getLength()).isEqualTo(1); - assertThat(path().stream().collect(Collectors.toList())).hasSize(1); + assertSoftly(softly -> { + softly.assertThat(path().getLength()).isEqualTo(1); + softly.assertThat(path().stream().collect(Collectors.toList())).hasSize(1); + + 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-574 + void getTail() { - assertThat(path("second.third2").getLength()).isEqualTo(3); - assertThat(path("second.third2").stream().collect(Collectors.toList())).hasSize(3); + assertSoftly(softly -> { - assertThat(path("withId.second.third").getLength()).isEqualTo(4); - assertThat(path("withId.second.third2.value").getLength()).isEqualTo(5); + softly.assertThat(path().getTail()).isEqualTo(null); + softly.assertThat(path("second").getTail()).isEqualTo(null); + softly.assertThat(path("second.third").getTail().toDotPath()).isEqualTo("third"); + softly.assertThat(path("second.third.value").getTail().toDotPath()).isEqualTo("third.value"); + }); + } + + @Test // GH-74 + void append() { + + + assertSoftly(softly -> { + + softly.assertThat(path("second").append(path()).toDotPath()).isEqualTo("second"); + softly.assertThat(path().append(path("second")).toDotPath()).isEqualTo("second"); + softly.assertThat(path().append(path("second.third")).toDotPath()).isEqualTo("second.third"); + AggregatePath value = path("second.third.value").getTail().getTail(); + softly.assertThat(path("second.third").append(value).toDotPath()).isEqualTo("second.third.value"); + }); } private AggregatePath path() { 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 14316048e41..e3856dc3b99 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 @@ -25,6 +25,7 @@ import org.junit.jupiter.api.Test; import org.springframework.data.annotation.Id; import org.springframework.data.mapping.PersistentPropertyPath; +import org.springframework.data.mapping.PersistentPropertyPaths; import org.springframework.data.mapping.model.SimpleTypeHolder; import org.springframework.data.relational.core.sql.SqlIdentifier; @@ -120,6 +121,33 @@ void aggregatePathsOfBasePropertyForDifferentInheritedEntitiesAreDifferent() { assertThat(aggregatePath1).isNotEqualTo(aggregatePath2); } + @Test // GH-574 + void entityWithoutIdHasNoIdPath() { + + RelationalPersistentEntity entity = context.getRequiredPersistentEntity(Parent.class); + List idPaths = context.getIdPaths(entity); + + assertThat(idPaths).isEmpty(); + } + + @Test // GH-574 + void entityWithoutSimpleIdHasSingleIdPath() { + + RelationalPersistentEntity entity = context.getRequiredPersistentEntity(EntityWithUuid.class); + List idPaths = context.getIdPaths(entity); + + assertThat(idPaths).containsExactly(context.getAggregatePath(context.getPersistentPropertyPath("uuid", EntityWithUuid.class))); + } + + @Test // GH-574 + void entityWithEmbeddedIdHasMultipleIdPaths() { + + RelationalPersistentEntity entity = context.getRequiredPersistentEntity(WithEmbeddedId.class); + List idPaths = context.getIdPaths(entity); + + assertThat(idPaths).containsExactlyInAnyOrder(context.getAggregatePath(context.getPersistentPropertyPath("id.a", WithEmbeddedId.class)), context.getAggregatePath(context.getPersistentPropertyPath("id.b", WithEmbeddedId.class))); + } + static class EntityWithUuid { @Id UUID uuid; } @@ -128,6 +156,13 @@ 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 +179,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/render/SelectRendererUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/SelectRendererUnitTests.java index 4bc4d0c7979..f2081833edd 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 @@ -689,7 +689,7 @@ void asteriskOfAliasedTableUsesAlias() { assertThat(rendered).isEqualTo("SELECT e.*, e.id FROM employee e"); } - @Test + @Test // GH-1844 void rendersCaseExpression() { Table table = SQL.table("table"); @@ -707,6 +707,24 @@ void rendersCaseExpression() { 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 the rendering of analytic functions. */