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 d796b95b6d..68b15c7442 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-relational-parent - 3.5.0-SNAPSHOT + 4.0.0-1737-nullable-embedded-with-collection-574-composite-id-SNAPSHOT pom Spring Data Relational Parent @@ -15,12 +15,12 @@ org.springframework.data.build spring-data-parent - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT spring-data-jdbc - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT 4.21.1 reuseReports diff --git a/spring-data-jdbc-distribution/pom.xml b/spring-data-jdbc-distribution/pom.xml index 9c02f50608..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 - 3.5.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 87d4f9704a..9f7d6c308e 100644 --- a/spring-data-jdbc/pom.xml +++ b/spring-data-jdbc/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-jdbc - 3.5.0-SNAPSHOT + 4.0.0-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 - 3.5.0-SNAPSHOT + 4.0.0-1737-nullable-embedded-with-collection-574-composite-id-SNAPSHOT diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/AggregateChangeExecutor.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/AggregateChangeExecutor.java index 1de697ad09..45b139b7ab 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/AggregateChangeExecutor.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/AggregateChangeExecutor.java @@ -15,22 +15,21 @@ */ package org.springframework.data.jdbc.core; -import org.springframework.dao.OptimisticLockingFailureException; +import java.util.List; + import org.springframework.data.jdbc.core.convert.DataAccessStrategy; import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.relational.core.conversion.AggregateChange; import org.springframework.data.relational.core.conversion.DbAction; -import org.springframework.data.relational.core.conversion.DbActionExecutionException; import org.springframework.data.relational.core.conversion.MutableAggregateChange; -import java.util.List; - /** * Executes an {@link MutableAggregateChange}. * * @author Jens Schauder * @author Myeonghyeon Lee * @author Chirag Tailor + * @author Mikhail Polivakha * @since 2.0 */ class AggregateChangeExecutor { @@ -80,42 +79,34 @@ void executeDelete(AggregateChange aggregateChange) { private void execute(DbAction action, JdbcAggregateChangeExecutionContext executionContext) { - try { - if (action instanceof DbAction.InsertRoot insertRoot) { - executionContext.executeInsertRoot(insertRoot); - } else if (action instanceof DbAction.BatchInsertRoot batchInsertRoot) { - executionContext.executeBatchInsertRoot(batchInsertRoot); - } else if (action instanceof DbAction.Insert insert) { - executionContext.executeInsert(insert); - } else if (action instanceof DbAction.BatchInsert batchInsert) { - executionContext.executeBatchInsert(batchInsert); - } else if (action instanceof DbAction.UpdateRoot updateRoot) { - executionContext.executeUpdateRoot(updateRoot); - } else if (action instanceof DbAction.Delete delete) { - executionContext.executeDelete(delete); - } else if (action instanceof DbAction.BatchDelete batchDelete) { - executionContext.executeBatchDelete(batchDelete); - } else if (action instanceof DbAction.DeleteAll deleteAll) { - executionContext.executeDeleteAll(deleteAll); - } else if (action instanceof DbAction.DeleteRoot deleteRoot) { - executionContext.executeDeleteRoot(deleteRoot); - } else if (action instanceof DbAction.BatchDeleteRoot batchDeleteRoot) { - executionContext.executeBatchDeleteRoot(batchDeleteRoot); - } else if (action instanceof DbAction.DeleteAllRoot deleteAllRoot) { - executionContext.executeDeleteAllRoot(deleteAllRoot); - } else if (action instanceof DbAction.AcquireLockRoot acquireLockRoot) { - executionContext.executeAcquireLock(acquireLockRoot); - } else if (action instanceof DbAction.AcquireLockAllRoot acquireLockAllRoot) { - executionContext.executeAcquireLockAllRoot(acquireLockAllRoot); - } else { - throw new RuntimeException("unexpected action"); - } - } catch (Exception e) { - - if (e instanceof OptimisticLockingFailureException) { - throw e; - } - throw new DbActionExecutionException(action, e); + if (action instanceof DbAction.InsertRoot insertRoot) { + executionContext.executeInsertRoot(insertRoot); + } else if (action instanceof DbAction.BatchInsertRoot batchInsertRoot) { + executionContext.executeBatchInsertRoot(batchInsertRoot); + } else if (action instanceof DbAction.Insert insert) { + executionContext.executeInsert(insert); + } else if (action instanceof DbAction.BatchInsert batchInsert) { + executionContext.executeBatchInsert(batchInsert); + } else if (action instanceof DbAction.UpdateRoot updateRoot) { + executionContext.executeUpdateRoot(updateRoot); + } else if (action instanceof DbAction.Delete delete) { + executionContext.executeDelete(delete); + } else if (action instanceof DbAction.BatchDelete batchDelete) { + executionContext.executeBatchDelete(batchDelete); + } else if (action instanceof DbAction.DeleteAll deleteAll) { + executionContext.executeDeleteAll(deleteAll); + } else if (action instanceof DbAction.DeleteRoot deleteRoot) { + executionContext.executeDeleteRoot(deleteRoot); + } else if (action instanceof DbAction.BatchDeleteRoot batchDeleteRoot) { + executionContext.executeBatchDeleteRoot(batchDeleteRoot); + } else if (action instanceof DbAction.DeleteAllRoot deleteAllRoot) { + executionContext.executeDeleteAllRoot(deleteAllRoot); + } else if (action instanceof DbAction.AcquireLockRoot acquireLockRoot) { + executionContext.executeAcquireLock(acquireLockRoot); + } else if (action instanceof DbAction.AcquireLockAllRoot acquireLockAllRoot) { + executionContext.executeAcquireLockAllRoot(acquireLockAllRoot); + } else { + throw new RuntimeException("unexpected action"); } } } diff --git a/spring-data-jdbc/src/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..57dc1ff487 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutionContext.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutionContext.java @@ -17,6 +17,7 @@ import java.util.*; import java.util.function.BiConsumer; +import java.util.function.Function; import java.util.stream.Collectors; import org.springframework.dao.IncorrectUpdateSemanticsDataAccessException; @@ -176,7 +177,8 @@ private Identifier getParentKeys(DbAction.WithDependingOn action, JdbcConvert Object id = getParentId(action); JdbcIdentifierBuilder identifier = JdbcIdentifierBuilder // - .forBackReferences(converter, context.getAggregatePath(action.getPropertyPath()), id); + .forBackReferences(converter, context.getAggregatePath(action.getPropertyPath()), + getValueProvider(id, context.getAggregatePath(action.getPropertyPath()), converter)); for (Map.Entry, Object> qualifier : action.getQualifiers() .entrySet()) { @@ -186,6 +188,22 @@ private Identifier getParentKeys(DbAction.WithDependingOn action, JdbcConvert return identifier.build(); } + static Function getValueProvider(Object idValue, AggregatePath path, JdbcConverter converter) { + + RelationalPersistentEntity entity = converter.getMappingContext() + .getPersistentEntity(path.getIdDefiningParentPath().getRequiredIdProperty().getType()); + + Function valueProvider = ap -> { + if (entity == null) { + return idValue; + } else { + PersistentPropertyPathAccessor propertyPathAccessor = entity.getPropertyPathAccessor(idValue); + return propertyPathAccessor.getProperty(ap.getRequiredPersistentPropertyPath()); + } + }; + return valueProvider; + } + private Object getParentId(DbAction.WithDependingOn action) { DbAction.WithEntity idOwningAction = getIdOwningAction(action, @@ -267,12 +285,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 +375,8 @@ private void updateWithVersion(DbAction.UpdateRoot update) { */ private static class StagedValues { - static final List> aggregators = Arrays.asList(SetAggregator.INSTANCE, MapAggregator.INSTANCE, - ListAggregator.INSTANCE, SingleElementAggregator.INSTANCE); + static final List> aggregators = Arrays.asList(SetAggregator.INSTANCE, + MapAggregator.INSTANCE, ListAggregator.INSTANCE, SingleElementAggregator.INSTANCE); Map> values = new HashMap<>(); diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java index c638a3e763..ac8cf92838 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/DefaultDataAccessStrategy.java @@ -117,6 +117,7 @@ public Object insert(T instance, Class domainType, Identifier identifier, public Object[] insert(List> insertSubjects, Class domainType, IdValueSource idValueSource) { Assert.notEmpty(insertSubjects, "Batch insert must contain at least one InsertSubject"); + SqlIdentifierParameterSource[] sqlParameterSources = insertSubjects.stream() .map(insertSubject -> sqlParametersFactory.forInsert( // insertSubject.getInstance(), // @@ -167,7 +168,7 @@ public boolean updateWithVersion(S instance, Class domainType, Number pre public void delete(Object id, Class domainType) { String deleteByIdSql = sql(domainType).getDeleteById(); - SqlParameterSource parameter = sqlParametersFactory.forQueryById(id, domainType, ID_SQL_PARAMETER); + SqlParameterSource parameter = sqlParametersFactory.forQueryById(id, domainType); operations.update(deleteByIdSql, parameter); } @@ -188,7 +189,7 @@ public void deleteWithVersion(Object id, Class domainType, Number previou RelationalPersistentEntity persistentEntity = getRequiredPersistentEntity(domainType); - SqlIdentifierParameterSource parameterSource = sqlParametersFactory.forQueryById(id, domainType, ID_SQL_PARAMETER); + SqlIdentifierParameterSource parameterSource = sqlParametersFactory.forQueryById(id, domainType); parameterSource.addValue(VERSION_SQL_PARAMETER, previousVersion); int affectedRows = operations.update(sql(domainType).getDeleteByIdAndVersion(), parameterSource); @@ -208,8 +209,7 @@ public void delete(Object rootId, PersistentPropertyPath prope public void acquireLockById(Object id, LockMode lockMode, Class domainType) { String acquireLockByIdSql = sql(domainType).getAcquireLockById(lockMode); - SqlIdentifierParameterSource parameter = sqlParametersFactory.forQueryById(id, domainType, ID_SQL_PARAMETER); + SqlIdentifierParameterSource parameter = sqlParametersFactory.forQueryById(id, domainType); operations.query(acquireLockByIdSql, parameter, ResultSet::next); } @@ -269,7 +269,7 @@ public long count(Class domainType) { public T findById(Object id, Class domainType) { String findOneSql = sql(domainType).getFindOne(); - SqlIdentifierParameterSource parameter = sqlParametersFactory.forQueryById(id, domainType, ID_SQL_PARAMETER); + SqlIdentifierParameterSource parameter = sqlParametersFactory.forQueryById(id, domainType); try { return operations.queryForObject(findOneSql, parameter, getEntityRowMapper(domainType)); @@ -355,7 +355,7 @@ public Object mapRow(ResultSet rs, int rowNum) throws SQLException { public boolean existsById(Object id, Class domainType) { String existsSql = sql(domainType).getExists(); - SqlParameterSource parameter = sqlParametersFactory.forQueryById(id, domainType, ID_SQL_PARAMETER); + SqlParameterSource parameter = sqlParametersFactory.forQueryById(id, domainType); Boolean result = operations.queryForObject(existsSql, parameter, Boolean.class); Assert.state(result != null, "The result of an exists query must not be null"); diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/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/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..b1d74f1876 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverter.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverter.java @@ -80,7 +80,7 @@ public class MappingJdbcConverter extends MappingRelationalConverter implements * {@link #MappingJdbcConverter(RelationalMappingContext, RelationResolver, CustomConversions, JdbcTypeFactory)} * (MappingContext, RelationResolver, JdbcTypeFactory)} to convert arrays and large objects into JDBC-specific types. * - * @param context must not be {@literal null}. + * @param context must not be {@literal null}. * @param relationResolver used to fetch additional relations from the database. Must not be {@literal null}. */ public MappingJdbcConverter(RelationalMappingContext context, RelationResolver relationResolver) { @@ -98,12 +98,12 @@ public MappingJdbcConverter(RelationalMappingContext context, RelationResolver r /** * Creates a new {@link MappingJdbcConverter} given {@link MappingContext}. * - * @param context must not be {@literal null}. + * @param context must not be {@literal null}. * @param relationResolver used to fetch additional relations from the database. Must not be {@literal null}. - * @param typeFactory must not be {@literal null} + * @param typeFactory must not be {@literal null} */ public MappingJdbcConverter(RelationalMappingContext context, RelationResolver relationResolver, - CustomConversions conversions, JdbcTypeFactory typeFactory) { + CustomConversions conversions, JdbcTypeFactory typeFactory) { super(context, conversions); @@ -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,10 +446,26 @@ public RelationalPropertyValueProvider withContext(ConversionContext context) { return context == this.context ? this : new ResolvingRelationalPropertyValueProvider(delegate.withContext(context), accessor, - (ResolvingConversionContext) context, identifier); + (ResolvingConversionContext) context, identifier); } } + private static Function getWrappedValueProvider(Function valueProvider, + AggregatePath aggregatePath) { + + AggregatePath idDefiningParentPath = aggregatePath.getIdDefiningParentPath(); + + if (!idDefiningParentPath.hasIdProperty()) { + return ap -> { + throw new IllegalStateException("This should never happen"); + }; + } + + RelationalPersistentProperty idProperty = idDefiningParentPath.getRequiredIdProperty(); + AggregatePath idPath = idProperty.isEntity() ? idDefiningParentPath.append(idProperty) : idDefiningParentPath; + return ap -> valueProvider.apply(idPath.append(ap)); + } + /** * Marker object to indicate that the property value provider should resolve relations. * @@ -472,7 +474,7 @@ public RelationalPropertyValueProvider withContext(ConversionContext context) { * @param identifier */ private record ResolvingConversionContext(ConversionContext delegate, AggregatePath aggregatePath, - Identifier identifier) implements ConversionContext { + Identifier identifier) implements ConversionContext { @Override public S convert(Object source, TypeInformation typeHint) { diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlContext.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlContext.java index 7663e6cd4f..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..dfc4d81ce5 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java @@ -17,6 +17,7 @@ import java.util.*; import java.util.function.Function; +import java.util.function.Predicate; import java.util.stream.Collectors; import org.springframework.data.domain.Pageable; @@ -33,7 +34,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 +59,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 +71,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 +107,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 +165,53 @@ private static boolean isDeeplyNested(AggregatePath path) { * given {@literal path} to those that reference the root entities specified by the {@literal rootCondition}. * * @param path specifies the table and id to select - * @param rootCondition the condition on the root of the path determining what to select - * @param filterColumn the column to apply the IN-condition to. + * @param conditionFunction a function for construction a where clause + * @param columns map making all columns available as a map from {@link AggregatePath} * @return the IN condition */ - private Condition getSubselectCondition(AggregatePath path, Function rootCondition, - Column filterColumn) { + private Condition getSubselectCondition(AggregatePath path, + Function, Condition> conditionFunction, Map columns) { AggregatePath parentPath = path.getParentPath(); if (!parentPath.hasIdProperty()) { if (isDeeplyNested(parentPath)) { - return getSubselectCondition(parentPath, rootCondition, filterColumn); + return getSubselectCondition(parentPath, conditionFunction, columns); } - return rootCondition.apply(filterColumn); + return conditionFunction.apply(columns); } - Table subSelectTable = Table.create(parentPath.getTableInfo().qualifiedTableName()); - Column idColumn = subSelectTable.column(parentPath.getTableInfo().idColumnName()); - Column selectFilterColumn = subSelectTable.column(parentPath.getTableInfo().effectiveIdColumnName()); + AggregatePath.TableInfo parentPathTableInfo = parentPath.getTableInfo(); + Table subSelectTable = Table.create(parentPathTableInfo.qualifiedTableName()); + + Map selectFilterColumns = new TreeMap<>(); + parentPathTableInfo.effectiveIdColumnInfos().forEach( // + (ap, ci) -> // + selectFilterColumns.put(ap, subSelectTable.column(ci.name())) // + ); Condition innerCondition; if (isFirstNonRoot(parentPath)) { // if the parent is the root of the path - // apply the rootCondition - innerCondition = rootCondition.apply(selectFilterColumn); + innerCondition = conditionFunction.apply(selectFilterColumns); } else { - // otherwise, we need another layer of subselect - innerCondition = getSubselectCondition(parentPath, rootCondition, selectFilterColumn); + innerCondition = getSubselectCondition(parentPath, conditionFunction, selectFilterColumns); } + List idColumns = parentPathTableInfo.idColumnInfos().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 +457,7 @@ String createDeleteAllSql(@Nullable PersistentPropertyPath path) { - return createDeleteByPathAndCriteria(mappingContext.getAggregatePath(path), - filterColumn -> filterColumn.isEqualTo(getBindMarker(ROOT_ID_PARAMETER))); + return createDeleteByPathAndCriteria(mappingContext.getAggregatePath(path), this::equalityCondition); } /** @@ -462,17 +479,74 @@ 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) { + + List columns = List.copyOf(columnMap.values()); + + if (columns.size() == 1) { + return Conditions.in(columns.get(0), getBindMarker(IDS_SQL_PARAMETER)); + } + return Conditions.in(TupleExpression.create(columns), getBindMarker(IDS_SQL_PARAMETER)); + } + + /** + * Constructs a where-condition. The where condition will be of the form + * {@literal = :bind-marker-a AND = :bind-marker-b ...} + */ + private Condition equalityCondition(Map columnMap) { + + AggregatePath.ColumnInfos idColumnInfos = mappingContext.getAggregatePath(entity).getTableInfo().idColumnInfos(); + + Condition result = null; + for (Map.Entry entry : columnMap.entrySet()) { + BindMarker bindMarker = getBindMarker(idColumnInfos.get(entry.getKey()).name()); + Comparison singleCondition = entry.getValue().isEqualTo(bindMarker); - return createDeleteByPathAndCriteria(mappingContext.getAggregatePath(path), - filterColumn -> filterColumn.in(getBindMarker(IDS_SQL_PARAMETER))); + result = result == null ? singleCondition : result.and(singleCondition); + } + Assert.state(result != null, "We need at least one condition"); + return result; + } + + /** + * Constructs a function for constructing where a condition. The where condition will be of the form + * {@literal IS NOT NULL AND IS NOT NULL ... } + */ + private Condition isNotNullCondition(Map columnMap) { + + Condition result = null; + for (Column column : columnMap.values()) { + Condition singleCondition = column.isNotNull(); + + result = result == null ? singleCondition : result.and(singleCondition); + } + Assert.state(result != null, "We need at least one condition"); + return result; } private String createFindOneSql() { - Select select = selectBuilder().where(getIdColumn().isEqualTo(getBindMarker(ID_SQL_PARAMETER))) // - .build(); + return render(selectBuilder().where(equalityIdWhereCondition()).build()); + } - return render(select); + private Condition equalityIdWhereCondition() { + + Condition aggregate = null; + for (Column column : getIdColumns()) { + + Comparison condition = column.isEqualTo(getBindMarker(column.getName())); + aggregate = aggregate == null ? condition : aggregate.and(condition); + } + + Assert.state(aggregate != null, "We need at least one id column"); + + return aggregate; } private String createAcquireLockById(LockMode lockMode) { @@ -480,9 +554,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 +568,7 @@ private String createAcquireLockAll(LockMode lockMode) { Table table = this.getTable(); Select select = StatementBuilder // - .select(getIdColumn()) // + .select(getSingleNonNullColumn()) // .from(table) // .lock(lockMode) // .build(); @@ -512,7 +586,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 +598,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 +620,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 +691,7 @@ Column getColumn(AggregatePath path) { return null; } - return sqlContext.getReverseColumn(path); + return sqlContext.getAnyReverseColumn(path); } return sqlContext.getColumn(path); @@ -619,32 +705,50 @@ 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(); + + final Condition[] joinCondition = { null }; + backRefColumnInfos.forEach((ap, ci) -> { + + Condition elementalCondition = currentTable.column(ci.name()) + .isEqualTo(parentTable.column(idColumnInfos.get(ap).name())); + joinCondition[0] = joinCondition[0] == null ? elementalCondition : joinCondition[0].and(elementalCondition); + }); return new Join( // currentTable, // - currentTable.column(path.getTableInfo().reverseColumnInfo().name()), // - parentTable.column(idDefiningParentPath.getTableInfo().idColumnName()) // + joinCondition[0] // ); + } 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 +819,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 +842,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 +860,18 @@ private String createDeleteByPathAndCriteria(AggregatePath path, Function columns = new TreeMap<>(); + AggregatePath.ColumnInfos columnInfos = path.getTableInfo().backReferenceColumnInfos(); + 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 +884,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 +910,22 @@ private Table getTable() { return sqlContext.getTable(); } - private Column getIdColumn() { - return sqlContext.getIdColumn(); + /** + * @return a single column of the primary key to be used in places where one need something not null to be selected. + */ + private Column getSingleNonNullColumn() { + + AggregatePath.ColumnInfos columnInfos = mappingContext.getAggregatePath(entity).getTableInfo().idColumnInfos(); + return columnInfos.any((ap, ci) -> sqlContext.getTable(columnInfos.fullPath(ap)).column(ci.name()).as(ci.alias())); + } + + private List getIdColumns() { + + AggregatePath.ColumnInfos columnInfos = mappingContext.getAggregatePath(entity).getTableInfo().idColumnInfos(); + List result = new ArrayList<>(columnInfos.size()); + columnInfos.forEach((ap, ci) -> result.add(sqlContext.getColumn(columnInfos.fullPath(ap)))); + + return result; } private Column getVersionColumn() { @@ -961,7 +1082,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 +1092,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 +1118,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 +1128,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 +1172,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/SqlParametersFactory.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlParametersFactory.java index 8bf9bb869f..0fdf3d5be0 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlParametersFactory.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlParametersFactory.java @@ -19,13 +19,17 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Function; import java.util.function.Predicate; import org.springframework.data.jdbc.core.mapping.JdbcValue; 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 +82,15 @@ SqlIdentifierParameterSource forInsert(T instance, Class domainType, Iden if (IdValueSource.PROVIDED.equals(idValueSource)) { - RelationalPersistentProperty idProperty = persistentEntity.getRequiredIdProperty(); - Object idValue = persistentEntity.getIdentifierAccessor(instance).getRequiredIdentifier(); - addConvertedPropertyValue(parameterSource, idProperty, idValue, idProperty.getColumnName()); + PersistentPropertyPathAccessor propertyPathAccessor = persistentEntity.getPropertyPathAccessor(instance); + + AggregatePath.ColumnInfos columnInfos = context.getAggregatePath(persistentEntity).getTableInfo().idColumnInfos(); + columnInfos.forEach((ap, __) -> { + Object idValue = propertyPathAccessor.getProperty(columnInfos.fullPath(ap).getRequiredPersistentPropertyPath()); + RelationalPersistentProperty idProperty = ap.getRequiredLeafProperty(); + addConvertedPropertyValue(parameterSource, idProperty, idValue, idProperty.getColumnName()); + }); + } return parameterSource; } @@ -104,20 +114,28 @@ SqlIdentifierParameterSource forUpdate(T instance, Class domainType) { * * @param id the entity id. Must not be {@code null}. * @param domainType the type of the instance. Must not be {@code null}. - * @param name the name to be used for the id parameter. * @return the {@link SqlIdentifierParameterSource} for the query. Guaranteed to not be {@code null}. * @since 2.4 */ - SqlIdentifierParameterSource forQueryById(Object id, Class domainType, SqlIdentifier name) { + SqlIdentifierParameterSource forQueryById(Object id, Class domainType) { SqlIdentifierParameterSource parameterSource = new SqlIdentifierParameterSource(); - addConvertedPropertyValue( // - parameterSource, // - getRequiredPersistentEntity(domainType).getRequiredIdProperty(), // - id, // - name // - ); + RelationalPersistentEntity entity = getRequiredPersistentEntity(domainType); + RelationalPersistentProperty singleIdProperty = entity.getRequiredIdProperty(); + + RelationalPersistentEntity complexId = context.getPersistentEntity(singleIdProperty); + + Function valueExtractor = complexId == null ? ap -> id + : ap -> complexId.getPropertyPathAccessor(id).getProperty(ap.getRequiredPersistentPropertyPath()); + + context.getAggregatePath(entity).getTableInfo().idColumnInfos() // + .forEach((ap, ci) -> addConvertedPropertyValue( // + parameterSource, // + ap.getRequiredLeafProperty(), // + valueExtractor.apply(ap), // + ci.name() // + )); return parameterSource; } @@ -133,8 +151,25 @@ SqlIdentifierParameterSource forQueryByIds(Iterable ids, Class domainT SqlIdentifierParameterSource parameterSource = new SqlIdentifierParameterSource(); - addConvertedPropertyValuesAsList(parameterSource, getRequiredPersistentEntity(domainType).getRequiredIdProperty(), - ids); + RelationalPersistentEntity entity = context.getRequiredPersistentEntity(domainType); + RelationalPersistentProperty singleIdProperty = entity.getRequiredIdProperty(); + RelationalPersistentEntity complexId = context.getPersistentEntity(singleIdProperty); + AggregatePath.ColumnInfos idColumnInfos = context.getAggregatePath(entity).getTableInfo().idColumnInfos(); + + BiFunction valueExtractor = complexId == null ? (id, ap) -> id + : (id, ap) -> complexId.getPropertyPathAccessor(id).getProperty(ap.getRequiredPersistentPropertyPath()); + + List parameterValues = new ArrayList<>(); + for (Object id : ids) { + + List tupleList = new ArrayList<>(); + idColumnInfos.forEach((ap, ci) -> { + tupleList.add(valueExtractor.apply(id, ap)); + }); + parameterValues.add(tupleList.toArray(new Object[0])); + } + + parameterSource.addValue(SqlGenerator.IDS_SQL_PARAMETER, parameterValues); return parameterSource; } @@ -156,21 +191,6 @@ SqlIdentifierParameterSource forQueryByIdentifier(Identifier identifier) { return parameterSource; } - /** - * Utility to create {@link Predicate}s. - */ - static class Predicates { - - /** - * Include all {@link Predicate} returning {@literal false} to never skip a property. - * - * @return the include all {@link Predicate}. - */ - static Predicate includeAll() { - return it -> false; - } - } - private void addConvertedPropertyValue(SqlIdentifierParameterSource parameterSource, RelationalPersistentProperty property, @Nullable Object value, SqlIdentifier name) { diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcDeleteQueryCreator.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcDeleteQueryCreator.java index a7d187b441..f81cc62260 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcDeleteQueryCreator.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcDeleteQueryCreator.java @@ -17,12 +17,10 @@ import java.util.ArrayList; import java.util.List; -import java.util.stream.Stream; import org.springframework.data.domain.Sort; import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.core.convert.QueryMapper; -import org.springframework.data.mapping.Parameter; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.relational.core.dialect.Dialect; import org.springframework.data.relational.core.dialect.RenderContextFactory; @@ -31,10 +29,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; @@ -49,8 +50,8 @@ import org.springframework.util.Assert; /** - * Implementation of {@link RelationalQueryCreator} that creates {@link List} of deletion {@link ParametrizedQuery} - * from a {@link PartTree}. + * Implementation of {@link RelationalQueryCreator} that creates {@link List} of deletion {@link ParametrizedQuery} from + * a {@link PartTree}. * * @author Yunyoung LEE * @author Nikita Konev @@ -96,13 +97,13 @@ 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 @@ -132,26 +133,33 @@ private void deleteRelations(List deleteChain, RelationalPersistentEntit AggregatePath aggregatePath = context.getAggregatePath(path); - // prevent duplication on recursive call - if (path.getLength() > 1 && !aggregatePath.getParentPath().isEmbedded()) { + if (aggregatePath.isEmbedded()) { continue; } - if (aggregatePath.isEntity() && !aggregatePath.isEmbedded()) { + if (aggregatePath.isEntity()) { + + SqlContext sqlContext = new SqlContext(); + + // MariaDB prior to 11.6 does not support aliases for delete statements + Table table = sqlContext.getUnaliasedTable(aggregatePath); + + List reverseColumns = aggregatePath.getTableInfo().backReferenceColumnInfos().toColumnList(table); + Expression expression = Expressions.of(reverseColumns); - SqlContext sqlContext = new SqlContext(aggregatePath.getLeafEntity()); + Condition inCondition = Conditions.in(expression, parentSelect); - Condition inCondition = Conditions - .in(sqlContext.getTable().column(aggregatePath.getTableInfo().reverseColumnInfo().name()), parentSelect); + List parentIdColumns = aggregatePath.getIdDefiningParentPath().getTableInfo().idColumnInfos() + .toColumnList(table); Select select = StatementBuilder.select( // - sqlContext.getTable().column(aggregatePath.getIdDefiningParentPath().getTableInfo().idColumnName()) // - ).from(sqlContext.getTable()) // + parentIdColumns // + ).from(table) // .where(inCondition) // .build(); deleteRelations(deleteChain, aggregatePath.getLeafEntity(), select); - deleteChain.add(StatementBuilder.delete(sqlContext.getTable()).where(inCondition).build()); + deleteChain.add(StatementBuilder.delete(table).where(inCondition).build()); } } } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java index cc28ff2f18..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/repository/query/StringBasedJdbcQuery.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java index 7c4ff0d78c..6f57149d7e 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java @@ -15,7 +15,7 @@ */ package org.springframework.data.jdbc.repository.query; -import static org.springframework.data.jdbc.repository.query.JdbcQueryExecution.*; +import static org.springframework.data.jdbc.repository.query.JdbcQueryExecution.ResultProcessingConverter; import java.lang.reflect.Array; import java.lang.reflect.Constructor; @@ -27,14 +27,10 @@ import java.util.function.Function; import java.util.function.Supplier; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import org.springframework.beans.BeanInstantiationException; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.BeanFactory; -import org.springframework.core.env.StandardEnvironment; import org.springframework.data.expression.ValueEvaluationContext; -import org.springframework.data.expression.ValueExpressionParser; import org.springframework.data.jdbc.core.convert.JdbcColumnTypes; import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.core.mapping.JdbcValue; @@ -42,11 +38,8 @@ import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.repository.query.RelationalParameterAccessor; import org.springframework.data.relational.repository.query.RelationalParametersParameterAccessor; -import org.springframework.data.repository.query.CachingValueExpressionDelegate; import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.Parameters; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; -import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.repository.query.ValueExpressionQueryRewriter; @@ -80,8 +73,7 @@ public class StringBasedJdbcQuery extends AbstractJdbcQuery { private static final String PARAMETER_NEEDS_TO_BE_NAMED = "For queries with named parameters you need to provide names for method parameters; Use @Param for query method parameters, or use the javac flag -parameters"; - private final static String LOCKING_IS_NOT_SUPPORTED = "Currently, @Lock is supported only on derived queries. In other words, for queries created with @Query, the locking condition specified with @Lock does nothing"; - private static final Log LOG = LogFactory.getLog(StringBasedJdbcQuery.class); + private static final String LOCKING_IS_NOT_SUPPORTED = "Currently, @Lock is supported only on derived queries. In other words, for queries created with @Query, the locking condition specified with @Lock does nothing. Offending method: "; private final JdbcConverter converter; private final RowMapperFactory rowMapperFactory; private final ValueExpressionQueryRewriter.ParsedQuery parsedQuery; @@ -91,43 +83,6 @@ public class StringBasedJdbcQuery extends AbstractJdbcQuery { private final CachedResultSetExtractorFactory cachedResultSetExtractorFactory; private final ValueExpressionDelegate delegate; - /** - * Creates a new {@link StringBasedJdbcQuery} for the given {@link JdbcQueryMethod}, {@link RelationalMappingContext} - * and {@link RowMapper}. - * - * @param queryMethod must not be {@literal null}. - * @param operations must not be {@literal null}. - * @param defaultRowMapper can be {@literal null} (only in case of a modifying query). - * @deprecated since 3.4, use the constructors accepting {@link ValueExpressionDelegate} instead. - */ - @Deprecated(since = "3.4") - public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOperations operations, - @Nullable RowMapper defaultRowMapper, JdbcConverter converter, - QueryMethodEvaluationContextProvider evaluationContextProvider) { - this(queryMethod.getRequiredQuery(), queryMethod, operations, result -> (RowMapper) defaultRowMapper, - converter, evaluationContextProvider); - } - - /** - * Creates a new {@link StringBasedJdbcQuery} for the given {@link JdbcQueryMethod}, {@link RelationalMappingContext} - * and {@link RowMapperFactory}. - * - * @param queryMethod must not be {@literal null}. - * @param operations must not be {@literal null}. - * @param rowMapperFactory must not be {@literal null}. - * @param converter must not be {@literal null}. - * @param evaluationContextProvider must not be {@literal null}. - * @since 2.3 - * @deprecated use alternative constructor - */ - @Deprecated(since = "3.4") - public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOperations operations, - RowMapperFactory rowMapperFactory, JdbcConverter converter, - QueryMethodEvaluationContextProvider evaluationContextProvider) { - this(queryMethod.getRequiredQuery(), queryMethod, operations, rowMapperFactory, converter, - evaluationContextProvider); - } - /** * Creates a new {@link StringBasedJdbcQuery} for the given {@link JdbcQueryMethod}, {@link RelationalMappingContext} * and {@link RowMapperFactory}. @@ -191,35 +146,12 @@ public StringBasedJdbcQuery(String query, JdbcQueryMethod queryMethod, NamedPara this.query = query; if (queryMethod.hasLockMode()) { - LOG.warn(LOCKING_IS_NOT_SUPPORTED); + throw new UnsupportedOperationException(LOCKING_IS_NOT_SUPPORTED + queryMethod); } this.parsedQuery = rewriter.parse(this.query); this.delegate = delegate; } - /** - * Creates a new {@link StringBasedJdbcQuery} for the given {@link JdbcQueryMethod}, {@link RelationalMappingContext} - * and {@link RowMapperFactory}. - * - * @param query must not be {@literal null} or empty. - * @param queryMethod must not be {@literal null}. - * @param operations must not be {@literal null}. - * @param rowMapperFactory must not be {@literal null}. - * @param converter must not be {@literal null}. - * @param evaluationContextProvider must not be {@literal null}. - * @since 3.4 - * @deprecated since 3.4, use the constructors accepting {@link ValueExpressionDelegate} instead. - */ - @Deprecated(since = "3.4") - public StringBasedJdbcQuery(String query, JdbcQueryMethod queryMethod, NamedParameterJdbcOperations operations, - RowMapperFactory rowMapperFactory, JdbcConverter converter, - QueryMethodEvaluationContextProvider evaluationContextProvider) { - this(query, queryMethod, operations, rowMapperFactory, converter, new CachingValueExpressionDelegate( - new QueryMethodValueEvaluationContextAccessor(new StandardEnvironment(), rootObject -> evaluationContextProvider - .getEvaluationContext(queryMethod.getParameters(), new Object[] { rootObject })), - ValueExpressionParser.create())); - } - @Override public Object execute(Object[] objects) { @@ -525,6 +457,7 @@ public boolean requiresRowMapper() { @Nullable static Constructor findPrimaryConstructor(Class clazz) { + try { return clazz.getDeclaredConstructor(); } catch (NoSuchMethodException ex) { diff --git a/spring-data-jdbc/src/main/kotlin/org/springframework/data/jdbc/core/JdbcAggregateOperationsExtensions.kt b/spring-data-jdbc/src/main/kotlin/org/springframework/data/jdbc/core/JdbcAggregateOperationsExtensions.kt index b1b7fcd26d..c0a46a20ea 100644 --- a/spring-data-jdbc/src/main/kotlin/org/springframework/data/jdbc/core/JdbcAggregateOperationsExtensions.kt +++ b/spring-data-jdbc/src/main/kotlin/org/springframework/data/jdbc/core/JdbcAggregateOperationsExtensions.kt @@ -20,7 +20,7 @@ import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable import org.springframework.data.domain.Sort import org.springframework.data.relational.core.query.Query -import java.util.Optional +import java.util.* /** * Kotlin extensions for [JdbcAggregateOperations]. @@ -80,7 +80,7 @@ inline fun JdbcAggregateOperations.findAll(sort: Sort): List = /** * Extension for [JdbcAggregateOperations.findAll] with pagination. */ -inline fun JdbcAggregateOperations.findAll(pageable: Pageable): Page = +inline fun JdbcAggregateOperations.findAll(pageable: Pageable): Page = findAll(T::class.java, pageable) /** @@ -98,7 +98,10 @@ inline fun JdbcAggregateOperations.findAll(query: Query): List = /** * Extension for [JdbcAggregateOperations.findAll] with query and pagination. */ -inline fun JdbcAggregateOperations.findAll(query: Query, pageable: Pageable): Page = +inline fun JdbcAggregateOperations.findAll( + query: Query, + pageable: Pageable +): Page = findAll(query, T::class.java, pageable) /** @@ -117,4 +120,4 @@ inline fun JdbcAggregateOperations.deleteAllById(ids: Iterable<*>): * Extension for [JdbcAggregateOperations.deleteAll]. */ inline fun JdbcAggregateOperations.deleteAll(): Unit = - deleteAll(T::class.java) \ No newline at end of file + deleteAll(T::class.java) diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AbstractJdbcAggregateTemplateIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AbstractJdbcAggregateTemplateIntegrationTests.java index 4f047f8406..00ce1278c7 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AbstractJdbcAggregateTemplateIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AbstractJdbcAggregateTemplateIntegrationTests.java @@ -53,7 +53,6 @@ import org.springframework.data.jdbc.testing.TestConfiguration; import org.springframework.data.jdbc.testing.TestDatabaseFeatures; import org.springframework.data.mapping.context.InvalidPersistentPropertyPath; -import org.springframework.data.relational.core.conversion.DbActionExecutionException; import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.Embedded; import org.springframework.data.relational.core.mapping.InsertOnlyProperty; @@ -317,7 +316,7 @@ void saveAndLoadManeEntitiesWithReferenceEntityLikeStream() { Stream streamable = template.streamAll(LegoSet.class); - assertThat(streamable) + assertThat(streamable) // .extracting("id", "manual.id", "manual.content") // .containsExactly(tuple(legoSet.id, legoSet.manual.id, legoSet.manual.content)); } @@ -563,9 +562,8 @@ void updateFailedRootDoesNotExist() { LegoSet entity = new LegoSet(); entity.id = 100L; // does not exist in the database - assertThatExceptionOfType(DbActionExecutionException.class) // - .isThrownBy(() -> template.save(entity)) // - .withCauseInstanceOf(IncorrectUpdateSemanticsDataAccessException.class); + assertThatExceptionOfType(IncorrectUpdateSemanticsDataAccessException.class) // + .isThrownBy(() -> template.save(entity)); } @Test // DATAJDBC-112 @@ -731,7 +729,7 @@ void saveAndLoadAnEntityWithArray() { void saveAndLoadAnEntityWithEmptyArray() { ArrayOwner arrayOwner = new ArrayOwner(); - arrayOwner.digits = new String[] { }; + arrayOwner.digits = new String[] {}; ArrayOwner saved = template.save(arrayOwner); @@ -855,7 +853,7 @@ void saveAndLoadAnEntityWithSet() { assertThat(reloaded.digits).isEqualTo(new HashSet<>(asList("one", "two", "three"))); } - @Test //GH-1737 + @Test // GH-1737 @EnabledOnFeature(SUPPORTS_ARRAYS) void saveAndLoadEmbeddedArray() { @@ -870,7 +868,7 @@ void saveAndLoadEmbeddedArray() { assertThat(reloaded.embeddedStringList.digits).containsExactly("one", "two", "three"); } - @Test //GH-1737 + @Test // GH-1737 @EnabledOnFeature(SUPPORTS_ARRAYS) void saveAndLoadEmptyEmbeddedArray() { @@ -1165,7 +1163,7 @@ void saveAndUpdateAggregateWithIdAndNullVersion() { aggregate.setVersion(null); aggregate.setId(23L); - assertThatThrownBy(() -> template.save(aggregate)).isInstanceOf(DbActionExecutionException.class); + assertThatThrownBy(() -> template.save(aggregate)).isInstanceOf(IncorrectUpdateSemanticsDataAccessException.class); } @Test // DATAJDBC-462 diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/CompositeIdAggregateTemplateHsqlIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/CompositeIdAggregateTemplateHsqlIntegrationTests.java new file mode 100644 index 0000000000..9354f9423a --- /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) +public class CompositeIdAggregateTemplateHsqlIntegrationTests { + + @Autowired JdbcAggregateOperations template; + @Autowired private NamedParameterJdbcOperations namedParameterJdbcTemplate; + + @Test // GH-574 + void saveAndLoadSimpleEntity() { + + SimpleEntity entity = template.insert(new SimpleEntity(new WrappedPk(23L), "alpha")); + + assertThat(entity.wrappedPk).isNotNull() // + .extracting(WrappedPk::id).isNotNull(); + + SimpleEntity reloaded = template.findById(entity.wrappedPk, SimpleEntity.class); + + assertThat(reloaded).isEqualTo(entity); + } + + @Test // GH-574 + void saveAndLoadEntityWithList() { + + WithList entity = template + .insert(new WithList(new WrappedPk(23L), "alpha", List.of(new Child("Romulus"), new Child("Remus")))); + + assertThat(entity.wrappedPk).isNotNull() // + .extracting(WrappedPk::id).isNotNull(); + + WithList reloaded = template.findById(entity.wrappedPk, WithList.class); + + assertThat(reloaded).isEqualTo(entity); + } + + @Test // GH-574 + void saveAndLoadSimpleEntityWithEmbeddedPk() { + + SimpleEntityWithEmbeddedPk entity = template + .insert(new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "x"), "alpha")); + + SimpleEntityWithEmbeddedPk reloaded = template.findById(entity.embeddedPk, SimpleEntityWithEmbeddedPk.class); + + assertThat(reloaded).isEqualTo(entity); + } + + @Test // GH-574 + void saveAndLoadSimpleEntitiesWithEmbeddedPk() { + + List entities = (List) template + .insertAll(List.of(new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "x"), "alpha"), + new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "y"), "beta"), + new SimpleEntityWithEmbeddedPk(new EmbeddedPk(24L, "y"), "gamma"))); + + List firstTwoPks = entities.stream().limit(2).map(SimpleEntityWithEmbeddedPk::embeddedPk).toList(); + Iterable reloaded = template.findAllById(firstTwoPks, SimpleEntityWithEmbeddedPk.class); + + assertThat(reloaded).containsExactlyInAnyOrder(entities.get(0), entities.get(1)); + } + + @Test // GH-574 + void deleteSingleSimpleEntityWithEmbeddedPk() { + + List entities = (List) template + .insertAll(List.of(new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "x"), "alpha"), + new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "y"), "beta"), + new SimpleEntityWithEmbeddedPk(new EmbeddedPk(24L, "y"), "gamma"))); + + template.delete(entities.get(1)); + + Iterable reloaded = template.findAll(SimpleEntityWithEmbeddedPk.class); + + assertThat(reloaded).containsExactlyInAnyOrder(entities.get(0), entities.get(2)); + } + + @Test // GH-574 + void deleteMultipleSimpleEntityWithEmbeddedPk() { + + List entities = (List) template + .insertAll(List.of(new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "x"), "alpha"), + new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "y"), "beta"), + new SimpleEntityWithEmbeddedPk(new EmbeddedPk(24L, "y"), "gamma"))); + + template.deleteAll(List.of(entities.get(1), entities.get(0))); + + Iterable reloaded = template.findAll(SimpleEntityWithEmbeddedPk.class); + + assertThat(reloaded).containsExactly(entities.get(2)); + } + + @Test // GH-574 + void existsSingleSimpleEntityWithEmbeddedPk() { + + List entities = (List) template + .insertAll(List.of(new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "x"), "alpha"), + new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "y"), "beta"), + new SimpleEntityWithEmbeddedPk(new EmbeddedPk(24L, "y"), "gamma"))); + + assertThat(template.existsById(entities.get(1).embeddedPk, SimpleEntityWithEmbeddedPk.class)).isTrue(); + assertThat(template.existsById(new EmbeddedPk(24L, "x"), SimpleEntityWithEmbeddedPk.class)).isFalse(); + + } + + @Test // GH-574 + void updateSingleSimpleEntityWithEmbeddedPk() { + + List entities = (List) template + .insertAll(List.of(new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "x"), "alpha"), + new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "y"), "beta"), + new SimpleEntityWithEmbeddedPk(new EmbeddedPk(24L, "y"), "gamma"))); + + SimpleEntityWithEmbeddedPk updated = new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "x"), "ALPHA"); + template.save(updated); + + Iterable reloaded = template.findAll(SimpleEntityWithEmbeddedPk.class); + + assertThat(reloaded).containsExactlyInAnyOrder(updated, entities.get(1), entities.get(2)); + } + + @Test // GH-574 + void saveAndLoadSingleReferenceAggregate() { + + SingleReference entity = template.insert(new SingleReference(new EmbeddedPk(23L, "x"), "alpha", new Child("Alf"))); + + SingleReference reloaded = template.findById(entity.embeddedPk, SingleReference.class); + + assertThat(reloaded).isEqualTo(entity); + } + + @Test // GH-574 + void updateSingleReferenceAggregate() { + + EmbeddedPk id = new EmbeddedPk(23L, "x"); + template.insert(new SingleReference(id, "alpha", new Child("Alf"))); + + SingleReference updated = new SingleReference(id, "beta", new Child("Barny")); + template.save(updated); + + List all = template.findAll(SingleReference.class); + + assertThat(all).containsExactly(updated); + } + + @Test // GH-574 + void saveAndLoadWithListAndCompositeId() { + + WithListAndCompositeId entity = template.insert( // + new WithListAndCompositeId( // + new EmbeddedPk(23L, "x"), "alpha", // + List.of( // + new Child("Alf"), // + new Child("Bob"), // + new Child("Flo") // + ) // + ) // + ); + + WithListAndCompositeId reloaded = template.findById(entity.embeddedPk, WithListAndCompositeId.class); + + assertThat(reloaded).isEqualTo(entity); + } + + @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 @Embedded(onEmpty = Embedded.OnEmpty.USE_NULL) WrappedPk wrappedPk, // + String name // + ) { + } + + private record Child(String name) { + } + + private record WithList( // + @Id @Embedded(onEmpty = Embedded.OnEmpty.USE_NULL) WrappedPk wrappedPk, // + String name, List children) { + } + + private record EmbeddedPk(Long one, String two) { + } + + private record SimpleEntityWithEmbeddedPk( // + @Id @Embedded(onEmpty = Embedded.OnEmpty.USE_NULL) EmbeddedPk embeddedPk, // + String name // + ) { + } + + private record SingleReference( // + @Id @Embedded(onEmpty = Embedded.OnEmpty.USE_NULL) EmbeddedPk embeddedPk, // + String name, // + Child child) { + } + + private record WithListAndCompositeId( // + @Id @Embedded(onEmpty = Embedded.OnEmpty.USE_NULL) EmbeddedPk embeddedPk, // + String name, // + List child) { + } + + @Configuration + @Import(TestConfiguration.class) + static class Config { + + @Bean + Class testClass() { + return CompositeIdAggregateTemplateHsqlIntegrationTests.class; + } + + @Bean + JdbcAggregateOperations operations(ApplicationEventPublisher publisher, RelationalMappingContext context, + DataAccessStrategy dataAccessStrategy, JdbcConverter converter) { + return new JdbcAggregateTemplate(publisher, context, converter, dataAccessStrategy); + } + } +} diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextImmutableUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextImmutableUnitTests.java index 78d05c03dc..ee7a75eddc 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextImmutableUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextImmutableUnitTests.java @@ -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.getValueProvider(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..e6bf1cb5c5 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcAggregateChangeExecutorContextUnitTests.java @@ -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"), + getValueProvider(value, toAggregatePath("content"), converter)).build(); } PersistentPropertyPath toPath(String path) { diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilderUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilderUnitTests.java index 5873ce23a1..f6a619af12 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilderUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilderUnitTests.java @@ -21,12 +21,17 @@ import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.function.Function; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.core.PersistentPropertyPathTestUtils; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; +import org.springframework.data.mapping.PersistentPropertyPathAccessor; import org.springframework.data.relational.core.mapping.AggregatePath; +import org.springframework.data.relational.core.mapping.Embedded; +import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; /** * Unit tests for the {@link JdbcIdentifierBuilder}. @@ -40,90 +45,167 @@ public class JdbcIdentifierBuilderUnitTests { 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 { + 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..4a5973c86e 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorEmbeddedUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorEmbeddedUnitTests.java @@ -20,17 +20,18 @@ import static org.assertj.core.api.SoftAssertions.*; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.core.PersistentPropertyPathTestUtils; import org.springframework.data.jdbc.core.mapping.AggregateReference; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; +import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.Embedded; import org.springframework.data.relational.core.mapping.Embedded.OnEmpty; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; +import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.core.mapping.Table; import org.springframework.data.relational.core.sql.Aliased; import org.springframework.data.relational.core.sql.SqlIdentifier; @@ -41,6 +42,7 @@ * * @author Bastian Wilhelm * @author Mark Paluch + * @author Jens Schauder */ class SqlGeneratorEmbeddedUnitTests { @@ -84,6 +86,139 @@ void findOne() { }); } + @Test // GH-574 + void findOneWrappedId() { + + SqlGenerator sqlGenerator = createSqlGenerator(WithWrappedId.class); + + String sql = sqlGenerator.getFindOne(); + + assertSoftly(softly -> { + + softly.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(); + + assertSoftly(softly -> { + + softly.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(); + + assertSoftly(softly -> { + + softly.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(); + + assertSoftly(softly -> { + + softly.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); + + assertSoftly(softly -> { + + softly.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); + + assertSoftly(softly -> { + + softly.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(); + + assertSoftly(softly -> { + + softly.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(); + + assertSoftly(softly -> { + + softly.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(); @@ -109,7 +244,8 @@ void findAll() { @Test // DATAJDBC-111 void findAllInList() { - final String sql = sqlGenerator.getFindAllInList(); + + String sql = sqlGenerator.getFindAllInList(); assertSoftly(softly -> { @@ -130,6 +266,43 @@ void findAllInList() { }); } + @Test // GH-574 + void findAllInListEmbeddedId() { + + SqlGenerator sqlGenerator = createSqlGenerator(WithEmbeddedId.class); + + String sql = sqlGenerator.getFindAllInList(); + + assertSoftly(softly -> { + + softly.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(); + + assertSoftly(softly -> { + + softly.assertThat(sql).startsWith("SELECT") // + .contains(" LEFT OUTER JOIN other_entity other ") // + .contains(" ON ") // + .contains(" other.with_embedded_id_and_reference_one = with_embedded_id_and_reference.one ") // + .contains(" other.with_embedded_id_and_reference_two = with_embedded_id_and_reference.two ") // + .contains(" 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()); @@ -175,21 +348,14 @@ void update() { } @Test // DATAJDBC-340 - @Disabled // this is just broken right now void deleteByPath() { + sqlGenerator = createSqlGenerator(DummyEntity2.class); + final String sql = sqlGenerator .createDeleteByPath(PersistentPropertyPathTestUtils.getPath("embedded.other", DummyEntity2.class, context)); - assertThat(sql).containsSequence("DELETE FROM other_entity", // - "WHERE", // - "embedded_with_reference IN (", // - "SELECT ", // - "id ", // - "FROM", // - "dummy_entity2", // - "WHERE", // - "embedded_with_reference = :rootId"); + assertThat(sql).isEqualTo("DELETE FROM other_entity WHERE other_entity.dummy_entity2 = :id"); } @Test // DATAJDBC-340 @@ -276,12 +442,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")); + softly.assertThat(join.joinTable().getName()).isEqualTo(SqlIdentifier.unquoted("other_entity")); + softly.assertThat(join.condition()) + .isEqualTo(SqlGeneratorUnitTests.equalsCondition("dummy_entity2", "id", join.joinTable(), "dummy_entity2")); }); } @@ -301,6 +464,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 +479,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 +497,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 +559,7 @@ static class OtherEntity { } @Table("a") - private - record WithEmbeddedAndAggregateReference(@Id long id, + private record WithEmbeddedAndAggregateReference(@Id long id, @Embedded.Nullable(prefix = "nested_") WithAggregateReference nested) { } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorFixedNamingStrategyUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorFixedNamingStrategyUnitTests.java index 502b310b52..5ecbdd9cc8 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorFixedNamingStrategyUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorFixedNamingStrategyUnitTests.java @@ -30,11 +30,12 @@ import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; /** - * Unit tests the {@link SqlGenerator} with a fixed {@link NamingStrategy} implementation containing a hard wired + * Unit tests the {@link SqlGenerator} with a fixed {@link NamingStrategy} implementation containing a hard-wired * schema, table, and property prefix. * * @author Greg Turnquist * @author Mark Paluch + * @author Jens Schauder */ class SqlGeneratorFixedNamingStrategyUnitTests { @@ -90,7 +91,7 @@ void findOneWithOverriddenFixedTableName() { + "FROM \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_DUMMYENTITY\" " + "LEFT OUTER JOIN \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_REFERENCEDENTITY\" \"ref\" ON \"ref\".\"FIXEDCUSTOMTABLEPREFIX_DUMMYENTITY\" = \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_DUMMYENTITY\".\"FIXEDCUSTOMPROPERTYPREFIX_ID\" L" + "EFT OUTER JOIN \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_SECONDLEVELREFERENCEDENTITY\" \"ref_further\" ON \"ref_further\".\"FIXEDCUSTOMTABLEPREFIX_REFERENCEDENTITY\" = \"ref\".\"FIXEDCUSTOMPROPERTYPREFIX_L1ID\" " - + "WHERE \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_DUMMYENTITY\".\"FIXEDCUSTOMPROPERTYPREFIX_ID\" = :id"); + + "WHERE \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_DUMMYENTITY\".\"FIXEDCUSTOMPROPERTYPREFIX_ID\" = :FixedCustomPropertyPrefix_id"); softAssertions.assertAll(); } @@ -121,7 +122,7 @@ void cascadingDeleteFirstLevel() { String sql = sqlGenerator.createDeleteByPath(getPath("ref")); assertThat(sql).isEqualTo("DELETE FROM \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_REFERENCEDENTITY\" " - + "WHERE \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_REFERENCEDENTITY\".\"FIXEDCUSTOMTABLEPREFIX_DUMMYENTITY\" = :rootId"); + + "WHERE \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_REFERENCEDENTITY\".\"FIXEDCUSTOMTABLEPREFIX_DUMMYENTITY\" = :FixedCustomPropertyPrefix_id"); } @Test // DATAJDBC-107 @@ -136,7 +137,7 @@ void cascadingDeleteAllSecondLevel() { + "WHERE \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_SECONDLEVELREFERENCEDENTITY\".\"FIXEDCUSTOMTABLEPREFIX_REFERENCEDENTITY\" IN " + "(SELECT \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_REFERENCEDENTITY\".\"FIXEDCUSTOMPROPERTYPREFIX_L1ID\" " + "FROM \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_REFERENCEDENTITY\" " - + "WHERE \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_REFERENCEDENTITY\".\"FIXEDCUSTOMTABLEPREFIX_DUMMYENTITY\" = :rootId)"); + + "WHERE \"FIXEDCUSTOMSCHEMA\".\"FIXEDCUSTOMTABLEPREFIX_REFERENCEDENTITY\".\"FIXEDCUSTOMTABLEPREFIX_DUMMYENTITY\" = :FixedCustomPropertyPrefix_id)"); } @Test // DATAJDBC-107 diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java index d095b27ccb..f700d2b08f 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java @@ -50,6 +50,7 @@ import org.springframework.data.relational.core.query.Criteria; import org.springframework.data.relational.core.query.Query; import org.springframework.data.relational.core.sql.Aliased; +import org.springframework.data.relational.core.sql.Comparison; import org.springframework.data.relational.core.sql.LockMode; import org.springframework.data.relational.core.sql.SqlIdentifier; import org.springframework.data.relational.core.sql.Table; @@ -85,6 +86,22 @@ class SqlGeneratorUnitTests { }); private SqlGenerator sqlGenerator; + static Comparison equalsCondition(Table parentTable, SqlIdentifier parentId, Table joinedTable, + SqlIdentifier joinedColumn) { + return org.springframework.data.relational.core.sql.Column.create(joinedColumn, joinedTable) + .isEqualTo(org.springframework.data.relational.core.sql.Column.create(parentId, parentTable)); + } + + static Comparison equalsCondition(SqlIdentifier parentTable, SqlIdentifier parentId, Table joinedTable, + SqlIdentifier joinedColumn) { + return equalsCondition(Table.create(parentTable), parentId, joinedTable, joinedColumn); + } + + static Comparison equalsCondition(String parentTable, String parentId, Table joinedTable, String joinedColumn) { + return equalsCondition(SqlIdentifier.unquoted(parentTable), SqlIdentifier.unquoted(parentId), joinedTable, + SqlIdentifier.unquoted(joinedColumn)); + } + @BeforeEach void setUp() { this.sqlGenerator = createSqlGenerator(DummyEntity.class); @@ -153,7 +170,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 +187,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 +237,7 @@ void deleteMapByPath() { String sql = sqlGenerator.createDeleteByPath(getPath("mappedElements", DummyEntity.class)); - assertThat(sql).isEqualTo("DELETE FROM element WHERE element.dummy_entity = :rootId"); + assertThat(sql).isEqualTo("DELETE FROM element WHERE element.dummy_entity = :id1"); } @Test // DATAJDBC-101 @@ -375,7 +392,8 @@ void selectBySortedQuery() { "ORDER BY dummy_entity.id1 ASC" // ); assertThat(sql).containsOnlyOnce("LEFT OUTER JOIN referenced_entity ref ON ref.dummy_entity = dummy_entity.id1"); - assertThat(sql).containsOnlyOnce("LEFT OUTER JOIN second_level_referenced_entity ref_further ON ref_further.referenced_entity = ref.x_l1id"); + assertThat(sql).containsOnlyOnce( + "LEFT OUTER JOIN second_level_referenced_entity ref_further ON ref_further.referenced_entity = ref.x_l1id"); } @Test // DATAJDBC-131, DATAJDBC-111 @@ -654,7 +672,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 +691,7 @@ void deletingLongChain() { "WHERE chain2.chain3 IN (" + // "SELECT chain3.x_three " + // "FROM chain3 " + // - "WHERE chain3.chain4 = :rootId" + // + "WHERE chain3.chain4 = :x_four" + // ")))"); } @@ -682,7 +700,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 +716,7 @@ void deletingLongChainNoIdWithBackreferenceNotReferencingTheRoot() { + "WHERE no_id_chain4.id_no_id_chain IN (" // + "SELECT id_no_id_chain.x_id " // + "FROM id_no_id_chain " // - + "WHERE id_no_id_chain.id_id_no_id_chain = :rootId" // + + "WHERE id_no_id_chain.id_id_no_id_chain = :x_id" // + "))"); } @@ -714,11 +732,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"))); + }); } @@ -745,13 +762,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")); }); } @@ -759,19 +774,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/JdbcRepositoryConcurrencyIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryConcurrencyIntegrationTests.java index 470c7fc88d..a1ed2ab243 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryConcurrencyIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryConcurrencyIntegrationTests.java @@ -53,6 +53,7 @@ * * @author Myeonghyeon Lee * @author Jens Schauder + * @author Mikhail Polivakha */ @ExtendWith(SpringExtension.class) public class JdbcRepositoryConcurrencyIntegrationTests { @@ -72,12 +73,9 @@ DummyEntityRepository dummyEntityRepository(JdbcRepositoryFactory factory) { } } - @Autowired - NamedParameterJdbcTemplate template; - @Autowired - DummyEntityRepository repository; - @Autowired - PlatformTransactionManager transactionManager; + @Autowired NamedParameterJdbcTemplate template; + @Autowired DummyEntityRepository repository; + @Autowired PlatformTransactionManager transactionManager; List concurrencyEntities; DummyEntity entity; @@ -159,7 +157,7 @@ public void concurrentUpdateAndDelete() throws Exception { } catch (Exception ex) { // When the delete execution is complete, the Update execution throws an // IncorrectUpdateSemanticsDataAccessException. - if (ex.getCause() instanceof IncorrectUpdateSemanticsDataAccessException) { + if (ex instanceof IncorrectUpdateSemanticsDataAccessException) { return null; } throw ex; @@ -193,7 +191,7 @@ public void concurrentUpdateAndDeleteAll() throws Exception { } catch (Exception ex) { // When the delete execution is complete, the Update execution throws an // IncorrectUpdateSemanticsDataAccessException. - if (ex.getCause() instanceof IncorrectUpdateSemanticsDataAccessException) { + if (ex instanceof IncorrectUpdateSemanticsDataAccessException) { return null; } throw ex; @@ -215,7 +213,7 @@ public void concurrentUpdateAndDeleteAll() throws Exception { } private void executeInParallel(CountDownLatch startLatch, CountDownLatch doneLatch, - UnaryOperator deleteAction, DummyEntity entity) { + UnaryOperator deleteAction, DummyEntity entity) { // delete new Thread(() -> { try { @@ -252,13 +250,11 @@ private static DummyEntity createDummyEntity() { return new DummyEntity(null, "Entity Name", new ArrayList<>()); } - interface DummyEntityRepository extends CrudRepository { - } + interface DummyEntityRepository extends CrudRepository {} static class DummyEntity { - @Id - private Long id; + @Id private Long id; String name; final List content; @@ -291,8 +287,7 @@ public DummyEntity withContent(List content) { static class Element { - @Id - private Long id; + @Id private Long id; final Long content; public Element(Long id, Long content) { diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/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/JdbcRepositoryIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java index 7c854b823f..50fe9d03a9 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java @@ -49,8 +49,10 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.core.io.ClassPathResource; +import org.springframework.dao.DuplicateKeyException; import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Transient; import org.springframework.data.domain.*; import org.springframework.data.jdbc.core.mapping.AggregateReference; import org.springframework.data.jdbc.repository.query.Modifying; @@ -64,8 +66,8 @@ import org.springframework.data.jdbc.testing.TestDatabaseFeatures; import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.MappedCollection; -import org.springframework.data.relational.core.mapping.Table; import org.springframework.data.relational.core.mapping.Sequence; +import org.springframework.data.relational.core.mapping.Table; import org.springframework.data.relational.core.mapping.event.AbstractRelationalEvent; import org.springframework.data.relational.core.mapping.event.AfterConvertEvent; import org.springframework.data.relational.core.sql.LockMode; @@ -75,11 +77,11 @@ import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries; import org.springframework.data.repository.core.support.RepositoryFactoryCustomizer; -import org.springframework.data.repository.query.ExtensionAwareQueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.FluentQuery; import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.QueryByExampleExecutor; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.data.spel.EvaluationContextProvider; +import org.springframework.data.spel.ExtensionAwareEvaluationContextProvider; import org.springframework.data.spel.spi.EvaluationContextExtension; import org.springframework.data.support.WindowIterator; import org.springframework.data.util.Streamable; @@ -104,11 +106,44 @@ public class JdbcRepositoryIntegrationTests { @Autowired NamedParameterJdbcTemplate template; @Autowired DummyEntityRepository repository; + + @Autowired ProvidedIdEntityRepository providedIdEntityRepository; @Autowired MyEventListener eventListener; @Autowired RootRepository rootRepository; @Autowired WithDelimitedColumnRepository withDelimitedColumnRepository; @Autowired EntityWithSequenceRepository entityWithSequenceRepository; + public static Stream findAllByExamplePageableSource() { + + return Stream.of( // + Arguments.of(PageRequest.of(0, 3), 3, 34, Arrays.asList("3", "4", "100")), // + Arguments.of(PageRequest.of(1, 10), 10, 10, Arrays.asList("9", "20", "30")), // + Arguments.of(PageRequest.of(2, 10), 10, 10, Arrays.asList("1", "2", "3")), // + Arguments.of(PageRequest.of(33, 3), 1, 34, Collections.emptyList()), // + Arguments.of(PageRequest.of(36, 3), 0, 34, Collections.emptyList()), // + Arguments.of(PageRequest.of(0, 10000), 100, 1, Collections.emptyList()), // + Arguments.of(PageRequest.of(100, 10000), 0, 1, Collections.emptyList()) // + ); + } + + private static DummyEntity createEntity() { + return createEntity("Entity Name"); + } + + private static DummyEntity createEntity(String entityName) { + return createEntity(entityName, it -> {}); + } + + private static DummyEntity createEntity(String entityName, Consumer customizer) { + + DummyEntity entity = new DummyEntity(); + entity.setName(entityName); + + customizer.accept(entity); + + return entity; + } + @BeforeEach public void before() { @@ -208,6 +243,16 @@ public void findAllFindsAllSpecifiedEntities() { .containsExactlyInAnyOrder(entity.getIdProp(), other.getIdProp()); } + @Test // GH-831 + public void duplicateKeyExceptionIsThrownInCaseOfUniqueKeyViolation() { + + ProvidedIdEntity first = ProvidedIdEntity.newInstance(1L, "name"); + ProvidedIdEntity second = ProvidedIdEntity.newInstance(1L, "other"); + + assertThatCode(() -> providedIdEntityRepository.save(first)).doesNotThrowAnyException(); + assertThatThrownBy(() -> providedIdEntityRepository.save(second)).isInstanceOf(DuplicateKeyException.class); + } + @Test // DATAJDBC-97 public void countsEntities() { @@ -938,18 +983,6 @@ void findAllByExamplePageable(Pageable pageRequest, int size, int totalPages, Li } } - public static Stream findAllByExamplePageableSource() { - return Stream.of( // - Arguments.of(PageRequest.of(0, 3), 3, 34, Arrays.asList("3", "4", "100")), // - Arguments.of(PageRequest.of(1, 10), 10, 10, Arrays.asList("9", "20", "30")), // - Arguments.of(PageRequest.of(2, 10), 10, 10, Arrays.asList("1", "2", "3")), // - Arguments.of(PageRequest.of(33, 3), 1, 34, Collections.emptyList()), // - Arguments.of(PageRequest.of(36, 3), 0, 34, Collections.emptyList()), // - Arguments.of(PageRequest.of(0, 10000), 100, 1, Collections.emptyList()), // - Arguments.of(PageRequest.of(100, 10000), 0, 1, Collections.emptyList()) // - ); - } - @Test // GH-1192 void existsByExampleShouldGetOne() { @@ -1432,10 +1465,18 @@ private Instant createDummyBeforeAndAfterNow() { return now; } + enum Direction { + LEFT, CENTER, RIGHT + } + interface DummyProjectExample { String getName(); } + interface ProvidedIdEntityRepository extends CrudRepository { + + } + interface DummyEntityRepository extends CrudRepository, QueryByExampleExecutor { @Lock(LockMode.PESSIMISTIC_WRITE) @@ -1532,6 +1573,10 @@ interface WithDelimitedColumnRepository extends CrudRepository {} + interface DummyProjection { + String getName(); + } + @Configuration @Import(TestConfiguration.class) static class Config { @@ -1543,6 +1588,11 @@ DummyEntityRepository dummyEntityRepository() { return factory.getRepository(DummyEntityRepository.class); } + @Bean + ProvidedIdEntityRepository providedIdEntityRepository() { + return factory.getRepository(ProvidedIdEntityRepository.class); + } + @Bean RootRepository rootRepository() { return factory.getRepository(RootRepository.class); @@ -1573,12 +1623,12 @@ MyEventListener eventListener() { } @Bean - public QueryMethodEvaluationContextProvider extensionAware(List exts) { - return new ExtensionAwareQueryMethodEvaluationContextProvider(exts); + public EvaluationContextProvider extensionAware(List exts) { + return new ExtensionAwareEvaluationContextProvider(exts); } @Bean - RepositoryFactoryCustomizer customizer(QueryMethodEvaluationContextProvider provider) { + RepositoryFactoryCustomizer customizer(EvaluationContextProvider provider) { return repositoryFactory -> repositoryFactory.setEvaluationContextProvider(provider); } @@ -1671,22 +1721,22 @@ public Long getId() { return this.id; } - public String getIdentifier() { - return this.identifier; - } - - public String getType() { - return this.type; - } - public void setId(Long id) { this.id = id; } + public String getIdentifier() { + return this.identifier; + } + public void setIdentifier(String identifier) { this.identifier = identifier; } + public String getType() { + return this.type; + } + public void setType(String type) { this.type = type; } @@ -1828,6 +1878,11 @@ public String getExtensionId() { return "myext"; } + @Override + public Object getRootObject() { + return new ExtensionRoot(); + } + public static class ExtensionRoot { // just public for testing purposes public static Long ID = 1L; @@ -1836,29 +1891,6 @@ public Long getId() { return ID; } } - - @Override - public Object getRootObject() { - return new ExtensionRoot(); - } - } - - private static DummyEntity createEntity() { - return createEntity("Entity Name"); - } - - private static DummyEntity createEntity(String entityName) { - return createEntity(entityName, it -> {}); - } - - private static DummyEntity createEntity(String entityName, Consumer customizer) { - - DummyEntity entity = new DummyEntity(); - entity.setName(entityName); - - customizer.accept(entity); - - return entity; } static class EntityWithSequence { @@ -1886,16 +1918,45 @@ public String getName() { } } + static class ProvidedIdEntity implements Persistable { + + @Id private final Long id; + + private String name; + + @Transient private boolean isNew; + + private ProvidedIdEntity(Long id, String name, boolean isNew) { + this.id = id; + this.name = name; + this.isNew = isNew; + } + + private static ProvidedIdEntity newInstance(Long id, String name) { + return new ProvidedIdEntity(id, name, true); + } + + @Override + public Long getId() { + return id; + } + + @Override + public boolean isNew() { + return isNew; + } + } + static class DummyEntity { String name; Instant pointInTime; OffsetDateTime offsetDateTime; - @Id private Long idProp; boolean flag; AggregateReference ref; Direction direction; byte[] bytes = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 }; + @Id private Long idProp; public DummyEntity(String name) { this.name = name; @@ -1907,54 +1968,54 @@ public String getName() { return this.name; } - public Instant getPointInTime() { - return this.pointInTime; - } - - public OffsetDateTime getOffsetDateTime() { - return this.offsetDateTime; - } - - public Long getIdProp() { - return this.idProp; - } - - public boolean isFlag() { - return this.flag; - } - - public AggregateReference getRef() { - return this.ref; - } - - public Direction getDirection() { - return this.direction; - } - public void setName(String name) { this.name = name; } + public Instant getPointInTime() { + return this.pointInTime; + } + public void setPointInTime(Instant pointInTime) { this.pointInTime = pointInTime; } + public OffsetDateTime getOffsetDateTime() { + return this.offsetDateTime; + } + public void setOffsetDateTime(OffsetDateTime offsetDateTime) { this.offsetDateTime = offsetDateTime; } + public Long getIdProp() { + return this.idProp; + } + public void setIdProp(Long idProp) { this.idProp = idProp; } + public boolean isFlag() { + return this.flag; + } + public void setFlag(boolean flag) { this.flag = flag; } + public AggregateReference getRef() { + return this.ref; + } + public void setRef(AggregateReference ref) { this.ref = ref; } + public Direction getDirection() { + return this.direction; + } + public void setDirection(Direction direction) { this.direction = direction; } @@ -1976,24 +2037,20 @@ public int hashCode() { return Objects.hash(name, pointInTime, offsetDateTime, idProp, flag, ref, direction); } - public void setBytes(byte[] bytes) { - this.bytes = bytes; - } - public byte[] getBytes() { return bytes; } + public void setBytes(byte[] bytes) { + this.bytes = bytes; + } + @Override public String toString() { return "DummyEntity{" + "name='" + name + '\'' + ", idProp=" + idProp + '}'; } } - enum Direction { - LEFT, CENTER, RIGHT - } - static class DummyDto { @Id Long idProp; String name; @@ -2030,10 +2087,6 @@ public AggregateReference getRef() { } } - interface DummyProjection { - String getName(); - } - static final class DtoProjection { private final String name; diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java index 27f4a47c29..ea84b3852b 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java @@ -39,6 +39,7 @@ import org.springframework.data.relational.core.dialect.Escaper; import org.springframework.data.relational.core.dialect.H2Dialect; import org.springframework.data.relational.core.mapping.Embedded; +import org.springframework.data.relational.core.mapping.Embedded.Nullable; import org.springframework.data.relational.core.mapping.MappedCollection; import org.springframework.data.relational.core.mapping.Table; import org.springframework.data.relational.core.sql.LockMode; @@ -65,8 +66,8 @@ public class PartTreeJdbcQueryUnitTests { private static final String TABLE = "\"users\""; - private static final String ALL_FIELDS = "\"users\".\"ID\" AS \"ID\", \"users\".\"AGE\" AS \"AGE\", \"users\".\"ACTIVE\" AS \"ACTIVE\", \"users\".\"LAST_NAME\" AS \"LAST_NAME\", \"users\".\"FIRST_NAME\" AS \"FIRST_NAME\", \"users\".\"DATE_OF_BIRTH\" AS \"DATE_OF_BIRTH\", \"users\".\"HOBBY_REFERENCE\" AS \"HOBBY_REFERENCE\", \"hated\".\"NAME\" AS \"HATED_NAME\", \"users\".\"USER_CITY\" AS \"USER_CITY\", \"users\".\"USER_STREET\" AS \"USER_STREET\""; - private static final String JOIN_CLAUSE = "FROM \"users\" LEFT OUTER JOIN \"HOBBY\" \"hated\" ON \"hated\".\"USERS\" = \"users\".\"ID\""; + private static final String ALL_FIELDS = "\"users\".\"AGE\" AS \"AGE\", \"users\".\"ACTIVE\" AS \"ACTIVE\", \"users\".\"LAST_NAME\" AS \"LAST_NAME\", \"users\".\"FIRST_NAME\" AS \"FIRST_NAME\", \"users\".\"DATE_OF_BIRTH\" AS \"DATE_OF_BIRTH\", \"users\".\"HOBBY_REFERENCE\" AS \"HOBBY_REFERENCE\", \"users\".\"ID\" AS \"ID\", \"users\".\"SUB_ID\" AS \"SUB_ID\", \"hated\".\"NAME\" AS \"HATED_NAME\", \"users\".\"USER_CITY\" AS \"USER_CITY\", \"users\".\"USER_STREET\" AS \"USER_STREET\""; + private static final String JOIN_CLAUSE = "FROM \"users\" LEFT OUTER JOIN \"HOBBY\" \"hated\" ON \"hated\".\"USERS_ID\" = \"users\".\"ID\" AND \"hated\".\"USERS_SUB_ID\" = \"users\".\"SUB_ID\""; private static final String BASE_SELECT = "SELECT " + ALL_FIELDS + " " + JOIN_CLAUSE; JdbcMappingContext mappingContext = new JdbcMappingContext(); @@ -778,7 +779,8 @@ interface UserRepository extends Repository { @Table("users") static class User { - @Id Long id; + @Id + @Nullable UserId id; String firstName; String lastName; Date dateOfBirth; @@ -786,7 +788,7 @@ static class User { Boolean active; @Embedded(prefix = "user_", onEmpty = Embedded.OnEmpty.USE_NULL) Address address; - @Embedded.Nullable AnotherEmbedded anotherEmbedded; + @Nullable AnotherEmbedded anotherEmbedded; List hobbies; Hobby hated; @@ -794,6 +796,9 @@ static class User { AggregateReference hobbyReference; } + record UserId(Long id, String subId) { + } + record Address(String street, String city) { } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java index c0d9cd5bf2..9d222e653a 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java @@ -53,6 +53,8 @@ import org.springframework.data.jdbc.support.JdbcUtil; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.relational.core.mapping.RelationalMappingContext; +import org.springframework.data.relational.core.sql.LockMode; +import org.springframework.data.relational.repository.Lock; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries; @@ -252,7 +254,7 @@ void sliceQueryNotSupported() { JdbcQueryMethod queryMethod = createMethod("sliceAll", Pageable.class); assertThatThrownBy( - () -> new StringBasedJdbcQuery(queryMethod, operations, result -> defaultRowMapper, converter, delegate)) + () -> new StringBasedJdbcQuery(queryMethod, operations, result -> defaultRowMapper, converter, delegate)) .isInstanceOf(UnsupportedOperationException.class) .hasMessageContaining("Slice queries are not supported using string-based queries"); } @@ -278,6 +280,16 @@ void limitNotSupported() { .isInstanceOf(UnsupportedOperationException.class); } + @Test // GH-2023 + void lockNotSupported() { + + JdbcQueryMethod queryMethod = createMethod("unsupportedWithLock", Long.class); + + assertThatThrownBy( + () -> new StringBasedJdbcQuery(queryMethod, operations, result -> defaultRowMapper, converter, delegate)) + .isInstanceOf(UnsupportedOperationException.class); + } + @Test // GH-1212 void convertsEnumCollectionParameterIntoStringCollectionParameter() { @@ -355,10 +367,12 @@ void spelCanBeUsedInsideQueries() { List list = new ArrayList<>(); list.add(new MyEvaluationContextProvider()); - QueryMethodValueEvaluationContextAccessor accessor = new QueryMethodValueEvaluationContextAccessor(new StandardEnvironment(), list); + QueryMethodValueEvaluationContextAccessor accessor = new QueryMethodValueEvaluationContextAccessor( + new StandardEnvironment(), list); this.delegate = new ValueExpressionDelegate(accessor, ValueExpressionParser.create()); - StringBasedJdbcQuery sut = new StringBasedJdbcQuery(queryMethod, operations, result -> defaultRowMapper, converter, delegate); + StringBasedJdbcQuery sut = new StringBasedJdbcQuery(queryMethod, operations, result -> defaultRowMapper, converter, + delegate); ArgumentCaptor paramSource = ArgumentCaptor.forClass(SqlParameterSource.class); ArgumentCaptor query = ArgumentCaptor.forClass(String.class); @@ -401,8 +415,8 @@ public SqlParameterSource extractParameterSource() { mock(RelationResolver.class)) : this.converter; - StringBasedJdbcQuery query = new StringBasedJdbcQuery(method.getDeclaredQuery(), method, operations, result -> mock(RowMapper.class), - converter, delegate); + StringBasedJdbcQuery query = new StringBasedJdbcQuery(method.getDeclaredQuery(), method, operations, + result -> mock(RowMapper.class), converter, delegate); query.execute(arguments); @@ -438,7 +452,8 @@ private StringBasedJdbcQuery createQuery(JdbcQueryMethod queryMethod) { } private StringBasedJdbcQuery createQuery(JdbcQueryMethod queryMethod, String preparedReference, Object value) { - return new StringBasedJdbcQuery(queryMethod, operations, new StubRowMapperFactory(preparedReference, value), converter, delegate); + return new StringBasedJdbcQuery(queryMethod, operations, new StubRowMapperFactory(preparedReference, value), + converter, delegate); } interface MyRepository extends Repository { @@ -505,6 +520,10 @@ interface MyRepository extends Repository { @Query("select count(1) from person where (firstname, lastname) in (:tuples)") Object findByListOfTuples(@Param("tuples") List tuples); + + @Lock(value = LockMode.PESSIMISTIC_READ) + @Query("SELECT * FROM person WHERE id = :id") + DummyEntity unsupportedWithLock(Long id); } private static class CustomRowMapper implements RowMapper { diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategyUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategyUnitTests.java index 7b70956890..114df1e305 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategyUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategyUnitTests.java @@ -40,7 +40,6 @@ import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.query.QueryLookupStrategy; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.util.TypeInformation; @@ -72,7 +71,6 @@ class JdbcQueryLookupStrategyUnitTests { private RepositoryMetadata metadata; private NamedQueries namedQueries = mock(NamedQueries.class); private NamedParameterJdbcOperations operations = mock(NamedParameterJdbcOperations.class); - QueryMethodEvaluationContextProvider evaluationContextProvider = mock(QueryMethodEvaluationContextProvider.class); @BeforeEach void setup() { diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java index ea3e5482cf..e1b429f34c 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java @@ -50,7 +50,7 @@ import org.springframework.data.relational.core.mapping.NamingStrategy; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.repository.core.NamedQueries; -import org.springframework.data.repository.query.ExtensionAwareQueryMethodEvaluationContextProvider; +import org.springframework.data.spel.ExtensionAwareEvaluationContextProvider; import org.springframework.data.spel.spi.EvaluationContextExtension; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; @@ -99,8 +99,7 @@ JdbcRepositoryFactory jdbcRepositoryFactory( namedQueries.map(it -> it.iterator().next()).ifPresent(factory::setNamedQueries); - factory.setEvaluationContextProvider( - new ExtensionAwareQueryMethodEvaluationContextProvider(evaulationContextExtensions)); + factory.setEvaluationContextProvider(new ExtensionAwareEvaluationContextProvider(evaulationContextExtensions)); return factory; } diff --git a/spring-data-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-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql index 1c00e779a6..38269a87db 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql @@ -5,6 +5,7 @@ DROP TABLE LEAF; DROP TABLE WITH_DELIMITED_COLUMN; DROP TABLE ENTITY_WITH_SEQUENCE; DROP SEQUENCE ENTITY_SEQUENCE; +DROP TABLE PROVIDED_ID_ENTITY; CREATE TABLE dummy_entity ( @@ -51,8 +52,14 @@ CREATE TABLE WITH_DELIMITED_COLUMN CREATE TABLE ENTITY_WITH_SEQUENCE ( - ID BIGINT, + ID BIGINT, NAME VARCHAR(100) ); -CREATE SEQUENCE ENTITY_SEQUENCE START WITH 1 INCREMENT BY 1 NO MAXVALUE; \ No newline at end of file +CREATE SEQUENCE ENTITY_SEQUENCE START WITH 1 INCREMENT BY 1 NO MAXVALUE; + +CREATE TABLE PROVIDED_ID_ENTITY +( + ID BIGINT NOT NULL PRIMARY KEY, + NAME VARCHAR(30) +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-h2.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-h2.sql index 6f9087b69d..c24060be24 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-h2.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-h2.sql @@ -43,8 +43,14 @@ CREATE TABLE WITH_DELIMITED_COLUMN CREATE TABLE ENTITY_WITH_SEQUENCE ( - ID BIGINT, + ID BIGINT, NAME VARCHAR(100) ); -CREATE SEQUENCE ENTITY_SEQUENCE START WITH 1 INCREMENT BY 1 NO MAXVALUE; \ No newline at end of file +CREATE SEQUENCE ENTITY_SEQUENCE START WITH 1 INCREMENT BY 1 NO MAXVALUE; + +CREATE TABLE PROVIDED_ID_ENTITY +( + ID BIGINT PRIMARY KEY, + NAME VARCHAR(30) +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql index 6f9087b69d..c24060be24 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql @@ -43,8 +43,14 @@ CREATE TABLE WITH_DELIMITED_COLUMN CREATE TABLE ENTITY_WITH_SEQUENCE ( - ID BIGINT, + ID BIGINT, NAME VARCHAR(100) ); -CREATE SEQUENCE ENTITY_SEQUENCE START WITH 1 INCREMENT BY 1 NO MAXVALUE; \ No newline at end of file +CREATE SEQUENCE ENTITY_SEQUENCE START WITH 1 INCREMENT BY 1 NO MAXVALUE; + +CREATE TABLE PROVIDED_ID_ENTITY +( + ID BIGINT PRIMARY KEY, + NAME VARCHAR(30) +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql index 23d3ad7221..6291f2b934 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql @@ -43,8 +43,14 @@ CREATE TABLE WITH_DELIMITED_COLUMN CREATE TABLE ENTITY_WITH_SEQUENCE ( - ID BIGINT, + ID BIGINT, NAME VARCHAR(100) ); -CREATE SEQUENCE `ENTITY_SEQUENCE` START WITH 1 INCREMENT BY 1 NO MAXVALUE; \ No newline at end of file +CREATE SEQUENCE `ENTITY_SEQUENCE` START WITH 1 INCREMENT BY 1 NO MAXVALUE; + +CREATE TABLE PROVIDED_ID_ENTITY +( + ID BIGINT PRIMARY KEY, + NAME VARCHAR(30) +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql index 69f191f65d..6c70d68a58 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql @@ -5,6 +5,7 @@ DROP TABLE IF EXISTS LEAF; DROP TABLE IF EXISTS WITH_DELIMITED_COLUMN; DROP TABLE IF EXISTS ENTITY_WITH_SEQUENCE; DROP SEQUENCE IF EXISTS ENTITY_SEQUENCE; +DROP TABLE IF EXISTS PROVIDED_ID_ENTITY; CREATE TABLE dummy_entity ( @@ -51,8 +52,14 @@ CREATE TABLE WITH_DELIMITED_COLUMN CREATE TABLE ENTITY_WITH_SEQUENCE ( - ID BIGINT, + ID BIGINT, NAME VARCHAR(100) ); -CREATE SEQUENCE ENTITY_SEQUENCE START WITH 1 INCREMENT BY 1 NO MAXVALUE; \ No newline at end of file +CREATE SEQUENCE ENTITY_SEQUENCE START WITH 1 INCREMENT BY 1 NO MAXVALUE; + +CREATE TABLE PROVIDED_ID_ENTITY +( + ID BIGINT PRIMARY KEY, + NAME VARCHAR(30) +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql index 0d3e16587f..035e52d4b5 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql @@ -1,5 +1,5 @@ SET -SQL_MODE='ALLOW_INVALID_DATES'; + SQL_MODE = 'ALLOW_INVALID_DATES'; CREATE TABLE DUMMY_ENTITY ( @@ -42,4 +42,10 @@ CREATE TABLE WITH_DELIMITED_COLUMN ID BIGINT AUTO_INCREMENT PRIMARY KEY, `ORG.XTUNIT.IDENTIFIER` VARCHAR(100), STYPE VARCHAR(100) -); \ No newline at end of file +); + +CREATE TABLE PROVIDED_ID_ENTITY +( + ID BIGINT PRIMARY KEY, + NAME VARCHAR(30) +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql index 428ff48f3f..1f122d15b9 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql @@ -5,6 +5,7 @@ DROP TABLE LEAF CASCADE CONSTRAINTS PURGE; DROP TABLE WITH_DELIMITED_COLUMN CASCADE CONSTRAINTS PURGE; DROP TABLE ENTITY_WITH_SEQUENCE CASCADE CONSTRAINTS PURGE; DROP SEQUENCE ENTITY_SEQUENCE; +DROP TABLE PROVIDED_ID_ENTITY CASCADE CONSTRAINTS PURGE; CREATE TABLE DUMMY_ENTITY ( @@ -51,8 +52,14 @@ CREATE TABLE WITH_DELIMITED_COLUMN CREATE TABLE ENTITY_WITH_SEQUENCE ( - ID NUMBER, - NAME VARCHAR(100) + ID NUMBER, + NAME VARCHAR2(100) ); -CREATE SEQUENCE ENTITY_SEQUENCE START WITH 1 INCREMENT BY 1; \ No newline at end of file +CREATE SEQUENCE ENTITY_SEQUENCE START WITH 1 INCREMENT BY 1; + +CREATE TABLE PROVIDED_ID_ENTITY +( + ID NUMBER PRIMARY KEY, + NAME VARCHAR2(30) +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql index 42e69437a7..05aea26e12 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql @@ -5,6 +5,7 @@ DROP TABLE LEAF; DROP TABLE WITH_DELIMITED_COLUMN; DROP TABLE ENTITY_WITH_SEQUENCE; DROP SEQUENCE ENTITY_SEQUENCE; +DROP TABLE PROVIDED_ID_ENTITY; CREATE TABLE dummy_entity ( @@ -51,8 +52,14 @@ CREATE TABLE "WITH_DELIMITED_COLUMN" CREATE TABLE ENTITY_WITH_SEQUENCE ( - ID BIGINT, + ID BIGINT, NAME VARCHAR(100) ); -CREATE SEQUENCE "ENTITY_SEQUENCE" START WITH 1 INCREMENT BY 1 NO MAXVALUE; \ No newline at end of file +CREATE SEQUENCE "ENTITY_SEQUENCE" START WITH 1 INCREMENT BY 1 NO MAXVALUE; + +CREATE TABLE PROVIDED_ID_ENTITY +( + ID BIGINT PRIMARY KEY, + NAME VARCHAR(30) +); diff --git a/spring-data-r2dbc/pom.xml b/spring-data-r2dbc/pom.xml index c46dd6bf6b..d2b28e8abc 100644 --- a/spring-data-r2dbc/pom.xml +++ b/spring-data-r2dbc/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-r2dbc - 3.5.0-SNAPSHOT + 4.0.0-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 - 3.5.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/query/R2dbcParameterAccessor.java b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcParameterAccessor.java index 744140b317..95ecebc615 100644 --- a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcParameterAccessor.java +++ b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcParameterAccessor.java @@ -15,7 +15,6 @@ */ package org.springframework.data.r2dbc.repository.query; -import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -25,9 +24,11 @@ import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import org.reactivestreams.Publisher; + import org.springframework.data.relational.repository.query.RelationalParametersParameterAccessor; import org.springframework.data.repository.util.ReactiveWrapperConverters; -import org.springframework.data.repository.util.ReactiveWrappers; +import org.springframework.data.util.ReactiveWrappers; /** * Reactive {@link org.springframework.data.repository.query.ParametersParameterAccessor} implementation that subscribes diff --git a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethod.java b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethod.java index f210ed90c7..f4d1401970 100644 --- a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethod.java +++ b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethod.java @@ -15,8 +15,6 @@ */ package org.springframework.data.r2dbc.repository.query; -import static org.springframework.data.repository.util.ClassUtils.*; - import java.lang.reflect.Method; import java.util.Optional; @@ -89,7 +87,7 @@ public R2dbcQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFa this.mappingContext = mappingContext; - if (hasParameterOfType(method, Pageable.class)) { + if (ReflectionUtils.hasParameterOfType(method, Pageable.class)) { TypeInformation returnType = TypeInformation.fromReturnTypeOf(method); @@ -110,7 +108,7 @@ public R2dbcQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFa method.toString())); } - if (hasParameterOfType(method, Sort.class)) { + if (ReflectionUtils.hasParameterOfType(method, Sort.class)) { throw new IllegalStateException(String.format("Method must not have Pageable *and* Sort parameter; " + "Use sorting capabilities on Pageable instead; Offending method: %s", method.toString())); } diff --git a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java index a72aa6dafa..fe0a6ccc0b 100644 --- a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java +++ b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java @@ -22,25 +22,18 @@ import java.util.List; import java.util.Map; -import org.springframework.core.env.StandardEnvironment; import org.springframework.data.expression.ReactiveValueEvaluationContextProvider; import org.springframework.data.expression.ValueEvaluationContext; import org.springframework.data.expression.ValueEvaluationContextProvider; -import org.springframework.data.expression.ValueExpressionParser; import org.springframework.data.r2dbc.convert.R2dbcConverter; import org.springframework.data.r2dbc.core.R2dbcEntityOperations; import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy; import org.springframework.data.r2dbc.dialect.BindTargetBinder; import org.springframework.data.r2dbc.repository.Query; import org.springframework.data.relational.repository.query.RelationalParameterAccessor; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; -import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor; -import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.spel.ExpressionDependencies; -import org.springframework.expression.ExpressionParser; -import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.r2dbc.core.DatabaseClient; import org.springframework.r2dbc.core.Parameter; import org.springframework.r2dbc.core.PreparedOperation; @@ -64,49 +57,9 @@ public class StringBasedR2dbcQuery extends AbstractR2dbcQuery { private final ReactiveDataAccessStrategy dataAccessStrategy; private final ReactiveValueEvaluationContextProvider valueContextProvider; - /** - * Creates a new {@link StringBasedR2dbcQuery} for the given {@link StringBasedR2dbcQuery}, {@link DatabaseClient}, - * {@link SpelExpressionParser}, and {@link QueryMethodEvaluationContextProvider}. - * - * @param queryMethod must not be {@literal null}. - * @param entityOperations must not be {@literal null}. - * @param converter must not be {@literal null}. - * @param dataAccessStrategy must not be {@literal null}. - * @param expressionParser must not be {@literal null}. - * @param evaluationContextProvider must not be {@literal null}. - * @deprecated use the constructor version with {@link ValueExpressionDelegate} - */ - @Deprecated(since = "3.4") - public StringBasedR2dbcQuery(R2dbcQueryMethod queryMethod, R2dbcEntityOperations entityOperations, - R2dbcConverter converter, ReactiveDataAccessStrategy dataAccessStrategy, ExpressionParser expressionParser, - ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider) { - this(queryMethod.getRequiredAnnotatedQuery(), queryMethod, entityOperations, converter, dataAccessStrategy, - expressionParser, evaluationContextProvider); - } - - /** - * Create a new {@link StringBasedR2dbcQuery} for the given {@code query}, {@link R2dbcQueryMethod}, - * {@link DatabaseClient}, {@link SpelExpressionParser}, and {@link QueryMethodEvaluationContextProvider}. - * - * @param query must not be {@literal null}. - * @param method must not be {@literal null}. - * @param entityOperations must not be {@literal null}. - * @param converter must not be {@literal null}. - * @param dataAccessStrategy must not be {@literal null}. - * @param expressionParser must not be {@literal null}. - * @param evaluationContextProvider must not be {@literal null}. - * @deprecated use the constructor version with {@link ValueExpressionDelegate} - */ - @Deprecated(since = "3.4") - public StringBasedR2dbcQuery(String query, R2dbcQueryMethod method, R2dbcEntityOperations entityOperations, - R2dbcConverter converter, ReactiveDataAccessStrategy dataAccessStrategy, ExpressionParser expressionParser, - ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider) { - this(query, method, entityOperations, converter, dataAccessStrategy, new ValueExpressionDelegate(new QueryMethodValueEvaluationContextAccessor(new StandardEnvironment(), evaluationContextProvider.getEvaluationContextProvider()), ValueExpressionParser.create(() -> expressionParser))); - } - /** * Create a new {@link StringBasedR2dbcQuery} for the given {@code query}, {@link R2dbcQueryMethod}, - * {@link DatabaseClient}, {@link SpelExpressionParser}, and {@link QueryMethodEvaluationContextProvider}. + * {@link DatabaseClient}, and {@link ValueExpressionDelegate}. * * @param method must not be {@literal null}. * @param entityOperations must not be {@literal null}. @@ -115,13 +68,15 @@ public StringBasedR2dbcQuery(String query, R2dbcQueryMethod method, R2dbcEntityO * @param valueExpressionDelegate must not be {@literal null}. */ public StringBasedR2dbcQuery(R2dbcQueryMethod method, R2dbcEntityOperations entityOperations, - R2dbcConverter converter, ReactiveDataAccessStrategy dataAccessStrategy, ValueExpressionDelegate valueExpressionDelegate) { - this(method.getRequiredAnnotatedQuery(), method, entityOperations, converter, dataAccessStrategy, valueExpressionDelegate); + R2dbcConverter converter, ReactiveDataAccessStrategy dataAccessStrategy, + ValueExpressionDelegate valueExpressionDelegate) { + this(method.getRequiredAnnotatedQuery(), method, entityOperations, converter, dataAccessStrategy, + valueExpressionDelegate); } /** * Create a new {@link StringBasedR2dbcQuery} for the given {@code query}, {@link R2dbcQueryMethod}, - * {@link DatabaseClient}, {@link SpelExpressionParser}, and {@link QueryMethodEvaluationContextProvider}. + * {@link DatabaseClient}, and {@link ValueExpressionDelegate}. * * @param method must not be {@literal null}. * @param entityOperations must not be {@literal null}. @@ -130,7 +85,8 @@ public StringBasedR2dbcQuery(R2dbcQueryMethod method, R2dbcEntityOperations enti * @param valueExpressionDelegate must not be {@literal null}. */ public StringBasedR2dbcQuery(String query, R2dbcQueryMethod method, R2dbcEntityOperations entityOperations, - R2dbcConverter converter, ReactiveDataAccessStrategy dataAccessStrategy, ValueExpressionDelegate valueExpressionDelegate) { + R2dbcConverter converter, ReactiveDataAccessStrategy dataAccessStrategy, + ValueExpressionDelegate valueExpressionDelegate) { super(method, entityOperations, converter); @@ -148,7 +104,6 @@ public StringBasedR2dbcQuery(String query, R2dbcQueryMethod method, R2dbcEntityO this.valueContextProvider = (ReactiveValueEvaluationContextProvider) valueContextProvider; this.expressionDependencies = createExpressionDependencies(); - if (method.isSliceQuery()) { throw new UnsupportedOperationException( "Slice queries are not supported using string-based queries; Offending method: " + method); diff --git a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java index 3540cfcd98..ff9110592b 100644 --- a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java +++ b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java @@ -40,7 +40,6 @@ import org.springframework.data.repository.query.CachingValueExpressionDelegate; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy.Key; -import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.lang.Nullable; @@ -78,7 +77,6 @@ public R2dbcRepositoryFactory(DatabaseClient databaseClient, ReactiveDataAccessS this.converter = dataAccessStrategy.getConverter(); this.mappingContext = this.converter.getMappingContext(); this.operations = new R2dbcEntityTemplate(this.databaseClient, this.dataAccessStrategy); - setEvaluationContextProvider(ReactiveQueryMethodEvaluationContextProvider.DEFAULT); } /** @@ -96,7 +94,6 @@ public R2dbcRepositoryFactory(R2dbcEntityOperations operations) { this.converter = dataAccessStrategy.getConverter(); this.mappingContext = this.converter.getMappingContext(); this.operations = operations; - setEvaluationContextProvider(ReactiveQueryMethodEvaluationContextProvider.DEFAULT); } @Override @@ -116,7 +113,8 @@ protected Object getTargetRepository(RepositoryInformation information) { @Override protected Optional getQueryLookupStrategy(@Nullable Key key, ValueExpressionDelegate valueExpressionDelegate) { - return Optional.of(new R2dbcQueryLookupStrategy(operations, new CachingValueExpressionDelegate(valueExpressionDelegate), converter, dataAccessStrategy)); + return Optional.of(new R2dbcQueryLookupStrategy(operations, + new CachingValueExpressionDelegate(valueExpressionDelegate), converter, dataAccessStrategy)); } public RelationalEntityInformation getEntityInformation(Class domainClass) { @@ -145,9 +143,8 @@ private static class R2dbcQueryLookupStrategy extends RelationalQueryLookupStrat private final ValueExpressionDelegate delegate; private final ReactiveDataAccessStrategy dataAccessStrategy; - R2dbcQueryLookupStrategy(R2dbcEntityOperations entityOperations, - ValueExpressionDelegate delegate, R2dbcConverter converter, - ReactiveDataAccessStrategy dataAccessStrategy) { + R2dbcQueryLookupStrategy(R2dbcEntityOperations entityOperations, ValueExpressionDelegate delegate, + R2dbcConverter converter, ReactiveDataAccessStrategy dataAccessStrategy) { super(converter.getMappingContext(), dataAccessStrategy.getDialect()); this.delegate = delegate; @@ -169,7 +166,8 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, : queryMethod.getRequiredAnnotatedQuery(); query = evaluateTableExpressions(metadata, query); - return new StringBasedR2dbcQuery(query, queryMethod, this.entityOperations, this.converter, this.dataAccessStrategy, this.delegate); + return new StringBasedR2dbcQuery(query, queryMethod, this.entityOperations, this.converter, + this.dataAccessStrategy, this.delegate); } else { return new PartTreeR2dbcQuery(queryMethod, this.entityOperations, this.converter, this.dataAccessStrategy); diff --git a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryBean.java b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryBean.java index 827cbc1c49..9f02fba47f 100644 --- a/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryBean.java +++ b/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryBean.java @@ -16,10 +16,8 @@ package org.springframework.data.r2dbc.repository.support; import java.io.Serializable; -import java.util.Optional; import org.springframework.beans.BeansException; -import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.data.mapping.context.MappingContext; @@ -29,8 +27,6 @@ import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; import org.springframework.data.repository.core.support.RepositoryFactorySupport; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; -import org.springframework.data.repository.query.ReactiveExtensionAwareQueryMethodEvaluationContextProvider; import org.springframework.lang.Nullable; import org.springframework.r2dbc.core.DatabaseClient; import org.springframework.util.Assert; @@ -98,12 +94,6 @@ protected final RepositoryFactorySupport createRepositoryFactory() { : getFactoryInstance(this.client, this.dataAccessStrategy); } - @Override - protected Optional createDefaultQueryMethodEvaluationContextProvider( - ListableBeanFactory beanFactory) { - return Optional.of(new ReactiveExtensionAwareQueryMethodEvaluationContextProvider(beanFactory)); - } - /** * Creates and initializes a {@link RepositoryFactorySupport} instance. * diff --git a/spring-data-r2dbc/src/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-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoryConfigurationExtensionUnitTests.java b/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoryConfigurationExtensionUnitTests.java index 5ae04ba37f..187d90d045 100644 --- a/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoryConfigurationExtensionUnitTests.java +++ b/spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoryConfigurationExtensionUnitTests.java @@ -49,7 +49,7 @@ class R2dbcRepositoryConfigurationExtensionUnitTests { private final BeanDefinitionRegistry registry = new DefaultListableBeanFactory(); private final RepositoryConfigurationSource configurationSource = new AnnotationRepositoryConfigurationSource( - metadata, EnableR2dbcRepositories.class, loader, environment, registry); + metadata, EnableR2dbcRepositories.class, loader, environment, registry, null); @Test // gh-13 void isStrictMatchIfDomainTypeIsAnnotatedWithDocument() { diff --git a/spring-data-relational/pom.xml b/spring-data-relational/pom.xml index 9760a0f1e7..14ea2ad98c 100644 --- a/spring-data-relational/pom.xml +++ b/spring-data-relational/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-relational - 3.5.0-SNAPSHOT + 4.0.0-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 - 3.5.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/mapping/AggregatePath.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/AggregatePath.java index abd3e084d3..a6edd906cc 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/AggregatePath.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/AggregatePath.java @@ -16,25 +16,38 @@ package org.springframework.data.relational.core.mapping; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.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 +65,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. */ @@ -227,42 +249,61 @@ default Stream stream() { */ 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 column used for the list index or map key of the leaf property of this path. - */ - @Nullable ColumnInfo qualifierColumnInfo, + /** + * 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 type of the qualifier column of the leaf property of this path or {@literal null} if this is not - * applicable. - */ - @Nullable Class qualifierColumnType, + /** + * 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 column name of the id column of the ancestor path that represents an actual table. - */ - SqlIdentifier idColumnName, + /** + * 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()); + } - /* - * 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 +314,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 +330,128 @@ static TableInfo of(AggregatePath path) { qualifierColumnType = path.getRequiredLeafProperty().getQualifierColumnType(); } - SqlIdentifier idColumnName = leafEntity.hasIdProperty() ? leafEntity.getIdColumn() : null; + ColumnInfos idColumnInfos = computeIdColumnInfos(tableOwner, leafEntity); - SqlIdentifier effectiveIdColumnName = tableOwner.isRoot() ? idColumnName : reverseColumnInfo.name(); + return new TableInfo(qualifiedTableName, tableAlias, backReferenceColumnInfos, qualifierColumnInfo, + qualifierColumnType, idColumnInfos); - return new TableInfo(qualifiedTableName, tableAlias, reverseColumnInfo, qualifierColumnInfo, qualifierColumnType, - idColumnName, effectiveIdColumnName); + } + private static ColumnInfos computeIdColumnInfos(AggregatePath tableOwner, + RelationalPersistentEntity leafEntity) { + + ColumnInfos idColumnInfos = ColumnInfos.empty(tableOwner); + if (!leafEntity.hasIdProperty()) { + return idColumnInfos; + } + + RelationalPersistentProperty idProperty = leafEntity.getRequiredIdProperty(); + AggregatePath idPath = tableOwner.append(idProperty); + + if (idProperty.isEntity()) { + ColumInfosBuilder ciBuilder = new ColumInfosBuilder(idPath); + idPath.getRequiredLeafEntity().doWithProperties((PropertyHandler) p -> { + AggregatePath idElementPath = idPath.append(p); + ciBuilder.add(idElementPath, ColumnInfo.of(idElementPath)); + }); + return ciBuilder.build(); + } else { + ColumInfosBuilder ciBuilder = new ColumInfosBuilder(idPath.getParentPath()); + ciBuilder.add(idPath, ColumnInfo.of(idPath)); + return ciBuilder.build(); + } } - } - record ColumnInfo( + private static ColumnInfos computeBackReferenceColumnInfos(AggregatePath path) { + + AggregatePath tableOwner = AggregatePathTraversal.getTableOwningPath(path); + + if (tableOwner.isRoot()) { + return ColumnInfos.empty(tableOwner); + } + + 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); + }); - /* 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) { + } 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.columnInfos.isEmpty() ? idColumnInfos : backReferenceColumnInfos; + } + } + + /** + * @param name the name of the column used to represent this property in the database. + * @param alias the alias for the column used to represent this property in the database. + * @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 +469,176 @@ 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 + * @since 4.0 + */ + class ColumnInfos { + + private final AggregatePath basePath; + private final Map columnInfos; + private final Map> columnCache = new HashMap<>(); + + /** + * Creates a new ColumnInfos instances based on the arguments. + * + * @param basePath The path on which all other paths in the other argument are based on. For the typical case of a + * composite id, this would be the path to the composite ids. + * @param columnInfos A map, mapping {@literal AggregatePath} instances to the respective {@literal ColumnInfo} + */ + ColumnInfos(AggregatePath basePath, Map columnInfos) { + + this.basePath = basePath; + this.columnInfos = columnInfos; + } + + /** + * An empty {@literal ColumnInfos} instance with a fixed base path. Useful as a base when collecting + * {@link ColumnInfo} instances into an {@literal ColumnInfos} instance. + * + * @param basePath The path on which paths in the {@literal ColumnInfos} or derived objects will be based on. + * @return an empty instance save the {@literal basePath}. + */ + public static ColumnInfos empty(AggregatePath basePath) { + return new ColumnInfos(basePath, new HashMap<>()); + } + + /** + * If this instance contains exactly one {@link ColumnInfo} it will be returned. + * + * @return the unique {@literal ColumnInfo} if present. + * @throws IllegalStateException if the number of contained {@literal ColumnInfo} instances is not exactly 1. + */ + public ColumnInfo unique() { + + Collection values = columnInfos.values(); + 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(); + } + + /** + * 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(); + } + + /** + * Converts the given {@link Table} into a list of {@link Column}s. This method retrieves and caches the list of + * columns for the specified table. If the columns are not already cached, it computes the list by mapping + * {@code columnInfos} to their corresponding {@link Column} in the provided table and then stores the result in the + * cache. + * + * @param table the {@link Table} for which the columns should be generated; must not be {@literal null}. + * @return a list of {@link Column}s associated with the specified {@link Table}. Guaranteed no to be + * {@literal null}. + */ + public List toColumnList(Table table) { + + return columnCache.computeIfAbsent(table, + t -> columnInfos.values().stream().map(columnInfo -> t.column(columnInfo.name)).toList()); + } + + /** + * 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 3.5 + */ + public T reduce(T identity, BiFunction accumulator, BinaryOperator combiner) { + + T result = identity; + + for (Map.Entry entry : columnInfos.entrySet()) { + + T mapped = accumulator.apply(entry.getKey(), entry.getValue()); + result = combiner.apply(result, mapped); + } + + return result; + } + + /** + * Calls the consumer for each pair of {@link AggregatePath} and {@literal ColumnInfo}. + * + * @param consumer the function to call. + */ + public void forEach(BiConsumer consumer) { + columnInfos.forEach(consumer); + } + + /** + * Calls the {@literal mapper} for each pair one pair of {@link AggregatePath} and {@link ColumnInfo}, if there is + * any. + * + * @param mapper the function to call. + * @return the result of the mapper + * @throws java.util.NoSuchElementException if this {@literal ColumnInfo} is empty. + */ + public T any(BiFunction mapper) { + + Map.Entry any = columnInfos.entrySet().iterator().next(); + return mapper.apply(any.getKey(), any.getValue()); + } + + /** + * Gets the {@link ColumnInfo} for the provided {@link AggregatePath} + * + * @param path for which to return the {@literal ColumnInfo} + * @return {@literal ColumnInfo} for the given path. + */ + public ColumnInfo get(AggregatePath path) { + return columnInfos.get(path); + } + + /** + * Constructs an {@link AggregatePath} from the {@literal basePath} and the provided argument. + * + * @param ap {@literal AggregatePath} to be appended to the {@literal basePath}. + * @return the combined (@literal AggregatePath} + */ + public AggregatePath fullPath(AggregatePath ap) { + return basePath.append(ap); + } + + /** + * Number of {@literal ColumnInfo} elements in this instance. + * + * @return the size of the collection of {@literal ColumnInfo}. + */ + public int size() { + return columnInfos.size(); + } + } + } 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/ColumInfosBuilder.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/ColumInfosBuilder.java new file mode 100644 index 0000000000..2e5c290325 --- /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.subtract(basePath), columnInfo); + } + + /** + * Build the final {@link AggregatePath.ColumnInfos} instance. + * + * @return a {@literal ColumnInfos} instance containing all the added {@link AggregatePath.ColumnInfo} instances. + */ + AggregatePath.ColumnInfos build() { + return new AggregatePath.ColumnInfos(basePath, columnInfoMap); + } + +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DefaultAggregatePath.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DefaultAggregatePath.java index b0bcc78cb2..dd264dbcca 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DefaultAggregatePath.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DefaultAggregatePath.java @@ -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,6 +208,47 @@ public AggregatePath getIdDefiningParentPath() { return AggregatePathTraversal.getIdDefiningPath(this); } + @Override + public AggregatePath getTail() { + + if (getLength() <= 2) { + return null; + } + + AggregatePath tail = null; + for (RelationalPersistentProperty prop : this.path) { + if (tail == null) { + tail = context.getAggregatePath(context.getPersistentEntity(prop)); + } else { + tail = tail.append(prop); + } + } + return tail; + } + + @Override + @Nullable + public AggregatePath 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)); + } + /** * Finds and returns the longest path with ich identical or an ancestor to the current path and maps directly to a * table. @@ -240,7 +295,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 27359f7592..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"); } @@ -106,13 +107,6 @@ public String getName() { return delegate.getName(); } - @Override - @Deprecated - @Nullable - public PreferredConstructor getPersistenceConstructor() { - return delegate.getPersistenceConstructor(); - } - @Override @Nullable public InstanceCreatorMetadata getInstanceCreatorMetadata() { diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java index eb6409e74e..47bd819900 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java @@ -148,9 +148,9 @@ protected RelationalPersistentProperty createPersistentProperty(Property propert } /** - * @since 3.2 * @return iff single query loading is enabled. * @see #setSingleQueryLoadingEnabled(boolean) + * @since 3.2 */ public boolean isSingleQueryLoadingEnabled() { return singleQueryLoadingEnabled; @@ -161,8 +161,8 @@ public boolean isSingleQueryLoadingEnabled() { * {@link org.springframework.data.relational.core.dialect.Dialect} supports it, Spring Data JDBC will try to use * Single Query Loading if possible. * - * @since 3.2 * @param singleQueryLoadingEnabled + * @since 3.2 */ public void setSingleQueryLoadingEnabled(boolean singleQueryLoadingEnabled) { this.singleQueryLoadingEnabled = singleQueryLoadingEnabled; @@ -217,7 +217,6 @@ private record AggregatePathCacheKey(RelationalPersistentEntity root, * Create a new AggregatePathCacheKey for a root entity. * * @param root the root entity. - * @return */ static AggregatePathCacheKey of(RelationalPersistentEntity root) { return new AggregatePathCacheKey(root, null); @@ -226,8 +225,7 @@ static AggregatePathCacheKey of(RelationalPersistentEntity root) { /** * Create a new AggregatePathCacheKey for a property path. * - * @param path - * @return + * @param path {@Literal AggregatePath} to obtain a cache key for. */ static AggregatePathCacheKey of(PersistentPropertyPath path) { return new AggregatePathCacheKey(path.getBaseProperty().getOwner(), path); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntity.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentEntity.java index 49e9b929c1..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/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..c5f4fe0d88 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Conditions.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Conditions.java @@ -247,7 +247,7 @@ public static In in(Expression columnOrExpression, Expression... expressions) { * @param subselect the subselect. * @return the {@link In} condition. */ - public static In in(Column column, Select subselect) { + public static In in(Expression column, Select subselect) { Assert.notNull(column, "Column must not be null"); Assert.notNull(subselect, "Subselect must not be null"); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Expressions.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Expressions.java index 328c37218a..42176e1e55 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Expressions.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Expressions.java @@ -15,15 +15,17 @@ */ package org.springframework.data.relational.core.sql; +import java.util.List; + /** * Factory for common {@link Expression}s. * * @author Mark Paluch * @author Jens Schauder - * @since 1.1 * @see SQL * @see Conditions * @see Functions + * @since 1.1 */ public abstract class Expressions { @@ -61,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..82ac2fe7f8 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/TupleExpression.java @@ -0,0 +1,60 @@ +/* + * 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.List; + +/** + * A tuple as used in conditions like + * + *

    + *   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 List expressions; + + private static Segment[] children(List expressions) { + return expressions.toArray(new Segment[0]); + } + + 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 32ce15dee1..40c21e1976 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ExpressionVisitor.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ExpressionVisitor.java @@ -48,7 +48,7 @@ class ExpressionVisitor extends TypedSubtreeVisitor implements PartR /** * Creates an {@code ExpressionVisitor}. * - * @param context must not be {@literal null}. + * @param context must not be {@literal null}. * @param aliasHandling controls if columns should be rendered as their alias or using their table names. * @since 2.3 */ @@ -78,6 +78,13 @@ Delegation enterMatched(Expression segment) { return Delegation.delegateTo(visitor); } + if (segment instanceof TupleExpression) { + + TupleVisitor visitor = new TupleVisitor(context); + partRenderer = visitor; + return Delegation.delegateTo(visitor); + } + if (segment instanceof AnalyticFunction) { AnalyticFunctionVisitor visitor = new AnalyticFunctionVisitor(context); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/TupleVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/TupleVisitor.java new file mode 100644 index 0000000000..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/conversion/DbActionExecutionExceptionUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/DbActionExecutionExceptionUnitTests.java deleted file mode 100644 index c81c7dd20a..0000000000 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/DbActionExecutionExceptionUnitTests.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2018-2025 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.relational.core.conversion; - -import org.junit.jupiter.api.Test; - -import static org.mockito.Mockito.*; - -/** - * Unit test for {@link DbActionExecutionException}. - * - * @author Jens Schauder - */ -public class DbActionExecutionExceptionUnitTests { - - @Test // DATAJDBC-162 - public void constructorWorksWithNullPropertyPath() { - - DbAction action = mock(DbAction.class); - new DbActionExecutionException(action, null); - } - -} 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..fa1db8e202 --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/ColumnInfosUnitTests.java @@ -0,0 +1,102 @@ +/* + * 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(basePath(DummyEntity.class)); + + 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("one+\"ONE\"", "two+\"TWO\""); + + columnInfos.get(getPath(CompositeId.class, "one")); + + } + + private AggregatePath getPath(Class type, String name) { + return basePath(type).append(context.getPersistentEntity(type).getPersistentProperty(name)); + } + + private AggregatePath basePath(Class type) { + return context.getAggregatePath(context.getPersistentEntity(type)); + } + + record DummyEntity(@Id String id, String name) { + } + + record CompositeId(String one, String two) { + } + + record 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..6ee97eacd7 --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/TupleExpressionUnitTests.java @@ -0,0 +1,53 @@ +/* + * 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..788605a294 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/SelectRendererUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/SelectRendererUnitTests.java @@ -17,6 +17,8 @@ import static org.assertj.core.api.Assertions.*; +import java.util.List; + import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.data.relational.core.dialect.PostgresDialect; @@ -24,8 +26,6 @@ import org.springframework.data.relational.core.sql.*; import org.springframework.util.StringUtils; -import java.util.List; - /** * Unit tests for {@link SqlRenderer}. * @@ -115,196 +115,6 @@ void shouldRenderCountFunctionWithAliasedColumn() { assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT COUNT(bar.foo), bar.foo AS foo_bar FROM bar"); } - @Test // DATAJDBC-309 - void shouldRenderSimpleJoin() { - - Table employee = SQL.table("employee"); - Table department = SQL.table("department"); - - Select select = Select.builder().select(employee.column("id"), department.column("name")).from(employee) // - .join(department).on(employee.column("department_id")).equals(department.column("id")) // - .build(); - - assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT employee.id, department.name FROM employee " - + "JOIN department ON employee.department_id = department.id"); - } - - @Test // DATAJDBC-340 - void shouldRenderOuterJoin() { - - Table employee = SQL.table("employee"); - Table department = SQL.table("department"); - - Select select = Select.builder().select(employee.column("id"), department.column("name")) // - .from(employee) // - .leftOuterJoin(department).on(employee.column("department_id")).equals(department.column("id")) // - .build(); - - assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT employee.id, department.name FROM employee " - + "LEFT OUTER JOIN department ON employee.department_id = department.id"); - } - - @Test // GH-1421 - void shouldRenderFullOuterJoin() { - - Table employee = SQL.table("employee"); - Table department = SQL.table("department"); - - Select select = Select.builder().select(employee.column("id"), department.column("name")) // - .from(employee) // - .join(department, Join.JoinType.FULL_OUTER_JOIN).on(employee.column("department_id")) - .equals(department.column("id")) // - .build(); - - assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT employee.id, department.name FROM employee " - + "FULL OUTER JOIN department ON employee.department_id = department.id"); - } - - @Test // DATAJDBC-309 - void shouldRenderSimpleJoinWithAnd() { - - Table employee = SQL.table("employee"); - Table department = SQL.table("department"); - - Select select = Select.builder().select(employee.column("id"), department.column("name")).from(employee) // - .join(department).on(employee.column("department_id")).equals(department.column("id")) // - .and(employee.column("tenant")).equals(department.column("tenant")) // - .build(); - - assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT employee.id, department.name FROM employee " // - + "JOIN department ON employee.department_id = department.id " // - + "AND employee.tenant = department.tenant"); - } - - @Test // #995 - void shouldRenderArbitraryJoinCondition() { - - Table employee = SQL.table("employee"); - Table department = SQL.table("department"); - - Select select = Select.builder() // - .select(employee.column("id"), department.column("name")) // - .from(employee) // - .join(department) // - .on(Conditions.isEqual(employee.column("department_id"), department.column("id")) // - .or(Conditions.isNotEqual(employee.column("tenant"), department.column("tenant")) // - )).build(); - - assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT employee.id, department.name FROM employee " // - + "JOIN department ON employee.department_id = department.id " // - + "OR employee.tenant != department.tenant"); - } - - @Test // #1009 - void shouldRenderJoinWithJustExpression() { - - Table employee = SQL.table("employee"); - Table department = SQL.table("department"); - - Select select = Select.builder().select(employee.column("id"), department.column("name")).from(employee) // - .join(department).on(Expressions.just("alpha")).equals(Expressions.just("beta")) // - .build(); - - assertThat(SqlRenderer.toString(select)) - .isEqualTo("SELECT employee.id, department.name FROM employee " + "JOIN department ON alpha = beta"); - } - - @Test // DATAJDBC-309 - void shouldRenderMultipleJoinWithAnd() { - - Table employee = SQL.table("employee"); - Table department = SQL.table("department"); - Table tenant = SQL.table("tenant").as("tenant_base"); - - Select select = Select.builder().select(employee.column("id"), department.column("name")).from(employee) // - .join(department).on(employee.column("department_id")).equals(department.column("id")) // - .and(employee.column("tenant")).equals(department.column("tenant")) // - .join(tenant).on(tenant.column("tenant_id")).equals(department.column("tenant")) // - .build(); - - assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT employee.id, department.name FROM employee " // - + "JOIN department ON employee.department_id = department.id " // - + "AND employee.tenant = department.tenant " // - + "JOIN tenant tenant_base ON tenant_base.tenant_id = department.tenant"); - } - - @Test // GH-1003 - void shouldRenderJoinWithInlineQuery() { - - Table employee = SQL.table("employee"); - Table department = SQL.table("department"); - - Select innerSelect = Select.builder() - .select(employee.column("id"), employee.column("department_Id"), employee.column("name")).from(employee) - .build(); - - InlineQuery one = InlineQuery.create(innerSelect, "one"); - - Select select = Select.builder().select(one.column("id"), department.column("name")).from(department) // - .join(one).on(one.column("department_id")).equals(department.column("id")) // - .build(); - - String sql = SqlRenderer.toString(select); - - assertThat(sql).isEqualTo("SELECT one.id, department.name FROM department " // - + "JOIN (SELECT employee.id, employee.department_Id, employee.name FROM employee) one " // - + "ON one.department_id = department.id"); - } - - @Test // GH-1362 - void shouldRenderNestedJoins() { - - Table merchantCustomers = Table.create("merchants_customers"); - Table customerDetails = Table.create("customer_details"); - - Select innerSelect = Select.builder().select(customerDetails.column("cd_user_id")).from(customerDetails) - .join(merchantCustomers) - .on(merchantCustomers.column("mc_user_id").isEqualTo(customerDetails.column("cd_user_id"))).build(); - - InlineQuery innerTable = InlineQuery.create(innerSelect, "inner"); - - Select select = Select.builder().select(merchantCustomers.asterisk()) // - .from(merchantCustomers) // - .join(innerTable).on(innerTable.column("i_user_id").isEqualTo(merchantCustomers.column("mc_user_id"))) // - .build(); - - String sql = SqlRenderer.toString(select); - - assertThat(sql).isEqualTo("SELECT merchants_customers.* FROM merchants_customers " + // - "JOIN (" + // - "SELECT customer_details.cd_user_id " + // - "FROM customer_details " + // - "JOIN merchants_customers ON merchants_customers.mc_user_id = customer_details.cd_user_id" + // - ") inner " + // - "ON inner.i_user_id = merchants_customers.mc_user_id"); - } - - @Test // GH-1003 - void shouldRenderJoinWithTwoInlineQueries() { - - Table employee = SQL.table("employee"); - Table department = SQL.table("department"); - - Select innerSelectOne = Select.builder() - .select(employee.column("id").as("empId"), employee.column("department_Id"), employee.column("name")) - .from(employee).build(); - Select innerSelectTwo = Select.builder().select(department.column("id"), department.column("name")).from(department) - .build(); - - InlineQuery one = InlineQuery.create(innerSelectOne, "one"); - InlineQuery two = InlineQuery.create(innerSelectTwo, "two"); - - Select select = Select.builder().select(one.column("empId"), two.column("name")).from(one) // - .join(two).on(two.column("department_id")).equals(one.column("empId")) // - .build(); - - String sql = SqlRenderer.toString(select); - assertThat(sql).isEqualTo("SELECT one.empId, two.name FROM (" // - + "SELECT employee.id AS empId, employee.department_Id, employee.name FROM employee) one " // - + "JOIN (SELECT department.id, department.name FROM department) two " // - + "ON two.department_id = one.empId"); - } - @Test // DATAJDBC-309 void shouldRenderOrderByName() { @@ -424,7 +234,6 @@ void shouldRenderSimpleFunctionWithSubselect() { Table floo = SQL.table("floo"); Column bah = floo.column("bah"); - Select subselect = Select.builder().select(bah).from(floo).build(); SimpleFunction func = SimpleFunction.create("func", List.of(SubselectExpression.of(subselect))); @@ -435,8 +244,8 @@ void shouldRenderSimpleFunctionWithSubselect() { .where(Conditions.isEqual(func, SQL.literalOf(23))) // .build(); - assertThat(SqlRenderer.toString(select)) - .isEqualTo("SELECT func(SELECT floo.bah FROM floo) AS alias FROM foo WHERE func(SELECT floo.bah FROM floo) = 23"); + assertThat(SqlRenderer.toString(select)).isEqualTo( + "SELECT func(SELECT floo.bah FROM floo) AS alias FROM foo WHERE func(SELECT floo.bah FROM floo) = 23"); } @Test // DATAJDBC-309 @@ -709,7 +518,7 @@ void asteriskOfAliasedTableUsesAlias() { assertThat(rendered).isEqualTo("SELECT e.*, e.id FROM employee e"); } - @Test + @Test // GH-1844 void rendersCaseExpression() { Table table = SQL.table("table"); @@ -724,7 +533,225 @@ void rendersCaseExpression() { .build(); String rendered = SqlRenderer.toString(select); - assertThat(rendered).isEqualTo("SELECT CASE WHEN table.name IS NULL THEN 1 WHEN table.name IS NOT NULL THEN table.name ELSE 3 END FROM table"); + assertThat(rendered).isEqualTo( + "SELECT CASE WHEN table.name IS NULL THEN 1 WHEN table.name IS NOT NULL THEN table.name ELSE 3 END FROM table"); + } + + @Test // GH-574 + void rendersTupleExpression() { + + Table table = SQL.table("table"); + Column first = table.column("first"); + Column middle = table.column("middle"); + Column last = table.column("last").as("anAlias"); + + TupleExpression tupleExpression = TupleExpression.create(first, SQL.literalOf(1), middle, last); // + + Select select = StatementBuilder.select(first) // + .from(table) // + .where(Conditions.in(tupleExpression, Expressions.just("some expression"))).build(); + + String rendered = SqlRenderer.toString(select); + assertThat(rendered).isEqualTo( + "SELECT table.first FROM table WHERE (table.first, 1, table.middle, table.last) IN (some expression)"); + } + + /** + * Tests for rendering joins. + */ + @Nested + class JoinsTests { + + @Test // DATAJDBC-309 + void shouldRenderSimpleJoin() { + + Table employee = SQL.table("employee"); + Table department = SQL.table("department"); + + Select select = Select.builder().select(employee.column("id"), department.column("name")).from(employee) // + .join(department).on(employee.column("department_id")).equals(department.column("id")) // + .build(); + + assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT employee.id, department.name FROM employee " + + "JOIN department ON employee.department_id = department.id"); + } + + @Test // DATAJDBC-340 + void shouldRenderOuterJoin() { + + Table employee = SQL.table("employee"); + Table department = SQL.table("department"); + + Select select = Select.builder().select(employee.column("id"), department.column("name")) // + .from(employee) // + .leftOuterJoin(department).on(employee.column("department_id")).equals(department.column("id")) // + .build(); + + assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT employee.id, department.name FROM employee " + + "LEFT OUTER JOIN department ON employee.department_id = department.id"); + } + + @Test // GH-1421 + void shouldRenderFullOuterJoin() { + + Table employee = SQL.table("employee"); + Table department = SQL.table("department"); + + Select select = Select.builder().select(employee.column("id"), department.column("name")) // + .from(employee) // + .join(department, Join.JoinType.FULL_OUTER_JOIN).on(employee.column("department_id")) + .equals(department.column("id")) // + .build(); + + assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT employee.id, department.name FROM employee " + + "FULL OUTER JOIN department ON employee.department_id = department.id"); + } + + @Test // DATAJDBC-309 + void shouldRenderSimpleJoinWithAnd() { + + Table employee = SQL.table("employee"); + Table department = SQL.table("department"); + + Select select = Select.builder().select(employee.column("id"), department.column("name")).from(employee) // + .join(department).on(employee.column("department_id")).equals(department.column("id")) // + .and(employee.column("tenant")).equals(department.column("tenant")) // + .build(); + + assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT employee.id, department.name FROM employee " // + + "JOIN department ON employee.department_id = department.id " // + + "AND employee.tenant = department.tenant"); + } + + @Test // #995 + void shouldRenderArbitraryJoinCondition() { + + Table employee = SQL.table("employee"); + Table department = SQL.table("department"); + + Select select = Select.builder() // + .select(employee.column("id"), department.column("name")) // + .from(employee) // + .join(department) // + .on(Conditions.isEqual(employee.column("department_id"), department.column("id")) // + .or(Conditions.isNotEqual(employee.column("tenant"), department.column("tenant")) // + )).build(); + + assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT employee.id, department.name FROM employee " // + + "JOIN department ON employee.department_id = department.id " // + + "OR employee.tenant != department.tenant"); + } + + @Test // #1009 + void shouldRenderJoinWithJustExpression() { + + Table employee = SQL.table("employee"); + Table department = SQL.table("department"); + + Select select = Select.builder().select(employee.column("id"), department.column("name")).from(employee) // + .join(department).on(Expressions.just("alpha")).equals(Expressions.just("beta")) // + .build(); + + assertThat(SqlRenderer.toString(select)) + .isEqualTo("SELECT employee.id, department.name FROM employee " + "JOIN department ON alpha = beta"); + } + + @Test // DATAJDBC-309 + void shouldRenderMultipleJoinWithAnd() { + + Table employee = SQL.table("employee"); + Table department = SQL.table("department"); + Table tenant = SQL.table("tenant").as("tenant_base"); + + Select select = Select.builder().select(employee.column("id"), department.column("name")).from(employee) // + .join(department).on(employee.column("department_id")).equals(department.column("id")) // + .and(employee.column("tenant")).equals(department.column("tenant")) // + .join(tenant).on(tenant.column("tenant_id")).equals(department.column("tenant")) // + .build(); + + assertThat(SqlRenderer.toString(select)).isEqualTo("SELECT employee.id, department.name FROM employee " // + + "JOIN department ON employee.department_id = department.id " // + + "AND employee.tenant = department.tenant " // + + "JOIN tenant tenant_base ON tenant_base.tenant_id = department.tenant"); + } + + @Test // GH-1003 + void shouldRenderJoinWithInlineQuery() { + + Table employee = SQL.table("employee"); + Table department = SQL.table("department"); + + Select innerSelect = Select.builder() + .select(employee.column("id"), employee.column("department_Id"), employee.column("name")).from(employee) + .build(); + + InlineQuery one = InlineQuery.create(innerSelect, "one"); + + Select select = Select.builder().select(one.column("id"), department.column("name")).from(department) // + .join(one).on(one.column("department_id")).equals(department.column("id")) // + .build(); + + String sql = SqlRenderer.toString(select); + + assertThat(sql).isEqualTo("SELECT one.id, department.name FROM department " // + + "JOIN (SELECT employee.id, employee.department_Id, employee.name FROM employee) one " // + + "ON one.department_id = department.id"); + } + + @Test // GH-1362 + void shouldRenderNestedJoins() { + + Table merchantCustomers = Table.create("merchants_customers"); + Table customerDetails = Table.create("customer_details"); + + Select innerSelect = Select.builder().select(customerDetails.column("cd_user_id")).from(customerDetails) + .join(merchantCustomers) + .on(merchantCustomers.column("mc_user_id").isEqualTo(customerDetails.column("cd_user_id"))).build(); + + InlineQuery innerTable = InlineQuery.create(innerSelect, "inner"); + + Select select = Select.builder().select(merchantCustomers.asterisk()) // + .from(merchantCustomers) // + .join(innerTable).on(innerTable.column("i_user_id").isEqualTo(merchantCustomers.column("mc_user_id"))) // + .build(); + + String sql = SqlRenderer.toString(select); + + assertThat(sql).isEqualTo("SELECT merchants_customers.* FROM merchants_customers " + // + "JOIN (" + // + "SELECT customer_details.cd_user_id " + // + "FROM customer_details " + // + "JOIN merchants_customers ON merchants_customers.mc_user_id = customer_details.cd_user_id" + // + ") inner " + // + "ON inner.i_user_id = merchants_customers.mc_user_id"); + } + + @Test // GH-1003 + void shouldRenderJoinWithTwoInlineQueries() { + + Table employee = SQL.table("employee"); + Table department = SQL.table("department"); + + Select innerSelectOne = Select.builder() + .select(employee.column("id").as("empId"), employee.column("department_Id"), employee.column("name")) + .from(employee).build(); + Select innerSelectTwo = Select.builder().select(department.column("id"), department.column("name")) + .from(department).build(); + + InlineQuery one = InlineQuery.create(innerSelectOne, "one"); + InlineQuery two = InlineQuery.create(innerSelectTwo, "two"); + + Select select = Select.builder().select(one.column("empId"), two.column("name")).from(one) // + .join(two).on(two.column("department_id")).equals(one.column("empId")) // + .build(); + + String sql = SqlRenderer.toString(select); + assertThat(sql).isEqualTo("SELECT one.empId, two.name FROM (" // + + "SELECT employee.id AS empId, employee.department_Id, employee.name FROM employee) one " // + + "JOIN (SELECT department.id, department.name FROM department) two " // + + "ON two.department_id = one.empId"); + } + } /** @@ -742,8 +769,8 @@ class AnalyticFunctionsTests { void renderEmptyOver() { Select select = StatementBuilder.select( // - AnalyticFunction.create("MAX", salary) // - ) // + AnalyticFunction.create("MAX", salary) // + ) // .from(employee) // .build(); diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sqlgeneration/AliasFactoryUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sqlgeneration/AliasFactoryUnitTests.java index 7ec6678f8b..2ed989e332 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/sqlgeneration/AliasFactoryUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sqlgeneration/AliasFactoryUnitTests.java @@ -20,11 +20,13 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.springframework.data.annotation.Id; import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.RelationalMappingContext; /** * Unit tests for the {@link AliasFactory}. + * * @author Jens Schauder */ class AliasFactoryUnitTests { @@ -55,8 +57,8 @@ void aliasSimpleProperty() { @Test // GH-1446 void nameGetsSanitized() { - String alias = aliasFactory.getColumnAlias( - context.getAggregatePath( context.getPersistentPropertyPath("evil", DummyEntity.class))); + String alias = aliasFactory + .getColumnAlias(context.getAggregatePath(context.getPersistentPropertyPath("evil", DummyEntity.class))); assertThat(alias).isEqualTo("c_ameannamecontains3illegal_characters_1"); } @@ -64,10 +66,10 @@ void nameGetsSanitized() { @Test // GH-1446 void aliasIsStable() { - String alias1 = aliasFactory.getColumnAlias( - context.getAggregatePath( context.getRequiredPersistentEntity(DummyEntity.class))); - String alias2 = aliasFactory.getColumnAlias( - context.getAggregatePath( context.getRequiredPersistentEntity(DummyEntity.class))); + String alias1 = aliasFactory + .getColumnAlias(context.getAggregatePath(context.getRequiredPersistentEntity(DummyEntity.class))); + String alias2 = aliasFactory + .getColumnAlias(context.getAggregatePath(context.getRequiredPersistentEntity(DummyEntity.class))); assertThat(alias1).isEqualTo(alias2); } @@ -79,10 +81,10 @@ class RnAlias { @Test // GH-1446 void aliasIsStable() { - String alias1 = aliasFactory.getRowNumberAlias( - context.getAggregatePath(context.getRequiredPersistentEntity(DummyEntity.class))); - String alias2 = aliasFactory.getRowNumberAlias( - context.getAggregatePath( context.getRequiredPersistentEntity(DummyEntity.class))); + String alias1 = aliasFactory + .getRowNumberAlias(context.getAggregatePath(context.getRequiredPersistentEntity(DummyEntity.class))); + String alias2 = aliasFactory + .getRowNumberAlias(context.getAggregatePath(context.getRequiredPersistentEntity(DummyEntity.class))); assertThat(alias1).isEqualTo(alias2); } @@ -90,11 +92,11 @@ void aliasIsStable() { @Test // GH-1446 void aliasProjectsOnTableReferencingPath() { - String alias1 = aliasFactory.getRowNumberAlias( - context.getAggregatePath(context.getRequiredPersistentEntity(DummyEntity.class))); + String alias1 = aliasFactory + .getRowNumberAlias(context.getAggregatePath(context.getRequiredPersistentEntity(DummyEntity.class))); - String alias2 = aliasFactory.getRowNumberAlias( - context.getAggregatePath(context.getPersistentPropertyPath("evil", DummyEntity.class))); + String alias2 = aliasFactory + .getRowNumberAlias(context.getAggregatePath(context.getPersistentPropertyPath("evil", DummyEntity.class))); assertThat(alias1).isEqualTo(alias2); } @@ -102,10 +104,10 @@ void aliasProjectsOnTableReferencingPath() { @Test // GH-1446 void rnAliasIsIndependentOfTableAlias() { - String alias1 = aliasFactory.getRowNumberAlias( - context.getAggregatePath(context.getRequiredPersistentEntity(DummyEntity.class))); - String alias2 = aliasFactory.getColumnAlias( - context.getAggregatePath(context.getRequiredPersistentEntity(DummyEntity.class))); + String alias1 = aliasFactory + .getRowNumberAlias(context.getAggregatePath(context.getRequiredPersistentEntity(DummyEntity.class))); + String alias2 = aliasFactory + .getColumnAlias(context.getAggregatePath(context.getRequiredPersistentEntity(DummyEntity.class))); assertThat(alias1).isNotEqualTo(alias2); } @@ -117,8 +119,8 @@ class BackReferenceAlias { @Test // GH-1446 void testBackReferenceAlias() { - String alias = aliasFactory.getBackReferenceAlias( - context.getAggregatePath(context.getPersistentPropertyPath("dummy", Reference.class))); + String alias = aliasFactory + .getBackReferenceAlias(context.getAggregatePath(context.getPersistentPropertyPath("dummy", Reference.class))); assertThat(alias).isEqualTo("br_dummy_entity_1"); } @@ -129,8 +131,8 @@ class KeyAlias { @Test // GH-1446 void testKeyAlias() { - String alias = aliasFactory.getKeyAlias( - context.getAggregatePath(context.getPersistentPropertyPath("dummy", Reference.class))); + String alias = aliasFactory + .getKeyAlias(context.getAggregatePath(context.getPersistentPropertyPath("dummy", Reference.class))); assertThat(alias).isEqualTo("key_dummy_entity_1"); } @@ -141,11 +143,11 @@ class TableAlias { @Test // GH-1448 void tableAliasIsDifferentForDifferentPathsToSameEntity() { - String alias = aliasFactory.getTableAlias( - context.getAggregatePath(context.getPersistentPropertyPath("dummy", Reference.class))); + String alias = aliasFactory + .getTableAlias(context.getAggregatePath(context.getPersistentPropertyPath("dummy", Reference.class))); - String alias2 = aliasFactory.getTableAlias( - context.getAggregatePath(context.getPersistentPropertyPath("dummy2", Reference.class))); + String alias2 = aliasFactory + .getTableAlias(context.getAggregatePath(context.getPersistentPropertyPath("dummy2", Reference.class))); assertThat(alias).isNotEqualTo(alias2); } @@ -158,6 +160,7 @@ static class DummyEntity { } static class Reference { + @Id Long id; DummyEntity dummy; DummyEntity dummy2; } diff --git a/src/main/antora/modules/ROOT/pages/jdbc/mapping.adoc b/src/main/antora/modules/ROOT/pages/jdbc/mapping.adoc index c3bba01ca0..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..0fb12a5706 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`: an entity with this annotation will be mapped to the table of the parent entity, instead of a separate table. +Allows to specify if the resulting columns should have a common prefix. +If all columns resulting from such an entity are `null` either the annotated entity will be `null` or _empty_, i.e. all of its properties will be `null`, depending on the value of `@Embedded.onEmpty()` +May be combined with `@Id` to form a composite id. * `@Id`: Applied at the field level to mark the primary key. +It may be combined with `@Embedded` to form a composite id. +* `@InsertOnlyProperty`: Marks a property as only to be written during insert. +Such a property on an aggregate root will only be written once and never updated. +Note that on a nested entity, all save operations result in an insert therefore this annotation has no effect on properties of nested entities. +* `@MappedCollection`: Allows for configuration how a collection, or a single nested entity gets mapped. `idColumn` specifies the column used for referencing the parent entities primary key. `keyColumn` specifies the column used to store the index of a `List` or the key of a `Map`. +* `@Sequence`: specify a database sequence for generating values for the annotated property. * `@Table`: Applied at the class level to indicate this class is a candidate for mapping to the database. You can specify the name of the table where the database is stored. * `@Transient`: By default, all fields are mapped to the row. @@ -22,3 +32,5 @@ However, this is not recommended, since it may cause problems with other tools. The value is `null` (`zero` for primitive types) is considered as marker for entities to be new. The initially stored value is `zero` (`one` for primitive types). The version gets incremented automatically on every update. + + diff --git a/src/main/antora/modules/ROOT/partials/mapping.adoc b/src/main/antora/modules/ROOT/partials/mapping.adoc index 7e864516e2..16e0c5b833 100644 --- a/src/main/antora/modules/ROOT/partials/mapping.adoc +++ b/src/main/antora/modules/ROOT/partials/mapping.adoc @@ -88,7 +88,6 @@ endif::[] You may use xref:value-expressions.adoc[Spring Data's SpEL support] to dynamically create column names. Once generated the names will be cached, so it is dynamic per mapping context only. - ifdef::embedded-entities[] [[entity-persistence.embedded-entities]] @@ -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 + +Entities may be annotated with `@Id` and `@Embedded`, resulting in a composite id on the database side. +The full embedded entity is considered the id, and therefore the check for determining if an aggregate is considered a new aggregate requiring an insert or an existing one, asking for an update is based on that entity, not its elements. +Most use cases will require a custom `BeforeConvertCallback` to set the id for new aggregate. + +==== +.Simple entity with composite id +[source,java] +---- +@Table("PERSON_WITH_COMPOSITE_ID") +record Person( <1> + @Id @Embedded.Nullable Name pk, <2> + String nickName, + Integer age +) { +} + +record Name(String first, String last) { +} +---- + +.Matching table for simple entity with composite id +[source,sql] +---- +CREATE TABLE PERSON_WITH_COMPOSITE_ID ( + FIRST VARCHAR(100), + LAST VARCHAR(100), + NICK_NAME VARCHAR(100), + AGE INT, + PRIMARY KEY (FIRST, LAST) <3> +); + + +---- + +<1> Entities may be represented as records without any special consideration +<2> `pk` is marked as id and embedded +<3> the two columns from the embedded `Name` entity make up the primary key in the database. + +Details of the create tables will depend on the database used. +==== + [[entity-persistence.read-only-properties]] == Read Only Properties diff --git a/src/main/resources/notice.txt b/src/main/resources/notice.txt index 2d1d063052..5cdfd0a640 100644 --- a/src/main/resources/notice.txt +++ b/src/main/resources/notice.txt @@ -1,4 +1,4 @@ -Spring Data Relational 3.5 RC1 (2025.0.0) +Spring Data Relational 4.0 M1 (2025.1.0) Copyright (c) [2017-2019] Pivotal Software, Inc. This product is licensed to you under the Apache License, Version 2.0 (the "License").