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 4d210d516da..f3b8e3b6c33 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 @@ -115,6 +115,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(), domainType, insertSubject.getIdentifier(), idValueSource)) @@ -160,7 +161,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); } @@ -181,7 +182,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); @@ -201,8 +202,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); } @@ -262,7 +262,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)); @@ -329,7 +329,7 @@ public Object mapRow(ResultSet rs, int rowNum) throws SQLException { public boolean existsById(Object id, Class domainType) { String existsSql = sql(domainType).getExists(); - SqlParameterSource parameter = sqlParametersFactory.forQueryById(id, domainType, ID_SQL_PARAMETER); + SqlParameterSource parameter = sqlParametersFactory.forQueryById(id, domainType); Boolean result = operations.queryForObject(existsSql, parameter, Boolean.class); Assert.state(result != null, "The result of an exists query must not be null"); diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcBackReferencePropertyValueProvider.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcBackReferencePropertyValueProvider.java deleted file mode 100644 index d5e0a44bb45..00000000000 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcBackReferencePropertyValueProvider.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2020-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.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 bdbdc9267aa..dfcc8a434de 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilder.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilder.java @@ -15,7 +15,13 @@ */ package org.springframework.data.jdbc.core.convert; +import java.util.function.Function; + +import org.springframework.data.mapping.PersistentPropertyPathAccessor; import org.springframework.data.relational.core.mapping.AggregatePath; +import org.springframework.data.relational.core.mapping.RelationalMappingContext; +import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; +import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.util.Assert; /** @@ -41,13 +47,31 @@ public static JdbcIdentifierBuilder empty() { */ public static JdbcIdentifierBuilder forBackReferences(JdbcConverter converter, AggregatePath path, Object value) { - Identifier identifier = Identifier.of( // - path.getTableInfo().reverseColumnInfo().name(), // - value, // - converter.getColumnType(path.getIdDefiningParentPath().getRequiredIdProperty()) // - ); + RelationalPersistentProperty idProperty = path.getIdDefiningParentPath().getRequiredIdProperty(); + AggregatePath.ColumnInfos reverseColumnInfos = path.getTableInfo().reverseColumnInfos(); + + // create property accessor + RelationalMappingContext mappingContext = converter.getMappingContext(); + RelationalPersistentEntity persistentEntity = mappingContext.getPersistentEntity(idProperty.getType()); + + Function valueProvider; + if (persistentEntity == null) { + valueProvider = ap -> value; + } else { + PersistentPropertyPathAccessor propertyPathAccessor = persistentEntity.getPropertyPathAccessor(value); + valueProvider = ap -> propertyPathAccessor.getProperty(ap.getRequiredPersistentPropertyPath()); + } + + final Identifier[] identifierHolder = new Identifier[] { Identifier.empty() }; + + reverseColumnInfos.forEach((ap, ci) -> { + + RelationalPersistentProperty property = ap.getRequiredLeafProperty(); + identifierHolder[0] = identifierHolder[0].withPart(ci.name(), valueProvider.apply(ap), + converter.getColumnType(property)); + }); - return new JdbcIdentifierBuilder(identifier); + return new JdbcIdentifierBuilder(identifierHolder[0]); } /** @@ -62,8 +86,9 @@ public JdbcIdentifierBuilder withQualifier(AggregatePath path, Object value) { Assert.notNull(path, "Path must not be null"); Assert.notNull(value, "Value must not be null"); - identifier = identifier.withPart(path.getTableInfo().qualifierColumnInfo().name(), value, - path.getTableInfo().qualifierColumnType()); + AggregatePath.TableInfo tableInfo = path.getTableInfo(); + identifier = identifier.withPart(tableInfo.qualifierColumnInfo().name(), value, + tableInfo.qualifierColumnType()); return this; } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverter.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverter.java index 2b3ad379456..db2d71edef8 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverter.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverter.java @@ -80,7 +80,7 @@ public class MappingJdbcConverter extends MappingRelationalConverter implements * {@link #MappingJdbcConverter(RelationalMappingContext, RelationResolver, CustomConversions, JdbcTypeFactory)} * (MappingContext, RelationResolver, JdbcTypeFactory)} to convert arrays and large objects into JDBC-specific types. * - * @param context must not be {@literal null}. + * @param context must not be {@literal null}. * @param relationResolver used to fetch additional relations from the database. Must not be {@literal null}. */ public MappingJdbcConverter(RelationalMappingContext context, RelationResolver relationResolver) { @@ -98,12 +98,12 @@ public MappingJdbcConverter(RelationalMappingContext context, RelationResolver r /** * Creates a new {@link MappingJdbcConverter} given {@link MappingContext}. * - * @param context must not be {@literal null}. + * @param context must not be {@literal null}. * @param relationResolver used to fetch additional relations from the database. Must not be {@literal null}. - * @param typeFactory must not be {@literal null} + * @param typeFactory must not be {@literal null} */ public MappingJdbcConverter(RelationalMappingContext context, RelationResolver relationResolver, - CustomConversions conversions, JdbcTypeFactory typeFactory) { + CustomConversions conversions, JdbcTypeFactory typeFactory) { super(context, conversions); @@ -220,7 +220,7 @@ private boolean canWriteAsJdbcValue(@Nullable Object value) { return true; } - if (value instanceof AggregateReference aggregateReference) { + if (value instanceof AggregateReference aggregateReference) { return canWriteAsJdbcValue(aggregateReference.getId()); } @@ -285,7 +285,7 @@ public R readAndResolve(TypeInformation type, RowDocument source, Identif @Override protected RelationalPropertyValueProvider newValueProvider(RowDocumentAccessor documentAccessor, - ValueExpressionEvaluator evaluator, ConversionContext context) { + ValueExpressionEvaluator evaluator, ConversionContext context) { if (context instanceof ResolvingConversionContext rcc) { @@ -314,7 +314,7 @@ class ResolvingRelationalPropertyValueProvider implements RelationalPropertyValu private final Identifier identifier; private ResolvingRelationalPropertyValueProvider(AggregatePathValueProvider delegate, RowDocumentAccessor accessor, - ResolvingConversionContext context, Identifier identifier) { + ResolvingConversionContext context, Identifier identifier) { AggregatePath path = context.aggregatePath(); @@ -323,7 +323,7 @@ private ResolvingRelationalPropertyValueProvider(AggregatePathValueProvider dele this.context = context; this.identifier = path.isEntity() ? potentiallyAppendIdentifier(identifier, path.getRequiredLeafEntity(), - property -> delegate.getValue(path.append(property))) + property -> delegate.getValue(path.append(property))) : identifier; } @@ -331,7 +331,7 @@ private ResolvingRelationalPropertyValueProvider(AggregatePathValueProvider dele * Conditionally append the identifier if the entity has an identifier property. */ static Identifier potentiallyAppendIdentifier(Identifier base, RelationalPersistentEntity entity, - Function getter) { + Function getter) { if (entity.hasIdProperty()) { @@ -361,24 +361,9 @@ public T getPropertyValue(RelationalPersistentProperty property) { if (property.isCollectionLike() || property.isMap()) { - Identifier identifierToUse = this.identifier; - AggregatePath idDefiningParentPath = aggregatePath.getIdDefiningParentPath(); + Identifier identifier = constructIdentifier(aggregatePath); - // note that the idDefiningParentPath might not itself have an id property, but have a combination of back - // references and possibly keys, that form an id - if (idDefiningParentPath.hasIdProperty()) { - - RelationalPersistentProperty identifier = idDefiningParentPath.getRequiredIdProperty(); - AggregatePath idPath = idDefiningParentPath.append(identifier); - Object value = delegate.getValue(idPath); - - Assert.state(value != null, "Identifier value must not be null at this point"); - - identifierToUse = Identifier.of(aggregatePath.getTableInfo().reverseColumnInfo().name(), value, - identifier.getActualType()); - } - - Iterable allByPath = relationResolver.findAllByPath(identifierToUse, + Iterable allByPath = relationResolver.findAllByPath(identifier, aggregatePath.getRequiredPersistentPropertyPath()); if (property.isCollectionLike()) { @@ -403,6 +388,29 @@ public T getPropertyValue(RelationalPersistentProperty property) { return (T) delegate.getValue(aggregatePath); } + private Identifier constructIdentifier(AggregatePath aggregatePath) { + + Identifier identifierToUse = this.identifier; + AggregatePath idDefiningParentPath = aggregatePath.getIdDefiningParentPath(); + + // note that the idDefiningParentPath might not itself have an id property, but have a combination of back + // references and possibly keys, that form an id + if (idDefiningParentPath.hasIdProperty()) { + + RelationalPersistentProperty idProperty = idDefiningParentPath.getRequiredIdProperty(); + AggregatePath idPath = idProperty.isEntity() ? idDefiningParentPath.append(idProperty) : idDefiningParentPath; + Identifier[] buildingIdentifier = new Identifier[] { Identifier.empty() }; + aggregatePath.getTableInfo().reverseColumnInfos().forEach((ap, ci) -> { + + Object value = delegate.getValue(idPath.append(ap)); + buildingIdentifier[0] = buildingIdentifier[0].withPart(ci.name(), value, + ap.getRequiredLeafProperty().getActualType()); + }); + identifierToUse = buildingIdentifier[0]; + } + return identifierToUse; + } + @Override public boolean hasValue(RelationalPersistentProperty property) { @@ -423,7 +431,7 @@ public boolean hasValue(RelationalPersistentProperty property) { return delegate.hasValue(toUse); } - return delegate.hasValue(aggregatePath.getTableInfo().reverseColumnInfo().alias()); + return delegate.hasValue(aggregatePath.getTableInfo().reverseColumnInfos().any().alias()); } return delegate.hasValue(aggregatePath); @@ -449,7 +457,7 @@ public boolean hasNonEmptyValue(RelationalPersistentProperty property) { return delegate.hasValue(toUse); } - return delegate.hasValue(aggregatePath.getTableInfo().reverseColumnInfo().alias()); + return delegate.hasValue(aggregatePath.getTableInfo().reverseColumnInfos().any().alias()); } return delegate.hasNonEmptyValue(aggregatePath); @@ -460,7 +468,7 @@ public RelationalPropertyValueProvider withContext(ConversionContext context) { return context == this.context ? this : new ResolvingRelationalPropertyValueProvider(delegate.withContext(context), accessor, - (ResolvingConversionContext) context, identifier); + (ResolvingConversionContext) context, identifier); } } @@ -472,7 +480,7 @@ public RelationalPropertyValueProvider withContext(ConversionContext context) { * @param identifier */ private record ResolvingConversionContext(ConversionContext delegate, AggregatePath aggregatePath, - Identifier identifier) implements ConversionContext { + Identifier identifier) implements ConversionContext { @Override public S convert(Object source, TypeInformation typeHint) { diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlContext.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlContext.java index 1102e134642..54b2f08ec0e 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlContext.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlContext.java @@ -40,10 +40,6 @@ class SqlContext { this.table = Table.create(entity.getQualifiedTableName()); } - Column getIdColumn() { - return table.column(entity.getIdColumn()); - } - Column getVersionColumn() { return table.column(entity.getRequiredVersionProperty().getColumnName()); } @@ -60,11 +56,20 @@ Table getTable(AggregatePath path) { } Column getColumn(AggregatePath path) { + AggregatePath.ColumnInfo columnInfo = path.getColumnInfo(); return getTable(path).column(columnInfo.name()).as(columnInfo.alias()); } - Column getReverseColumn(AggregatePath path) { - return getTable(path).column(path.getTableInfo().reverseColumnInfo().name()).as(path.getTableInfo().reverseColumnInfo().alias()); + /** + * A token reverse column, used in selects to identify, if an entity is present or {@literal null}. + * + * @param path must not be null. + * @return a {@literal Column} that is part of the effective primary key for the given path. + */ + Column getAnyReverseColumn(AggregatePath path) { + + AggregatePath.ColumnInfo columnInfo = path.getTableInfo().reverseColumnInfos().any(); + return getTable(path).column(columnInfo.name()).as(columnInfo.alias()); } } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java index 4be21ccf34e..18e995f4bab 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlGenerator.java @@ -36,6 +36,7 @@ import org.springframework.data.relational.core.sql.render.RenderContext; import org.springframework.data.relational.core.sql.render.SqlRenderer; import org.springframework.data.util.Lazy; +import org.springframework.data.util.Pair; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -62,9 +63,7 @@ class SqlGenerator { static final SqlIdentifier VERSION_SQL_PARAMETER = SqlIdentifier.unquoted("___oldOptimisticLockingVersion"); - static final SqlIdentifier ID_SQL_PARAMETER = SqlIdentifier.unquoted("id"); static final SqlIdentifier IDS_SQL_PARAMETER = SqlIdentifier.unquoted("ids"); - static final SqlIdentifier ROOT_ID_PARAMETER = SqlIdentifier.unquoted("rootId"); /** * Length of an aggregate path that is one longer then the root path. @@ -73,7 +72,6 @@ class SqlGenerator { private final RelationalPersistentEntity entity; private final RelationalMappingContext mappingContext; - private final RenderContext renderContext; private final SqlContext sqlContext; private final SqlRenderer sqlRenderer; @@ -96,6 +94,10 @@ class SqlGenerator { private final QueryMapper queryMapper; private final Dialect dialect; + private final Function, Condition> inCondition; + private final Function, Condition> equalityCondition; + private final Function, Condition> notNullCondition; + /** * Create a new {@link SqlGenerator} given {@link RelationalMappingContext} and {@link RelationalPersistentEntity}. * @@ -110,11 +112,15 @@ class SqlGenerator { this.mappingContext = mappingContext; this.entity = entity; this.sqlContext = new SqlContext(entity); - this.renderContext = new RenderContextFactory(dialect).createRenderContext(); - this.sqlRenderer = SqlRenderer.create(renderContext); + this.sqlRenderer = SqlRenderer.create(new RenderContextFactory(dialect).createRenderContext()); this.columns = new Columns(entity, mappingContext, converter); this.queryMapper = new QueryMapper(converter); this.dialect = dialect; + + inCondition = inCondition(); + equalityCondition = equalityCondition(); + notNullCondition = isNotNullCondition(); + } /** @@ -156,44 +162,58 @@ private static boolean isDeeplyNested(AggregatePath path) { * given {@literal path} to those that reference the root entities specified by the {@literal rootCondition}. * * @param path specifies the table and id to select - * @param rootCondition the condition on the root of the path determining what to select - * @param filterColumn the column to apply the IN-condition to. + * @param conditionFunction a function for construction a where clause + * @param columns map making all columns available as a map from {@link AggregatePath} * @return the IN condition */ - private Condition getSubselectCondition(AggregatePath path, Function rootCondition, - Column filterColumn) { + private Condition getSubselectCondition(AggregatePath path, + Function, Condition> conditionFunction, Map columns) { AggregatePath parentPath = path.getParentPath(); if (!parentPath.hasIdProperty()) { if (isDeeplyNested(parentPath)) { - return getSubselectCondition(parentPath, rootCondition, filterColumn); + return getSubselectCondition(parentPath, conditionFunction, columns); } - return rootCondition.apply(filterColumn); + return conditionFunction.apply(columns); } - Table subSelectTable = Table.create(parentPath.getTableInfo().qualifiedTableName()); - Column idColumn = subSelectTable.column(parentPath.getTableInfo().idColumnName()); - Column selectFilterColumn = subSelectTable.column(parentPath.getTableInfo().effectiveIdColumnName()); + AggregatePath.TableInfo parentPathTableInfo = parentPath.getTableInfo(); + Table subSelectTable = Table.create(parentPathTableInfo.qualifiedTableName()); + + Map selectFilterColumns = new TreeMap<>(); + parentPathTableInfo.effectiveIdColumnInfos().forEach( // + (ap, ci) -> // + selectFilterColumns.put(ap, subSelectTable.column(ci.name())) // + ); Condition innerCondition; if (isFirstNonRoot(parentPath)) { // if the parent is the root of the path - // apply the rootCondition - innerCondition = rootCondition.apply(selectFilterColumn); + innerCondition = conditionFunction.apply(selectFilterColumns); } else { - // otherwise, we need another layer of subselect - innerCondition = getSubselectCondition(parentPath, rootCondition, selectFilterColumn); + innerCondition = getSubselectCondition(parentPath, conditionFunction, selectFilterColumns); } + List idColumns = parentPathTableInfo.idColumnInfos().toList(ci -> subSelectTable.column(ci.name())); + Select select = Select.builder() // - .select(idColumn) // + .select(idColumns) // .from(subSelectTable) // .where(innerCondition).build(); - return filterColumn.in(select); + return Conditions.in(toExpression(columns), select); + } + + private Expression toExpression(Map columnsMap) { + + if (columnsMap.size() == 1) { + return columnsMap.values().iterator().next(); + } else { + return TupleExpression.create(new ArrayList<>(columnsMap.values())); + } } private BindMarker getBindMarker(SqlIdentifier columnName) { @@ -439,7 +459,7 @@ String createDeleteAllSql(@Nullable PersistentPropertyPath path) { - return createDeleteByPathAndCriteria(mappingContext.getAggregatePath(path), - filterColumn -> filterColumn.isEqualTo(getBindMarker(ROOT_ID_PARAMETER))); + return createDeleteByPathAndCriteria(mappingContext.getAggregatePath(path), equalityCondition); } /** @@ -462,17 +481,82 @@ String createDeleteByPath(PersistentPropertyPath p * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. */ String createDeleteInByPath(PersistentPropertyPath path) { + return createDeleteByPathAndCriteria(mappingContext.getAggregatePath(path), inCondition); + } + + /** + * Constructs a function for constructing a where condition. The where condition will be of the form + * {@literal IN :bind-marker} + */ + private Function, Condition> inCondition() { + + return columnMap -> { + + List columns = List.copyOf(columnMap.values()); + + if (columns.size() == 1) { + return Conditions.in(columns.get(0), getBindMarker(IDS_SQL_PARAMETER)); + } + return Conditions.in(TupleExpression.create(columns), getBindMarker(IDS_SQL_PARAMETER)); + }; + } + + /** + * Constructs a function for constructing a where. The where condition will be of the form + * {@literal = :bind-marker-a AND = :bind-marker-b ...} + */ + private Function, Condition> equalityCondition() { + + AggregatePath.ColumnInfos idColumnInfos = mappingContext.getAggregatePath(entity).getTableInfo().idColumnInfos(); - return createDeleteByPathAndCriteria(mappingContext.getAggregatePath(path), - filterColumn -> filterColumn.in(getBindMarker(IDS_SQL_PARAMETER))); + return columnMap -> { + + Condition result = null; + for (Map.Entry entry : columnMap.entrySet()) { + BindMarker bindMarker = getBindMarker(idColumnInfos.get(entry.getKey()).name()); + Comparison singleCondition = entry.getValue().isEqualTo(bindMarker); + + result = result == null ? singleCondition : result.and(singleCondition); + } + return result; + }; + } + + /** + * Constructs a function for constructing where a condition. The where condition will be of the form + * {@literal IS NOT NULL AND IS NOT NULL ... } + */ + private Function, Condition> isNotNullCondition() { + + return columnMap -> { + + Condition result = null; + for (Column column : columnMap.values()) { + Condition singleCondition = column.isNotNull(); + + result = result == null ? singleCondition : result.and(singleCondition); + } + return result; + }; } private String createFindOneSql() { - Select select = selectBuilder().where(getIdColumn().isEqualTo(getBindMarker(ID_SQL_PARAMETER))) // - .build(); + return render(selectBuilder().where(equalityIdWhereCondition()).build()); + } - return render(select); + private Condition equalityIdWhereCondition() { + + Condition aggregate = null; + for (Column column : getIdColumns()) { + + Comparison condition = column.isEqualTo(getBindMarker(column.getName())); + aggregate = aggregate == null ? condition : aggregate.and(condition); + } + + Assert.state(aggregate != null, "We need at least one id column"); + + return aggregate; } private String createAcquireLockById(LockMode lockMode) { @@ -480,9 +564,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 +578,7 @@ private String createAcquireLockAll(LockMode lockMode) { Table table = this.getTable(); Select select = StatementBuilder // - .select(getIdColumn()) // + .select(getSingleNonNullColumn()) // .from(table) // .lock(lockMode) // .build(); @@ -538,14 +622,24 @@ private SelectBuilder.SelectWhere selectBuilder(Collection keyCol columnExpressions.add(table.column(keyColumn).as(keyColumn)); } - SelectBuilder.SelectAndFrom selectBuilder = StatementBuilder.select(columnExpressions); - SelectBuilder.SelectJoin baseSelect = selectBuilder.from(table); + SelectBuilder.SelectJoin baseSelect = StatementBuilder.select(columnExpressions).from(table); + + return (SelectBuilder.SelectWhere) addJoins(baseSelect, joinTables); + } + + private static SelectBuilder.SelectJoin addJoins(SelectBuilder.SelectJoin baseSelect, List joinTables) { for (Join join : joinTables) { - baseSelect = baseSelect.leftOuterJoin(join.joinTable).on(join.joinColumn).equals(join.parentId); - } - return (SelectBuilder.SelectWhere) baseSelect; + Condition condition = null; + for (Pair columnPair : join.columns) { + Comparison elementalCondition = columnPair.getFirst().isEqualTo(columnPair.getSecond()); + condition = condition == null ? elementalCondition : condition.and(elementalCondition); + } + + baseSelect = baseSelect.leftOuterJoin(join.joinTable).on(Objects.requireNonNull(condition)); + } + return baseSelect; } private SelectBuilder.SelectOrdered selectBuilder(Collection keyColumns, Sort sort, @@ -605,7 +699,7 @@ Column getColumn(AggregatePath path) { return null; } - return sqlContext.getReverseColumn(path); + return sqlContext.getAnyReverseColumn(path); } return sqlContext.getColumn(path); @@ -619,32 +713,46 @@ Join getJoin(AggregatePath path) { } Table currentTable = sqlContext.getTable(path); + AggregatePath.ColumnInfos backRefColumnInfos = path.getTableInfo().reverseColumnInfos(); AggregatePath idDefiningParentPath = path.getIdDefiningParentPath(); Table parentTable = sqlContext.getTable(idDefiningParentPath); + AggregatePath.ColumnInfos idColumnInfos = idDefiningParentPath.getTableInfo().idColumnInfos(); + + List> joinConditions = new ArrayList<>(); + backRefColumnInfos.forEach((ap, ci) -> { + joinConditions.add(Pair.of(currentTable.column(ci.name()), parentTable.column(idColumnInfos.get(ap).name()))); + }); return new Join( // currentTable, // - currentTable.column(path.getTableInfo().reverseColumnInfo().name()), // - parentTable.column(idDefiningParentPath.getTableInfo().idColumnName()) // + joinConditions // ); } private String createFindAllInListSql() { - Select select = selectBuilder().where(getIdColumn().in(getBindMarker(IDS_SQL_PARAMETER))).build(); + In condition = idInWhereClause(); + Select select = selectBuilder().where(condition).build(); return render(select); } - private String createExistsSql() { + private In idInWhereClause() { + + List idColumns = getIdColumns(); + Expression expression = idColumns.size() == 1 ? idColumns.get(0) : TupleExpression.create(idColumns); + + return Conditions.in(expression, getBindMarker(IDS_SQL_PARAMETER)); + } + private String createExistsSql() { Table table = getTable(); Select select = StatementBuilder // - .select(Functions.count(getIdColumn())) // + .select(Functions.count(getSingleNonNullColumn())) // .from(table) // - .where(getIdColumn().isEqualTo(getBindMarker(ID_SQL_PARAMETER))) // + .where(equalityIdWhereCondition()) // .build(); return render(select); @@ -715,7 +823,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 +846,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 +864,18 @@ private String createDeleteByPathAndCriteria(AggregatePath path, Function columns = new TreeMap<>(); + AggregatePath.ColumnInfos columnInfos = path.getTableInfo().reverseColumnInfos(); + columnInfos.forEach((ag, ci) -> columns.put(ag, table.column(ci.name()))); if (isFirstNonRoot(path)) { delete = builder // - .where(rootCondition.apply(filterColumn)) // + .where(multiIdCondition.apply(columns)) // .build(); } else { - Condition condition = getSubselectCondition(path, rootCondition, filterColumn); + Condition condition = getSubselectCondition(path, multiIdCondition, columns); delete = builder.where(condition).build(); } @@ -777,7 +888,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 +914,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 +1086,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 +1096,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 +1122,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 +1132,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 +1176,7 @@ SelectBuilder.SelectOrdered applyCriteria(@Nullable CriteriaDefinition criteria, /** * Value object representing a {@code JOIN} association. */ - static final class Join { - - private final Table joinTable; - private final Column joinColumn; - private final Column parentId; - - Join(Table joinTable, Column joinColumn, Column parentId) { - - Assert.notNull(joinTable, "JoinTable must not be null"); - Assert.notNull(joinColumn, "JoinColumn must not be null"); - Assert.notNull(parentId, "ParentId must not be null"); - - this.joinTable = joinTable; - this.joinColumn = joinColumn; - this.parentId = parentId; - } - - Table getJoinTable() { - return this.joinTable; - } - - Column getJoinColumn() { - return this.joinColumn; - } - - Column getParentId() { - return this.parentId; - } - - @Override - public boolean equals(@Nullable Object o) { - - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Join join = (Join) o; - return joinTable.equals(join.joinTable) && joinColumn.equals(join.joinColumn) && parentId.equals(join.parentId); - } - - @Override - public int hashCode() { - return Objects.hash(joinTable, joinColumn, parentId); - } - - @Override - public String toString() { - - return "Join{" + // - "joinTable=" + joinTable + // - ", joinColumn=" + joinColumn + // - ", parentId=" + parentId + // - '}'; - } + record Join(Table joinTable, List> columns) { } /** diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlParametersFactory.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlParametersFactory.java index 94f90de501f..b1a6c72d078 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlParametersFactory.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlParametersFactory.java @@ -25,7 +25,9 @@ import org.springframework.data.jdbc.support.JdbcUtil; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.PersistentPropertyPathAccessor; import org.springframework.data.relational.core.conversion.IdValueSource; +import org.springframework.data.relational.core.mapping.AggregatePath; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; @@ -78,9 +80,15 @@ SqlIdentifierParameterSource forInsert(T instance, Class domainType, Iden if (IdValueSource.PROVIDED.equals(idValueSource)) { - RelationalPersistentProperty idProperty = persistentEntity.getRequiredIdProperty(); - Object idValue = persistentEntity.getIdentifierAccessor(instance).getRequiredIdentifier(); - addConvertedPropertyValue(parameterSource, idProperty, idValue, idProperty.getColumnName()); + PersistentPropertyPathAccessor propertyPathAccessor = persistentEntity.getPropertyPathAccessor(instance); + + AggregatePath.ColumnInfos columnInfos = context.getAggregatePath(persistentEntity).getTableInfo().idColumnInfos(); + columnInfos.forEach((ap, __) -> { + Object idValue = propertyPathAccessor.getProperty(columnInfos.fullPath(ap).getRequiredPersistentPropertyPath()); + RelationalPersistentProperty idProperty = ap.getRequiredLeafProperty(); + addConvertedPropertyValue(parameterSource, idProperty, idValue, idProperty.getColumnName()); + }); + } return parameterSource; } @@ -104,20 +112,40 @@ SqlIdentifierParameterSource forUpdate(T instance, Class domainType) { * * @param id the entity id. Must not be {@code null}. * @param domainType the type of the instance. Must not be {@code null}. - * @param name the name to be used for the id parameter. * @return the {@link SqlIdentifierParameterSource} for the query. Guaranteed to not be {@code null}. * @since 2.4 */ - SqlIdentifierParameterSource forQueryById(Object id, Class domainType, SqlIdentifier name) { + SqlIdentifierParameterSource forQueryById(Object id, Class domainType) { SqlIdentifierParameterSource parameterSource = new SqlIdentifierParameterSource(); - addConvertedPropertyValue( // - parameterSource, // - getRequiredPersistentEntity(domainType).getRequiredIdProperty(), // - id, // - name // - ); + RelationalPersistentEntity entity = getRequiredPersistentEntity(domainType); + RelationalPersistentProperty singleIdProperty = entity.getRequiredIdProperty(); + + if (singleIdProperty.isEntity()) { + + RelationalPersistentEntity complexId = context.getPersistentEntity(singleIdProperty); + PersistentPropertyPathAccessor accessor = complexId.getPropertyPathAccessor(id); + + context.getAggregatePath(entity).getTableInfo().idColumnInfos().forEach((ap, ci) -> { + Object idValue = accessor.getProperty(ap.getRequiredPersistentPropertyPath()); + + addConvertedPropertyValue( // + parameterSource, // + ap.getRequiredLeafProperty(), // + idValue, // + ci.name() // + ); + }); + } else { + + addConvertedPropertyValue( // + parameterSource, // + singleIdProperty, // + id, // + singleIdProperty.getColumnName() // + ); + } return parameterSource; } @@ -133,9 +161,32 @@ SqlIdentifierParameterSource forQueryByIds(Iterable ids, Class domainT SqlIdentifierParameterSource parameterSource = new SqlIdentifierParameterSource(); - addConvertedPropertyValuesAsList(parameterSource, getRequiredPersistentEntity(domainType).getRequiredIdProperty(), - ids); + RelationalPersistentEntity entity = context.getPersistentEntity(domainType); + RelationalPersistentProperty singleIdProperty = entity.getRequiredIdProperty(); + + if (singleIdProperty.isEntity()) { + + RelationalPersistentEntity complexId = context.getPersistentEntity(singleIdProperty); + AggregatePath.ColumnInfos idColumnInfos = context.getAggregatePath(entity).getTableInfo().idColumnInfos(); + + List parameterValues = new ArrayList<>(); + for (Object id : ids) { + + PersistentPropertyPathAccessor accessor = complexId.getPropertyPathAccessor(id); + + List tupleList = new ArrayList<>(); + idColumnInfos.forEach((ap, ci) -> { + tupleList.add(accessor.getProperty(ap.getRequiredPersistentPropertyPath())); + }); + parameterValues.add(tupleList.toArray(new Object[0])); + } + + parameterSource.addValue(SqlGenerator.IDS_SQL_PARAMETER, parameterValues); + } else { + addConvertedPropertyValuesAsList(parameterSource, getRequiredPersistentEntity(domainType).getRequiredIdProperty(), + ids); + } return parameterSource; } @@ -156,21 +207,6 @@ SqlIdentifierParameterSource forQueryByIdentifier(Identifier identifier) { return parameterSource; } - /** - * Utility to create {@link Predicate}s. - */ - static class Predicates { - - /** - * Include all {@link Predicate} returning {@literal false} to never skip a property. - * - * @return the include all {@link Predicate}. - */ - static Predicate includeAll() { - return it -> false; - } - } - private void addConvertedPropertyValue(SqlIdentifierParameterSource parameterSource, RelationalPersistentProperty property, @Nullable Object value, SqlIdentifier name) { diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java index 8351db58b7b..6e21bc50662 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java @@ -17,7 +17,6 @@ import java.util.ArrayList; import java.util.List; -import java.util.Objects; import java.util.Optional; import org.springframework.data.domain.Pageable; @@ -32,14 +31,7 @@ import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.core.query.Criteria; -import org.springframework.data.relational.core.sql.Column; -import org.springframework.data.relational.core.sql.Expression; -import org.springframework.data.relational.core.sql.Expressions; -import org.springframework.data.relational.core.sql.Functions; -import org.springframework.data.relational.core.sql.Select; -import org.springframework.data.relational.core.sql.SelectBuilder; -import org.springframework.data.relational.core.sql.StatementBuilder; -import org.springframework.data.relational.core.sql.Table; +import org.springframework.data.relational.core.sql.*; import org.springframework.data.relational.core.sql.render.SqlRenderer; import org.springframework.data.relational.repository.Lock; import org.springframework.data.relational.repository.query.RelationalEntityMetadata; @@ -222,7 +214,8 @@ SelectBuilder.SelectLimitOffset createSelectClause(RelationalPersistentEntity SelectBuilder.SelectJoin builder; if (tree.isExistsProjection()) { - Column idColumn = table.column(entity.getIdColumn()); + AggregatePath.ColumnInfo anyIdColumnInfo = context.getAggregatePath(entity).getTableInfo().idColumnInfos().any(); + Column idColumn = table.column(anyIdColumnInfo.name()); builder = Select.builder().select(idColumn).from(table); } else if (tree.isCountProjection()) { builder = Select.builder().select(Functions.count(Expressions.asterisk())).from(table); @@ -237,7 +230,7 @@ private SelectBuilder.SelectJoin selectBuilder(Table table) { List columnExpressions = new ArrayList<>(); RelationalPersistentEntity entity = entityMetadata.getTableEntity(); - SqlContext sqlContext = new SqlContext(entity); + SqlContext sqlContext = new SqlContext(); List joinTables = new ArrayList<>(); for (PersistentPropertyPath path : context @@ -267,7 +260,19 @@ private SelectBuilder.SelectJoin selectBuilder(Table table) { SelectBuilder.SelectJoin baseSelect = selectBuilder.from(table); for (Join join : joinTables) { - baseSelect = baseSelect.leftOuterJoin(join.joinTable).on(join.joinColumn).equals(join.parentId); + + Condition condition = null; + + for (int i = 0; i < join.joinColumns.size(); i++) { + Column parentColumn = join.parentId.get(i); + Column joinColumn = join.joinColumns.get(i); + Comparison singleCondition = joinColumn.isEqualTo(parentColumn); + condition = condition == null ? singleCondition : condition.and(singleCondition); + } + + Assert.state(condition != null, "No condition found"); + + baseSelect = baseSelect.leftOuterJoin(join.joinTable).on(condition); } return baseSelect; @@ -276,7 +281,7 @@ private SelectBuilder.SelectJoin selectBuilder(Table table) { /** * Create a {@link Column} for {@link AggregatePath}. * - * @param sqlContext + * @param sqlContext for generating SQL constructs. * @param path the path to the column in question. * @return the statement as a {@link String}. Guaranteed to be not {@literal null}. */ @@ -293,9 +298,6 @@ private Column getColumn(SqlContext sqlContext, AggregatePath path) { if (path.isEntity()) { - // Simple entities without id include there backreference as an synthetic id in order to distinguish null entities - // from entities with only null values. - if (path.isQualified() // || path.isCollectionLike() // || path.hasIdProperty() // @@ -303,7 +305,9 @@ private Column getColumn(SqlContext sqlContext, AggregatePath path) { return null; } - return sqlContext.getReverseColumn(path); + // Simple entities without id include there backreference as an synthetic id in order to distinguish null entities + // from entities with only null values. + return sqlContext.getAnyReverseColumn(path); } return sqlContext.getColumn(path); @@ -321,53 +325,25 @@ Join getJoin(SqlContext sqlContext, AggregatePath path) { AggregatePath idDefiningParentPath = path.getIdDefiningParentPath(); Table parentTable = sqlContext.getTable(idDefiningParentPath); + List reverseColumns = path.getTableInfo().reverseColumnInfos().toList(ci -> currentTable.column(ci.name())); + List idColumns = idDefiningParentPath.getTableInfo().idColumnInfos() + .toList(ci -> parentTable.column(ci.name())); return new Join( // currentTable, // - currentTable.column(path.getTableInfo().reverseColumnInfo().name()), // - parentTable.column(idDefiningParentPath.getTableInfo().idColumnName()) // + reverseColumns, // + idColumns // ); } /** * Value object representing a {@code JOIN} association. */ - static private final class Join { + private record Join(Table joinTable, List joinColumns, List parentId) { - private final Table joinTable; - private final Column joinColumn; - private final Column parentId; - - Join(Table joinTable, Column joinColumn, Column parentId) { - - Assert.notNull(joinTable, "JoinTable must not be null"); - Assert.notNull(joinColumn, "JoinColumn must not be null"); - Assert.notNull(parentId, "ParentId must not be null"); - - this.joinTable = joinTable; - this.joinColumn = joinColumn; - this.parentId = parentId; - } - - @Override - public boolean equals(@Nullable Object o) { - - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - Join join = (Join) o; - return joinTable.equals(join.joinTable) && joinColumn.equals(join.joinColumn) && parentId.equals(join.parentId); - } - - @Override - public int hashCode() { - return Objects.hash(joinTable, joinColumn, parentId); + Join { + Assert.isTrue(joinColumns.size() == parentId.size(), + "Both sides of a join condition must have the same number of columns"); } - @Override - public String toString() { - - return "Join{" + "joinTable=" + joinTable + ", joinColumn=" + joinColumn + ", parentId=" + parentId + '}'; - } } } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/SqlContext.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/SqlContext.java index dcab7323738..60b6ba12052 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,23 @@ */ class SqlContext { - private final RelationalPersistentEntity entity; - private final Table table; - - SqlContext(RelationalPersistentEntity entity) { - - this.entity = entity; - this.table = Table.create(entity.getQualifiedTableName()); - } - - Column getIdColumn() { - return table.column(entity.getIdColumn()); - } - - Column getVersionColumn() { - return table.column(entity.getRequiredVersionProperty().getColumnName()); - } - - Table getTable() { - return table; - } - Table getTable(AggregatePath path) { - SqlIdentifier tableAlias = path.getTableInfo().tableAlias(); - Table table = Table.create(path.getTableInfo().qualifiedTableName()); + AggregatePath.TableInfo tableInfo = path.getTableInfo(); + SqlIdentifier tableAlias = tableInfo.tableAlias(); + Table table = Table.create(tableInfo.qualifiedTableName()); return tableAlias == null ? table : table.as(tableAlias); } Column getColumn(AggregatePath path) { + AggregatePath.ColumnInfo columnInfo = path.getColumnInfo(); - AggregatePath.ColumnInfo columnInfo1 = path.getColumnInfo(); - return getTable(path).column(columnInfo1.name()).as(columnInfo.alias()); + 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()); + Column getAnyReverseColumn(AggregatePath path) { + + AggregatePath.ColumnInfo anyReverseColumnInfo = path.getTableInfo().reverseColumnInfos().any(); + return getTable(path).column(anyReverseColumnInfo.name()).as(anyReverseColumnInfo.alias()); } } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AbstractJdbcAggregateTemplateIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AbstractJdbcAggregateTemplateIntegrationTests.java index d1a085bd264..0dc51d49dc2 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AbstractJdbcAggregateTemplateIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/AbstractJdbcAggregateTemplateIntegrationTests.java @@ -28,7 +28,6 @@ import java.util.function.Function; import java.util.stream.IntStream; -import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; @@ -686,7 +685,7 @@ void saveAndLoadAnEntityWithArray() { void saveAndLoadAnEntityWithEmptyArray() { ArrayOwner arrayOwner = new ArrayOwner(); - arrayOwner.digits = new String[] { }; + arrayOwner.digits = new String[] {}; ArrayOwner saved = template.save(arrayOwner); @@ -810,7 +809,7 @@ void saveAndLoadAnEntityWithSet() { assertThat(reloaded.digits).isEqualTo(new HashSet<>(asList("one", "two", "three"))); } - @Test //GH-1737 + @Test // GH-1737 @EnabledOnFeature(SUPPORTS_ARRAYS) void saveAndLoadEmbeddedArray() { @@ -825,7 +824,7 @@ void saveAndLoadEmbeddedArray() { assertThat(reloaded.embeddedStringList.digits).containsExactly("one", "two", "three"); } - @Test //GH-1737 + @Test // GH-1737 @EnabledOnFeature(SUPPORTS_ARRAYS) void saveAndLoadEmptyEmbeddedArray() { diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/CompositeIdAggregateTemplateHsqlIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/CompositeIdAggregateTemplateHsqlIntegrationTests.java new file mode 100644 index 00000000000..dc105248786 --- /dev/null +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/CompositeIdAggregateTemplateHsqlIntegrationTests.java @@ -0,0 +1,258 @@ +/* + * Copyright 2017-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.core; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.data.annotation.Id; +import org.springframework.data.jdbc.core.convert.DataAccessStrategy; +import org.springframework.data.jdbc.core.convert.JdbcConverter; +import org.springframework.data.jdbc.testing.DatabaseType; +import org.springframework.data.jdbc.testing.EnabledOnDatabase; +import org.springframework.data.jdbc.testing.IntegrationTest; +import org.springframework.data.jdbc.testing.TestConfiguration; +import org.springframework.data.relational.core.mapping.Embedded; +import org.springframework.data.relational.core.mapping.RelationalMappingContext; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; + +/** + * Integration tests for {@link JdbcAggregateTemplate} and it's handling of entities with embedded entities as keys. + * + * @author Jens Schauder + */ +@IntegrationTest +@EnabledOnDatabase(DatabaseType.HSQL) +public class CompositeIdAggregateTemplateHsqlIntegrationTests { + + @Autowired JdbcAggregateOperations template; + @Autowired private NamedParameterJdbcOperations namedParameterJdbcTemplate; + + @Test // GH-574 + void saveAndLoadSimpleEntity() { + + SimpleEntity entity = template.insert(new SimpleEntity(new WrappedPk(23L), "alpha")); + + assertThat(entity.wrappedPk).isNotNull() // + .extracting(WrappedPk::id).isNotNull(); + + SimpleEntity reloaded = template.findById(entity.wrappedPk, SimpleEntity.class); + + assertThat(reloaded).isEqualTo(entity); + } + + @Test // GH-574 + void saveAndLoadEntityWithList() { + + WithList entity = template + .insert(new WithList(new WrappedPk(23L), "alpha", List.of(new Child("Romulus"), new Child("Remus")))); + + assertThat(entity.wrappedPk).isNotNull() // + .extracting(WrappedPk::id).isNotNull(); + + WithList reloaded = template.findById(entity.wrappedPk, WithList.class); + + assertThat(reloaded).isEqualTo(entity); + } + + @Test // GH-574 + void saveAndLoadSimpleEntityWithEmbeddedPk() { + + SimpleEntityWithEmbeddedPk entity = template + .insert(new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "x"), "alpha")); + + SimpleEntityWithEmbeddedPk reloaded = template.findById(entity.embeddedPk, SimpleEntityWithEmbeddedPk.class); + + assertThat(reloaded).isEqualTo(entity); + } + + @Test // GH-574 + void saveAndLoadSimpleEntitiesWithEmbeddedPk() { + + List entities = (List) template + .insertAll(List.of(new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "x"), "alpha"), + new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "y"), "beta"), + new SimpleEntityWithEmbeddedPk(new EmbeddedPk(24L, "y"), "gamma"))); + + List firstTwoPks = entities.stream().limit(2).map(SimpleEntityWithEmbeddedPk::embeddedPk).toList(); + Iterable reloaded = template.findAllById(firstTwoPks, SimpleEntityWithEmbeddedPk.class); + + assertThat(reloaded).containsExactlyInAnyOrder(entities.get(0), entities.get(1)); + } + + @Test // GH-574 + void deleteSingleSimpleEntityWithEmbeddedPk() { + + List entities = (List) template + .insertAll(List.of(new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "x"), "alpha"), + new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "y"), "beta"), + new SimpleEntityWithEmbeddedPk(new EmbeddedPk(24L, "y"), "gamma"))); + + template.delete(entities.get(1)); + + Iterable reloaded = template.findAll(SimpleEntityWithEmbeddedPk.class); + + assertThat(reloaded).containsExactlyInAnyOrder(entities.get(0), entities.get(2)); + } + + @Test // GH-574 + void deleteMultipleSimpleEntityWithEmbeddedPk() { + + List entities = (List) template + .insertAll(List.of(new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "x"), "alpha"), + new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "y"), "beta"), + new SimpleEntityWithEmbeddedPk(new EmbeddedPk(24L, "y"), "gamma"))); + + template.deleteAll(List.of(entities.get(1), entities.get(0))); + + Iterable reloaded = template.findAll(SimpleEntityWithEmbeddedPk.class); + + assertThat(reloaded).containsExactly(entities.get(2)); + } + + @Test // GH-574 + void existsSingleSimpleEntityWithEmbeddedPk() { + + List entities = (List) template + .insertAll(List.of(new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "x"), "alpha"), + new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "y"), "beta"), + new SimpleEntityWithEmbeddedPk(new EmbeddedPk(24L, "y"), "gamma"))); + + assertThat(template.existsById(entities.get(1).embeddedPk, SimpleEntityWithEmbeddedPk.class)).isTrue(); + assertThat(template.existsById(new EmbeddedPk(24L, "x"), SimpleEntityWithEmbeddedPk.class)).isFalse(); + + } + + @Test // GH-574 + void updateSingleSimpleEntityWithEmbeddedPk() { + + List entities = (List) template + .insertAll(List.of(new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "x"), "alpha"), + new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "y"), "beta"), + new SimpleEntityWithEmbeddedPk(new EmbeddedPk(24L, "y"), "gamma"))); + + SimpleEntityWithEmbeddedPk updated = new SimpleEntityWithEmbeddedPk(new EmbeddedPk(23L, "x"), "ALPHA"); + template.save(updated); + + Iterable reloaded = template.findAll(SimpleEntityWithEmbeddedPk.class); + + assertThat(reloaded).containsExactlyInAnyOrder(updated, entities.get(1), entities.get(2)); + } + + @Test // GH-574 + void saveAndLoadSingleReferenceAggregate() { + + SingleReference entity = template.insert(new SingleReference(new EmbeddedPk(23L, "x"), "alpha", new Child("Alf"))); + + SingleReference reloaded = template.findById(entity.embeddedPk, SingleReference.class); + + assertThat(reloaded).isEqualTo(entity); + } + + @Test // GH-574 + void updateSingleReferenceAggregate() { + + EmbeddedPk id = new EmbeddedPk(23L, "x"); + template.insert(new SingleReference(id, "alpha", new Child("Alf"))); + + SingleReference updated = new SingleReference(id, "beta", new Child("Barny")); + template.save(updated); + + List all = template.findAll(SingleReference.class); + + assertThat(all).containsExactly(updated); + } + + @Test // GH-574 + void saveAndLoadWithListAndCompositeId() { + + WithListAndCompositeId entity = template.insert( // + new WithListAndCompositeId( // + new EmbeddedPk(23L, "x"), "alpha", // + List.of( // + new Child("Alf"), // + new Child("Bob"), // + new Child("Flo") // + ) // + ) // + ); + + WithListAndCompositeId reloaded = template.findById(entity.embeddedPk, WithListAndCompositeId.class); + + assertThat(reloaded).isEqualTo(entity); + } + + private record WrappedPk(Long id) { + } + + private record SimpleEntity( // + @Id @Embedded(onEmpty = Embedded.OnEmpty.USE_NULL) WrappedPk wrappedPk, // + String name // + ) { + } + + private record Child(String name) { + } + + private record WithList( // + @Id @Embedded(onEmpty = Embedded.OnEmpty.USE_NULL) WrappedPk wrappedPk, // + String name, List children) { + } + + private record EmbeddedPk(Long one, String two) { + } + + private record SimpleEntityWithEmbeddedPk( // + @Id @Embedded(onEmpty = Embedded.OnEmpty.USE_NULL) EmbeddedPk embeddedPk, // + String name // + ) { + } + + private record SingleReference( // + @Id @Embedded(onEmpty = Embedded.OnEmpty.USE_NULL) EmbeddedPk embeddedPk, // + String name, // + Child child) { + } + + private record WithListAndCompositeId( // + @Id @Embedded(onEmpty = Embedded.OnEmpty.USE_NULL) EmbeddedPk embeddedPk, // + String name, // + List child) { + } + + @Configuration + @Import(TestConfiguration.class) + static class Config { + + @Bean + Class testClass() { + return CompositeIdAggregateTemplateHsqlIntegrationTests.class; + } + + @Bean + JdbcAggregateOperations operations(ApplicationEventPublisher publisher, RelationalMappingContext context, + DataAccessStrategy dataAccessStrategy, JdbcConverter converter) { + return new JdbcAggregateTemplate(publisher, context, converter, dataAccessStrategy); + } + } +} diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilderUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilderUnitTests.java index 5cb72019b48..ad0802d45a0 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilderUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilderUnitTests.java @@ -22,11 +22,13 @@ import java.util.Map; import java.util.UUID; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.core.PersistentPropertyPathTestUtils; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.relational.core.mapping.AggregatePath; +import org.springframework.data.relational.core.mapping.Embedded; /** * Unit tests for the {@link JdbcIdentifierBuilder}. @@ -40,90 +42,144 @@ public class JdbcIdentifierBuilderUnitTests { throw new UnsupportedOperationException(); }); - @Test // DATAJDBC-326 - public void parametersWithPropertyKeysUseTheParentPropertyJdbcType() { - - Identifier identifier = JdbcIdentifierBuilder.forBackReferences(converter, getPath("child"), "eins").build(); - - assertThat(identifier.getParts()) // - .extracting("name", "value", "targetType") // - .containsExactly( // - tuple(quoted("DUMMY_ENTITY"), "eins", UUID.class) // - ); + @Nested + class WithSimpleId { + @Test // DATAJDBC-326 + void parametersWithPropertyKeysUseTheParentPropertyJdbcType() { + + Identifier identifier = JdbcIdentifierBuilder.forBackReferences(converter, getPath("child"), "eins").build(); + + assertThat(identifier.getParts()) // + .extracting("name", "value", "targetType") // + .containsExactly( // + tuple(quoted("DUMMY_ENTITY"), "eins", UUID.class) // + ); + } + + @Test // DATAJDBC-326 + void qualifiersForMaps() { + + AggregatePath path = getPath("children"); + + Identifier identifier = JdbcIdentifierBuilder // + .forBackReferences(converter, path, "parent-eins") // + .withQualifier(path, "map-key-eins") // + .build(); + + assertThat(identifier.getParts()) // + .extracting("name", "value", "targetType") // + .containsExactlyInAnyOrder( // + tuple(quoted("DUMMY_ENTITY"), "parent-eins", UUID.class), // + tuple(quoted("DUMMY_ENTITY_KEY"), "map-key-eins", String.class) // + ); + } + + @Test // DATAJDBC-326 + void qualifiersForLists() { + + AggregatePath path = getPath("moreChildren"); + + Identifier identifier = JdbcIdentifierBuilder // + .forBackReferences(converter, path, "parent-eins") // + .withQualifier(path, "list-index-eins") // + .build(); + + assertThat(identifier.getParts()) // + .extracting("name", "value", "targetType") // + .containsExactlyInAnyOrder( // + tuple(quoted("DUMMY_ENTITY"), "parent-eins", UUID.class), // + tuple(quoted("DUMMY_ENTITY_KEY"), "list-index-eins", Integer.class) // + ); + } + + @Test // DATAJDBC-326 + void backreferenceAcrossEmbeddable() { + + Identifier identifier = JdbcIdentifierBuilder // + .forBackReferences(converter, getPath("embeddable.child"), "parent-eins") // + .build(); + + assertThat(identifier.getParts()) // + .extracting("name", "value", "targetType") // + .containsExactly( // + tuple(quoted("DUMMY_ENTITY"), "parent-eins", UUID.class) // + ); + } + + @Test // DATAJDBC-326 + void backreferenceAcrossNoId() { + + Identifier identifier = JdbcIdentifierBuilder // + .forBackReferences(converter, getPath("noId.child"), "parent-eins") // + .build(); + + assertThat(identifier.getParts()) // + .extracting("name", "value", "targetType") // + .containsExactly( // + tuple(quoted("DUMMY_ENTITY"), "parent-eins", UUID.class) // + ); + } + + private AggregatePath getPath(String dotPath) { + return JdbcIdentifierBuilderUnitTests.this.getPath(dotPath, DummyEntity.class); + } } - @Test // DATAJDBC-326 - public void qualifiersForMaps() { + @Nested + class WithCompositeId { - AggregatePath path = getPath("children"); + CompositeId exampleId = new CompositeId("parent-eins", 23); - Identifier identifier = JdbcIdentifierBuilder // - .forBackReferences(converter, path, "parent-eins") // - .withQualifier(path, "map-key-eins") // - .build(); + @Test // GH-574 + void forBackReferences() { - assertThat(identifier.getParts()) // - .extracting("name", "value", "targetType") // - .containsExactlyInAnyOrder( // - tuple(quoted("DUMMY_ENTITY"), "parent-eins", UUID.class), // - tuple(quoted("DUMMY_ENTITY_KEY"), "map-key-eins", String.class) // - ); - } + AggregatePath path = getPath("children"); - @Test // DATAJDBC-326 - public void qualifiersForLists() { + Identifier identifier = JdbcIdentifierBuilder // + .forBackReferences(converter, path, exampleId) // + .build(); - AggregatePath path = getPath("moreChildren"); + assertThat(identifier.getParts()) // + .extracting("name", "value", "targetType") // + .containsExactlyInAnyOrder( // + tuple(quoted("DUMMY_ENTITY_WITH_COMPOSITE_ID_ONE"), exampleId.one, String.class), // + tuple(quoted("DUMMY_ENTITY_WITH_COMPOSITE_ID_TWO"), exampleId.two, Integer.class) // + ); + } - Identifier identifier = JdbcIdentifierBuilder // - .forBackReferences(converter, path, "parent-eins") // - .withQualifier(path, "list-index-eins") // - .build(); + private AggregatePath getPath(String dotPath) { + return JdbcIdentifierBuilderUnitTests.this.getPath(dotPath, DummyEntityWithCompositeId.class); + } + } - assertThat(identifier.getParts()) // - .extracting("name", "value", "targetType") // - .containsExactlyInAnyOrder( // - tuple(quoted("DUMMY_ENTITY"), "parent-eins", UUID.class), // - tuple(quoted("DUMMY_ENTITY_KEY"), "list-index-eins", Integer.class) // - ); + private AggregatePath getPath(String dotPath, Class entityType) { + return context.getAggregatePath(PersistentPropertyPathTestUtils.getPath(dotPath, entityType, context)); } - @Test // DATAJDBC-326 - public void backreferenceAcrossEmbeddable() { + @SuppressWarnings("unused") + static class DummyEntity { - Identifier identifier = JdbcIdentifierBuilder // - .forBackReferences(converter, getPath("embeddable.child"), "parent-eins") // - .build(); + @Id UUID id; + String one; + Long two; + Child child; - assertThat(identifier.getParts()) // - .extracting("name", "value", "targetType") // - .containsExactly( // - tuple(quoted("DUMMY_ENTITY"), "parent-eins", UUID.class) // - ); - } + Map children; - @Test // DATAJDBC-326 - public void backreferenceAcrossNoId() { + List moreChildren; - Identifier identifier = JdbcIdentifierBuilder // - .forBackReferences(converter, getPath("noId.child"), "parent-eins") // - .build(); + Embeddable embeddable; - assertThat(identifier.getParts()) // - .extracting("name", "value", "targetType") // - .containsExactly( // - tuple(quoted("DUMMY_ENTITY"), "parent-eins", UUID.class) // - ); + NoId noId; } - private AggregatePath getPath(String dotPath) { - return context.getAggregatePath(PersistentPropertyPathTestUtils.getPath(dotPath, DummyEntity.class, context)); + record CompositeId(String one, Integer two) { } - @SuppressWarnings("unused") - static class DummyEntity { + static class DummyEntityWithCompositeId { - @Id UUID id; + @Embedded(onEmpty = Embedded.OnEmpty.USE_NULL) + @Id CompositeId id; String one; Long two; Child child; diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorContextBasedNamingStrategyUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorContextBasedNamingStrategyUnitTests.java index a4c30c02efa..af8909fd002 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorContextBasedNamingStrategyUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorContextBasedNamingStrategyUnitTests.java @@ -89,7 +89,7 @@ public void cascadingDeleteFirstLevel() { assertThat(sql).isEqualTo( // "DELETE FROM " // + user + ".referenced_entity WHERE " // - + user + ".referenced_entity.dummy_entity = :rootId" // + + user + ".referenced_entity.dummy_entity = :id" // ); }); } @@ -107,7 +107,7 @@ public void cascadingDeleteAllSecondLevel() { "DELETE FROM " + user + ".second_level_referenced_entity " // + "WHERE " + user + ".second_level_referenced_entity.referenced_entity IN " // + "(SELECT " + user + ".referenced_entity.l1id FROM " + user + ".referenced_entity " // - + "WHERE " + user + ".referenced_entity.dummy_entity = :rootId)"); + + "WHERE " + user + ".referenced_entity.dummy_entity = :id)"); }); } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorEmbeddedUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorEmbeddedUnitTests.java index ea5afb8db86..086e0afc1f4 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorEmbeddedUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorEmbeddedUnitTests.java @@ -20,17 +20,18 @@ import static org.assertj.core.api.SoftAssertions.*; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.core.PersistentPropertyPathTestUtils; import org.springframework.data.jdbc.core.mapping.AggregateReference; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; +import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.Embedded; import org.springframework.data.relational.core.mapping.Embedded.OnEmpty; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; +import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.core.mapping.Table; import org.springframework.data.relational.core.sql.Aliased; import org.springframework.data.relational.core.sql.SqlIdentifier; @@ -41,6 +42,7 @@ * * @author Bastian Wilhelm * @author Mark Paluch + * @author Jens Schauder */ class SqlGeneratorEmbeddedUnitTests { @@ -84,6 +86,139 @@ void findOne() { }); } + @Test // GH-574 + void findOneWrappedId() { + + SqlGenerator sqlGenerator = createSqlGenerator(DummyEntityWithWrappedId.class); + + String sql = sqlGenerator.getFindOne(); + + assertSoftly(softly -> { + + softly.assertThat(sql).startsWith("SELECT") // + .contains("dummy_entity_with_wrapped_id.name AS name") // + .contains("dummy_entity_with_wrapped_id.id") // + .contains("WHERE dummy_entity_with_wrapped_id.id = :id"); + }); + } + + @Test // GH-574 + void findOneEmbeddedId() { + + SqlGenerator sqlGenerator = createSqlGenerator(DummyEntityWithEmbeddedId.class); + + String sql = sqlGenerator.getFindOne(); + + assertSoftly(softly -> { + + softly.assertThat(sql).startsWith("SELECT") // + .contains("dummy_entity_with_embedded_id.name AS name") // + .contains("dummy_entity_with_embedded_id.one") // + .contains("dummy_entity_with_embedded_id.two") // + .contains(" WHERE ") // + .contains("dummy_entity_with_embedded_id.one = :one") // + .contains("dummy_entity_with_embedded_id.two = :two"); + }); + } + + @Test // GH-574 + void deleteByIdEmbeddedId() { + + SqlGenerator sqlGenerator = createSqlGenerator(DummyEntityWithEmbeddedId.class); + + String sql = sqlGenerator.getDeleteById(); + + assertSoftly(softly -> { + + softly.assertThat(sql).startsWith("DELETE") // + .contains(" WHERE ") // + .contains("dummy_entity_with_embedded_id.one = :one") // + .contains("dummy_entity_with_embedded_id.two = :two"); + }); + } + + @Test // GH-574 + void deleteByIdInEmbeddedId() { + + SqlGenerator sqlGenerator = createSqlGenerator(DummyEntityWithEmbeddedId.class); + + String sql = sqlGenerator.getDeleteByIdIn(); + + assertSoftly(softly -> { + + softly.assertThat(sql).startsWith("DELETE") // + .contains(" WHERE ") // + .contains("(dummy_entity_with_embedded_id.one, dummy_entity_with_embedded_id.two) IN (:ids)"); + }); + } + + @Test // GH-574 + void deleteByPathEmbeddedId() { + + SqlGenerator sqlGenerator = createSqlGenerator(DummyEntityWithEmbeddedId.class); + PersistentPropertyPath path = PersistentPropertyPathTestUtils.getPath("other", + DummyEntityWithEmbeddedIdAndReference.class, context); + + String sql = sqlGenerator.createDeleteByPath(path); + + assertSoftly(softly -> { + + softly.assertThat(sql).startsWith("DELETE FROM other_entity WHERE") // + .contains("other_entity.dummy_entity_with_embedded_id_and_reference_one = :one") // + .contains("other_entity.dummy_entity_with_embedded_id_and_reference_two = :two"); + }); + } + + @Test // GH-574 + void deleteInByPathEmbeddedId() { + + SqlGenerator sqlGenerator = createSqlGenerator(DummyEntityWithEmbeddedId.class); + PersistentPropertyPath path = PersistentPropertyPathTestUtils.getPath("other", + DummyEntityWithEmbeddedIdAndReference.class, context); + + String sql = sqlGenerator.createDeleteInByPath(path); + + assertSoftly(softly -> { + + softly.assertThat(sql).startsWith("DELETE FROM other_entity WHERE") // + .contains(" WHERE ") // + .contains( + "(other_entity.dummy_entity_with_embedded_id_and_reference_one, other_entity.dummy_entity_with_embedded_id_and_reference_two) IN (:ids)"); + }); + } + + @Test // GH-574 + void updateWithEmbeddedId() { + + SqlGenerator sqlGenerator = createSqlGenerator(DummyEntityWithEmbeddedId.class); + + String sql = sqlGenerator.getUpdate(); + + assertSoftly(softly -> { + + softly.assertThat(sql).startsWith("UPDATE") // + .contains(" WHERE ") // + .contains("dummy_entity_with_embedded_id.one = :one") // + .contains("dummy_entity_with_embedded_id.two = :two"); + }); + } + + @Test // GH-574 + void existsByIdEmbeddedId() { + + SqlGenerator sqlGenerator = createSqlGenerator(DummyEntityWithEmbeddedId.class); + + String sql = sqlGenerator.getExists(); + + assertSoftly(softly -> { + + softly.assertThat(sql).startsWith("SELECT COUNT") // + .contains(" WHERE ") // + .contains("dummy_entity_with_embedded_id.one = :one") // + .contains("dummy_entity_with_embedded_id.two = :two"); + }); + } + @Test // DATAJDBC-111 void findAll() { final String sql = sqlGenerator.getFindAll(); @@ -109,7 +244,8 @@ void findAll() { @Test // DATAJDBC-111 void findAllInList() { - final String sql = sqlGenerator.getFindAllInList(); + + String sql = sqlGenerator.getFindAllInList(); assertSoftly(softly -> { @@ -130,6 +266,45 @@ void findAllInList() { }); } + @Test // GH-574 + void findAllInListEmbeddedId() { + + SqlGenerator sqlGenerator = createSqlGenerator(DummyEntityWithEmbeddedId.class); + + String sql = sqlGenerator.getFindAllInList(); + + assertSoftly(softly -> { + + softly.assertThat(sql).startsWith("SELECT") // + .contains("dummy_entity_with_embedded_id.name AS name") // + .contains("dummy_entity_with_embedded_id.one") // + .contains("dummy_entity_with_embedded_id.two") // + .contains(" WHERE (dummy_entity_with_embedded_id.one, dummy_entity_with_embedded_id.two) IN (:ids)"); + }); + } + + @Test // GH-574 + void findOneWithReference() { + + SqlGenerator sqlGenerator = createSqlGenerator(DummyEntityWithEmbeddedIdAndReference.class); + + String sql = sqlGenerator.getFindOne(); + + assertSoftly(softly -> { + + softly.assertThat(sql).startsWith("SELECT") // + .contains(" LEFT OUTER JOIN other_entity other ") // + .contains(" ON ") // + .contains( + " other.dummy_entity_with_embedded_id_and_reference_one = dummy_entity_with_embedded_id_and_reference.one ") // + .contains( + " other.dummy_entity_with_embedded_id_and_reference_two = dummy_entity_with_embedded_id_and_reference.two ") // + .contains(" WHERE ") // + .contains("dummy_entity_with_embedded_id_and_reference.one = :one") // + .contains("dummy_entity_with_embedded_id_and_reference.two = :two"); + }); + } + @Test // DATAJDBC-111 void insert() { final String sql = sqlGenerator.getInsert(emptySet()); @@ -175,21 +350,14 @@ void update() { } @Test // DATAJDBC-340 - @Disabled // this is just broken right now void deleteByPath() { + sqlGenerator = createSqlGenerator(DummyEntity2.class); + final String sql = sqlGenerator .createDeleteByPath(PersistentPropertyPathTestUtils.getPath("embedded.other", DummyEntity2.class, context)); - assertThat(sql).containsSequence("DELETE FROM other_entity", // - "WHERE", // - "embedded_with_reference IN (", // - "SELECT ", // - "id ", // - "FROM", // - "dummy_entity2", // - "WHERE", // - "embedded_with_reference = :rootId"); + assertThat(sql).isEqualTo("DELETE FROM other_entity WHERE other_entity.dummy_entity2 = :id"); } @Test // DATAJDBC-340 @@ -276,12 +444,18 @@ void joinForEmbeddedWithReference() { SqlGenerator.Join join = generateJoin("embedded.other", DummyEntity2.class); assertSoftly(softly -> { - - softly.assertThat(join.getJoinTable().getName()).isEqualTo(SqlIdentifier.unquoted("other_entity")); - softly.assertThat(join.getJoinColumn().getTable()).isEqualTo(join.getJoinTable()); - softly.assertThat(join.getJoinColumn().getName()).isEqualTo(SqlIdentifier.unquoted("dummy_entity2")); - softly.assertThat(join.getParentId().getName()).isEqualTo(SqlIdentifier.unquoted("id")); - softly.assertThat(join.getParentId().getTable().getName()).isEqualTo(SqlIdentifier.unquoted("dummy_entity2")); + softly.assertThat(join.joinTable().getName()).isEqualTo(SqlIdentifier.unquoted("other_entity")); + softly.assertThat(join.columns()).extracting( // + pair -> pair.getFirst().getTable(), // + pair -> pair.getFirst().getName(), // + pair -> pair.getSecond().getTable().getName(), // + pair -> pair.getSecond().getName() // + ).contains(tuple( // + join.joinTable(), // + SqlIdentifier.unquoted("dummy_entity2"), // + SqlIdentifier.unquoted("dummy_entity2"), // + SqlIdentifier.unquoted("id") // + )); }); } @@ -301,6 +475,7 @@ void columnForEmbeddedWithReferenceProperty() { SqlIdentifier.unquoted("prefix_other_value")); } + @Nullable private SqlGenerator.Join generateJoin(String path, Class type) { return createSqlGenerator(type) .getJoin(context.getAggregatePath(PersistentPropertyPathTestUtils.getPath(path, type, context))); @@ -315,6 +490,7 @@ private SqlIdentifier getAlias(Object maybeAliased) { return null; } + @Nullable private org.springframework.data.relational.core.sql.Column generatedColumn(String path, Class type) { return createSqlGenerator(type) @@ -332,15 +508,47 @@ static class DummyEntity { @Embedded(onEmpty = OnEmpty.USE_NULL) CascadedEmbedded embeddable; } + record WrappedId(Long id) { + } + + static class DummyEntityWithWrappedId { + + @Id + @Embedded(onEmpty = OnEmpty.USE_NULL) WrappedId wrappedId; + + String name; + } + + record EmbeddedId(Long one, String two) { + } + + static class DummyEntityWithEmbeddedId { + + @Id + @Embedded(onEmpty = OnEmpty.USE_NULL) EmbeddedId embeddedId; + + String name; + + } + + static class DummyEntityWithEmbeddedIdAndReference { + + @Id + @Embedded(onEmpty = OnEmpty.USE_NULL) EmbeddedId embeddedId; + + String name; + OtherEntity other; + } + @SuppressWarnings("unused") static class CascadedEmbedded { String test; - @Embedded(onEmpty = OnEmpty.USE_NULL, prefix = "prefix2_") Embeddable prefixedEmbeddable; - @Embedded(onEmpty = OnEmpty.USE_NULL) Embeddable embeddable; + @Embedded(onEmpty = OnEmpty.USE_NULL, prefix = "prefix2_") NoId prefixedEmbeddable; + @Embedded(onEmpty = OnEmpty.USE_NULL) NoId embeddable; } @SuppressWarnings("unused") - static class Embeddable { + static class NoId { Long attr1; String attr2; } @@ -362,8 +570,7 @@ static class OtherEntity { } @Table("a") - private - record WithEmbeddedAndAggregateReference(@Id long id, + private record WithEmbeddedAndAggregateReference(@Id long id, @Embedded.Nullable(prefix = "nested_") WithAggregateReference nested) { } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorFixedNamingStrategyUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorFixedNamingStrategyUnitTests.java index d545be74e6c..b3c244941ac 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 f9b9c8b6fa7..184bf12debd 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlGeneratorUnitTests.java @@ -153,7 +153,7 @@ void cascadingDeleteFirstLevel() { String sql = sqlGenerator.createDeleteByPath(getPath("ref", DummyEntity.class)); - assertThat(sql).isEqualTo("DELETE FROM referenced_entity WHERE referenced_entity.dummy_entity = :rootId"); + assertThat(sql).isEqualTo("DELETE FROM referenced_entity WHERE referenced_entity.dummy_entity = :id1"); } @Test // GH-537 @@ -170,7 +170,7 @@ void cascadingDeleteByPathSecondLevel() { String sql = sqlGenerator.createDeleteByPath(getPath("ref.further", DummyEntity.class)); assertThat(sql).isEqualTo( - "DELETE FROM second_level_referenced_entity WHERE second_level_referenced_entity.referenced_entity IN (SELECT referenced_entity.x_l1id FROM referenced_entity WHERE referenced_entity.dummy_entity = :rootId)"); + "DELETE FROM second_level_referenced_entity WHERE second_level_referenced_entity.referenced_entity IN (SELECT referenced_entity.x_l1id FROM referenced_entity WHERE referenced_entity.dummy_entity = :id1)"); } @Test // GH-537 @@ -220,7 +220,7 @@ void deleteMapByPath() { String sql = sqlGenerator.createDeleteByPath(getPath("mappedElements", DummyEntity.class)); - assertThat(sql).isEqualTo("DELETE FROM element WHERE element.dummy_entity = :rootId"); + assertThat(sql).isEqualTo("DELETE FROM element WHERE element.dummy_entity = :id1"); } @Test // DATAJDBC-101 @@ -375,7 +375,8 @@ void selectBySortedQuery() { "ORDER BY dummy_entity.id1 ASC" // ); assertThat(sql).containsOnlyOnce("LEFT OUTER JOIN referenced_entity ref ON ref.dummy_entity = dummy_entity.id1"); - assertThat(sql).containsOnlyOnce("LEFT OUTER JOIN second_level_referenced_entity ref_further ON ref_further.referenced_entity = ref.x_l1id"); + assertThat(sql).containsOnlyOnce( + "LEFT OUTER JOIN second_level_referenced_entity ref_further ON ref_further.referenced_entity = ref.x_l1id"); } @Test // DATAJDBC-131, DATAJDBC-111 @@ -654,7 +655,7 @@ void readOnlyPropertyIncludedIntoQuery_when_generateFindOneSql() { + "entity_with_read_only_property.x_name AS x_name, " // + "entity_with_read_only_property.x_read_only_value AS x_read_only_value " // + "FROM entity_with_read_only_property " // - + "WHERE entity_with_read_only_property.x_id = :id" // + + "WHERE entity_with_read_only_property.x_id = :x_id" // ); } @@ -673,7 +674,7 @@ void deletingLongChain() { "WHERE chain2.chain3 IN (" + // "SELECT chain3.x_three " + // "FROM chain3 " + // - "WHERE chain3.chain4 = :rootId" + // + "WHERE chain3.chain4 = :x_four" + // ")))"); } @@ -682,7 +683,7 @@ void deletingLongChainNoId() { assertThat(createSqlGenerator(NoIdChain4.class) .createDeleteByPath(getPath("chain3.chain2.chain1.chain0", NoIdChain4.class))) // - .isEqualTo("DELETE FROM no_id_chain0 WHERE no_id_chain0.no_id_chain4 = :rootId"); + .isEqualTo("DELETE FROM no_id_chain0 WHERE no_id_chain0.no_id_chain4 = :x_four"); } @Test // DATAJDBC-359 @@ -698,7 +699,7 @@ void deletingLongChainNoIdWithBackreferenceNotReferencingTheRoot() { + "WHERE no_id_chain4.id_no_id_chain IN (" // + "SELECT id_no_id_chain.x_id " // + "FROM id_no_id_chain " // - + "WHERE id_no_id_chain.id_id_no_id_chain = :rootId" // + + "WHERE id_no_id_chain.id_id_no_id_chain = :x_id" // + "))"); } @@ -714,11 +715,18 @@ void joinForSimpleReference() { assertSoftly(softly -> { - softly.assertThat(join.getJoinTable().getName()).isEqualTo(SqlIdentifier.quoted("REFERENCED_ENTITY")); - softly.assertThat(join.getJoinColumn().getTable()).isEqualTo(join.getJoinTable()); - softly.assertThat(join.getJoinColumn().getName()).isEqualTo(SqlIdentifier.quoted("DUMMY_ENTITY")); - softly.assertThat(join.getParentId().getName()).isEqualTo(SqlIdentifier.quoted("id1")); - softly.assertThat(join.getParentId().getTable().getName()).isEqualTo(SqlIdentifier.quoted("DUMMY_ENTITY")); + softly.assertThat(join.joinTable().getName()).isEqualTo(SqlIdentifier.quoted("REFERENCED_ENTITY")); + softly.assertThat(join.columns()).extracting( // + pair -> pair.getFirst().getTable(), // + pair -> pair.getFirst().getName(), // + pair -> pair.getSecond().getTable().getName(), // + pair -> pair.getSecond().getName() // + ).contains(tuple( // + join.joinTable(), // + SqlIdentifier.quoted("DUMMY_ENTITY"), // + SqlIdentifier.quoted("DUMMY_ENTITY"), // + SqlIdentifier.quoted("id1") // + )); }); } @@ -745,13 +753,18 @@ void joinForSecondLevelReference() { SqlGenerator.Join join = generateJoin("ref.further", DummyEntity.class); assertSoftly(softly -> { - - softly.assertThat(join.getJoinTable().getName()) - .isEqualTo(SqlIdentifier.quoted("SECOND_LEVEL_REFERENCED_ENTITY")); - softly.assertThat(join.getJoinColumn().getTable()).isEqualTo(join.getJoinTable()); - softly.assertThat(join.getJoinColumn().getName()).isEqualTo(SqlIdentifier.quoted("REFERENCED_ENTITY")); - softly.assertThat(join.getParentId().getName()).isEqualTo(SqlIdentifier.quoted("X_L1ID")); - softly.assertThat(join.getParentId().getTable().getName()).isEqualTo(SqlIdentifier.quoted("REFERENCED_ENTITY")); + softly.assertThat(join.joinTable().getName()).isEqualTo(SqlIdentifier.quoted("SECOND_LEVEL_REFERENCED_ENTITY")); + softly.assertThat(join.columns()).extracting( // + pair -> pair.getFirst().getTable(), // + pair -> pair.getFirst().getName(), // + pair -> pair.getSecond().getTable().getName(), // + pair -> pair.getSecond().getName() // + ).contains(tuple( // + join.joinTable(), // + SqlIdentifier.quoted("REFERENCED_ENTITY"), // + SqlIdentifier.quoted("REFERENCED_ENTITY"), // + SqlIdentifier.quoted("X_L1ID") // + )); }); } @@ -759,19 +772,25 @@ void joinForSecondLevelReference() { void joinForOneToOneWithoutId() { SqlGenerator.Join join = generateJoin("child", ParentOfNoIdChild.class); - Table joinTable = join.getJoinTable(); + Table joinTable = join.joinTable(); assertSoftly(softly -> { softly.assertThat(joinTable.getName()).isEqualTo(SqlIdentifier.quoted("NO_ID_CHILD")); softly.assertThat(joinTable).isInstanceOf(Aliased.class); softly.assertThat(((Aliased) joinTable).getAlias()).isEqualTo(SqlIdentifier.quoted("child")); - softly.assertThat(join.getJoinColumn().getTable()).isEqualTo(joinTable); - softly.assertThat(join.getJoinColumn().getName()).isEqualTo(SqlIdentifier.quoted("PARENT_OF_NO_ID_CHILD")); - softly.assertThat(join.getParentId().getName()).isEqualTo(SqlIdentifier.quoted("X_ID")); - softly.assertThat(join.getParentId().getTable().getName()) - .isEqualTo(SqlIdentifier.quoted("PARENT_OF_NO_ID_CHILD")); + softly.assertThat(join.columns()).extracting( // + pair -> pair.getFirst().getTable(), // + pair -> pair.getFirst().getName(), // + pair -> pair.getSecond().getTable().getName(), // + pair -> pair.getSecond().getName() // + ).contains(tuple( // + join.joinTable(), // + SqlIdentifier.quoted("PARENT_OF_NO_ID_CHILD"), // + SqlIdentifier.quoted("PARENT_OF_NO_ID_CHILD"), // + SqlIdentifier.quoted("X_ID") // + )); }); } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlParametersFactoryTest.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlParametersFactoryUnitTests.java similarity index 86% rename from spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlParametersFactoryTest.java rename to spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlParametersFactoryUnitTests.java index 7fd0f6e9a8b..3ba18cc5d33 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; @@ -33,8 +34,8 @@ import org.springframework.data.convert.WritingConverter; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.relational.core.conversion.IdValueSource; -import org.springframework.data.relational.core.dialect.AnsiDialect; import org.springframework.data.relational.core.mapping.Column; +import org.springframework.data.relational.core.mapping.Embedded; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.sql.SqlIdentifier; import org.springframework.jdbc.core.JdbcOperations; @@ -44,29 +45,28 @@ * * @author Chirag Tailor */ -class SqlParametersFactoryTest { +class SqlParametersFactoryUnitTests { RelationalMappingContext context = new JdbcMappingContext(); RelationResolver relationResolver = mock(RelationResolver.class); MappingJdbcConverter converter = new MappingJdbcConverter(context, relationResolver); - AnsiDialect dialect = AnsiDialect.INSTANCE; 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)); @@ -87,8 +87,7 @@ public void considersConfiguredWriteConverterForIdValueObjectsWhichReferencedInO assertThat(sqlParameterSource.getValue("DUMMYENTITYROOT")).isEqualTo(rawId); } - @Test - // DATAJDBC-146 + @Test // DATAJDBC-146 void identifiersGetAddedAsParameters() { long id = 4711L; @@ -102,8 +101,7 @@ void identifiersGetAddedAsParameters() { assertThat(sqlParameterSource.getValue("reference")).isEqualTo(reference); } - @Test - // DATAJDBC-146 + @Test // DATAJDBC-146 void additionalIdentifierForIdDoesNotLeadToDuplicateParameters() { long id = 4711L; @@ -115,8 +113,7 @@ void additionalIdentifierForIdDoesNotLeadToDuplicateParameters() { assertThat(sqlParameterSource.getValue("id")).isEqualTo(id); } - @Test - // DATAJDBC-235 + @Test // DATAJDBC-235 void considersConfiguredWriteConverter() { SqlParametersFactory sqlParametersFactory = createSqlParametersFactoryWithConverters( @@ -130,8 +127,7 @@ void considersConfiguredWriteConverter() { assertThat(sqlParameterSource.getValue("flag")).isEqualTo("T"); } - @Test - // DATAJDBC-412 + @Test // DATAJDBC-412 void considersConfiguredWriteConverterForIdValueObjects_onWrite() { SqlParametersFactory sqlParametersFactory = createSqlParametersFactoryWithConverters( @@ -148,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"); @@ -164,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 { @@ -301,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/query/PartTreeJdbcQueryUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java index 3785c826bb1..9bf7b616010 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java @@ -39,6 +39,7 @@ import org.springframework.data.relational.core.dialect.Escaper; import org.springframework.data.relational.core.dialect.H2Dialect; import org.springframework.data.relational.core.mapping.Embedded; +import org.springframework.data.relational.core.mapping.Embedded.Nullable; import org.springframework.data.relational.core.mapping.MappedCollection; import org.springframework.data.relational.core.mapping.Table; import org.springframework.data.relational.core.sql.LockMode; @@ -65,8 +66,8 @@ public class PartTreeJdbcQueryUnitTests { private static final String TABLE = "\"users\""; - private static final String ALL_FIELDS = "\"users\".\"ID\" AS \"ID\", \"users\".\"AGE\" AS \"AGE\", \"users\".\"ACTIVE\" AS \"ACTIVE\", \"users\".\"LAST_NAME\" AS \"LAST_NAME\", \"users\".\"FIRST_NAME\" AS \"FIRST_NAME\", \"users\".\"DATE_OF_BIRTH\" AS \"DATE_OF_BIRTH\", \"users\".\"HOBBY_REFERENCE\" AS \"HOBBY_REFERENCE\", \"hated\".\"NAME\" AS \"HATED_NAME\", \"users\".\"USER_CITY\" AS \"USER_CITY\", \"users\".\"USER_STREET\" AS \"USER_STREET\""; - private static final String JOIN_CLAUSE = "FROM \"users\" LEFT OUTER JOIN \"HOBBY\" \"hated\" ON \"hated\".\"USERS\" = \"users\".\"ID\""; + private static final String ALL_FIELDS = "\"users\".\"AGE\" AS \"AGE\", \"users\".\"ACTIVE\" AS \"ACTIVE\", \"users\".\"LAST_NAME\" AS \"LAST_NAME\", \"users\".\"FIRST_NAME\" AS \"FIRST_NAME\", \"users\".\"DATE_OF_BIRTH\" AS \"DATE_OF_BIRTH\", \"users\".\"HOBBY_REFERENCE\" AS \"HOBBY_REFERENCE\", \"users\".\"ID\" AS \"ID\", \"users\".\"SUB_ID\" AS \"SUB_ID\", \"hated\".\"NAME\" AS \"HATED_NAME\", \"users\".\"USER_CITY\" AS \"USER_CITY\", \"users\".\"USER_STREET\" AS \"USER_STREET\""; + private static final String JOIN_CLAUSE = "FROM \"users\" LEFT OUTER JOIN \"HOBBY\" \"hated\" ON \"hated\".\"USERS_ID\" = \"users\".\"ID\" AND \"hated\".\"USERS_SUB_ID\" = \"users\".\"SUB_ID\""; private static final String BASE_SELECT = "SELECT " + ALL_FIELDS + " " + JOIN_CLAUSE; JdbcMappingContext mappingContext = new JdbcMappingContext(); @@ -778,7 +779,8 @@ interface UserRepository extends Repository { @Table("users") static class User { - @Id Long id; + @Id + @Nullable UserId id; String firstName; String lastName; Date dateOfBirth; @@ -786,7 +788,7 @@ static class User { Boolean active; @Embedded(prefix = "user_", onEmpty = Embedded.OnEmpty.USE_NULL) Address address; - @Embedded.Nullable AnotherEmbedded anotherEmbedded; + @Nullable AnotherEmbedded anotherEmbedded; List hobbies; Hobby hated; @@ -794,6 +796,9 @@ static class User { AggregateReference hobbyReference; } + record UserId(Long id, String subId) { + } + record Address(String street, String city) { } diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/CompositeIdAggregateTemplateHsqlIntegrationTests-hsql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/CompositeIdAggregateTemplateHsqlIntegrationTests-hsql.sql new file mode 100644 index 00000000000..604cbefb2a6 --- /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 21e80a6c989..7ca796018c0 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-hsql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-hsql.sql @@ -1,328 +1,328 @@ CREATE TABLE LEGO_SET ( - "id1" BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, - NAME VARCHAR(30) + "id1" BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, + NAME VARCHAR(30) ); CREATE TABLE MANUAL ( - "id2" BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, - LEGO_SET BIGINT, - "alternative" BIGINT, - CONTENT VARCHAR(2000) + "id2" BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, + LEGO_SET BIGINT, + "alternative" BIGINT, + CONTENT VARCHAR(2000) ); ALTER TABLE MANUAL - ADD FOREIGN KEY (LEGO_SET) - REFERENCES LEGO_SET ("id1"); + ADD FOREIGN KEY (LEGO_SET) + REFERENCES LEGO_SET ("id1"); CREATE TABLE ONE_TO_ONE_PARENT ( - "id3" BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, - content VARCHAR(30) + "id3" BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, + content VARCHAR(30) ); CREATE TABLE Child_No_Id ( - ONE_TO_ONE_PARENT INTEGER PRIMARY KEY, - content VARCHAR(30) + ONE_TO_ONE_PARENT INTEGER PRIMARY KEY, + content VARCHAR(30) ); CREATE TABLE SIMPLE_LIST_PARENT ( - ID BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, - NAME VARCHAR(100) + ID BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, + NAME VARCHAR(100) ); CREATE TABLE LIST_PARENT ( - "id4" BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, - NAME VARCHAR(100) + "id4" BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, + NAME VARCHAR(100) ); CREATE TABLE ELEMENT_NO_ID ( - CONTENT VARCHAR(100), - SIMPLE_LIST_PARENT_KEY BIGINT, - SIMPLE_LIST_PARENT BIGINT, - LIST_PARENT_KEY BIGINT, - LIST_PARENT BIGINT + CONTENT VARCHAR(100), + SIMPLE_LIST_PARENT_KEY BIGINT, + SIMPLE_LIST_PARENT BIGINT, + LIST_PARENT_KEY BIGINT, + LIST_PARENT BIGINT ); ALTER TABLE ELEMENT_NO_ID - ADD FOREIGN KEY (LIST_PARENT) - REFERENCES LIST_PARENT ("id4"); + ADD FOREIGN KEY (LIST_PARENT) + REFERENCES LIST_PARENT ("id4"); CREATE TABLE ARRAY_OWNER ( - ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, - DIGITS VARCHAR(20) ARRAY[10] NOT NULL, - MULTIDIMENSIONAL VARCHAR(20) ARRAY[10] NULL + ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, + DIGITS VARCHAR(20) ARRAY[10] NOT NULL, + MULTIDIMENSIONAL VARCHAR(20) ARRAY[10] NULL ); CREATE TABLE BYTE_ARRAY_OWNER ( - ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, - BINARY_DATA VARBINARY(20) NOT NULL + ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, + BINARY_DATA VARBINARY(20) NOT NULL ); CREATE TABLE DOUBLE_LIST_OWNER ( - ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, - DIGITS DOUBLE PRECISION ARRAY[10] + ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, + DIGITS DOUBLE PRECISION ARRAY[10] ); CREATE TABLE FLOAT_LIST_OWNER ( - ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, - DIGITS FLOAT ARRAY[10] + ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, + DIGITS FLOAT ARRAY[10] ); CREATE TABLE CHAIN4 ( - FOUR BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 40) PRIMARY KEY, - FOUR_VALUE VARCHAR(20) + FOUR BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 40) PRIMARY KEY, + FOUR_VALUE VARCHAR(20) ); CREATE TABLE CHAIN3 ( - THREE BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 30) PRIMARY KEY, - THREE_VALUE VARCHAR(20), - CHAIN4 BIGINT, - FOREIGN KEY (CHAIN4) REFERENCES CHAIN4 (FOUR) + THREE BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 30) PRIMARY KEY, + THREE_VALUE VARCHAR(20), + CHAIN4 BIGINT, + FOREIGN KEY (CHAIN4) REFERENCES CHAIN4 (FOUR) ); CREATE TABLE CHAIN2 ( - TWO BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 20) PRIMARY KEY, - TWO_VALUE VARCHAR(20), - CHAIN3 BIGINT, - FOREIGN KEY (CHAIN3) REFERENCES CHAIN3 (THREE) + TWO BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 20) PRIMARY KEY, + TWO_VALUE VARCHAR(20), + CHAIN3 BIGINT, + FOREIGN KEY (CHAIN3) REFERENCES CHAIN3 (THREE) ); CREATE TABLE CHAIN1 ( - ONE BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 10) PRIMARY KEY, - ONE_VALUE VARCHAR(20), - CHAIN2 BIGINT, - FOREIGN KEY (CHAIN2) REFERENCES CHAIN2 (TWO) + ONE BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 10) PRIMARY KEY, + ONE_VALUE VARCHAR(20), + CHAIN2 BIGINT, + FOREIGN KEY (CHAIN2) REFERENCES CHAIN2 (TWO) ); CREATE TABLE CHAIN0 ( - ZERO BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 0) PRIMARY KEY, - ZERO_VALUE VARCHAR(20), - CHAIN1 BIGINT, - FOREIGN KEY (CHAIN1) REFERENCES CHAIN1 (ONE) + ZERO BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 0) PRIMARY KEY, + ZERO_VALUE VARCHAR(20), + CHAIN1 BIGINT, + FOREIGN KEY (CHAIN1) REFERENCES CHAIN1 (ONE) ); CREATE TABLE NO_ID_CHAIN4 ( - FOUR BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 40) PRIMARY KEY, - FOUR_VALUE VARCHAR(20) + FOUR BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 40) PRIMARY KEY, + FOUR_VALUE VARCHAR(20) ); CREATE TABLE NO_ID_CHAIN3 ( - THREE_VALUE VARCHAR(20), - NO_ID_CHAIN4 BIGINT, - FOREIGN KEY (NO_ID_CHAIN4) REFERENCES NO_ID_CHAIN4 (FOUR) + THREE_VALUE VARCHAR(20), + NO_ID_CHAIN4 BIGINT, + FOREIGN KEY (NO_ID_CHAIN4) REFERENCES NO_ID_CHAIN4 (FOUR) ); CREATE TABLE NO_ID_CHAIN2 ( - TWO_VALUE VARCHAR(20), - NO_ID_CHAIN4 BIGINT, - FOREIGN KEY (NO_ID_CHAIN4) REFERENCES NO_ID_CHAIN4 (FOUR) + TWO_VALUE VARCHAR(20), + NO_ID_CHAIN4 BIGINT, + FOREIGN KEY (NO_ID_CHAIN4) REFERENCES NO_ID_CHAIN4 (FOUR) ); CREATE TABLE NO_ID_CHAIN1 ( - ONE_VALUE VARCHAR(20), - NO_ID_CHAIN4 BIGINT, - FOREIGN KEY (NO_ID_CHAIN4) REFERENCES NO_ID_CHAIN4 (FOUR) + ONE_VALUE VARCHAR(20), + NO_ID_CHAIN4 BIGINT, + FOREIGN KEY (NO_ID_CHAIN4) REFERENCES NO_ID_CHAIN4 (FOUR) ); CREATE TABLE NO_ID_CHAIN0 ( - ZERO_VALUE VARCHAR(20), - NO_ID_CHAIN4 BIGINT, - FOREIGN KEY (NO_ID_CHAIN4) REFERENCES NO_ID_CHAIN4 (FOUR) + ZERO_VALUE VARCHAR(20), + NO_ID_CHAIN4 BIGINT, + FOREIGN KEY (NO_ID_CHAIN4) REFERENCES NO_ID_CHAIN4 (FOUR) ); CREATE TABLE NO_ID_LIST_CHAIN4 ( - FOUR BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 40) PRIMARY KEY, - FOUR_VALUE VARCHAR(20) + FOUR BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 40) PRIMARY KEY, + FOUR_VALUE VARCHAR(20) ); CREATE TABLE NO_ID_LIST_CHAIN3 ( - THREE_VALUE VARCHAR(20), - NO_ID_LIST_CHAIN4 BIGINT, - NO_ID_LIST_CHAIN4_KEY BIGINT, - PRIMARY KEY (NO_ID_LIST_CHAIN4, - NO_ID_LIST_CHAIN4_KEY), - FOREIGN KEY (NO_ID_LIST_CHAIN4) REFERENCES NO_ID_LIST_CHAIN4 (FOUR) + THREE_VALUE VARCHAR(20), + NO_ID_LIST_CHAIN4 BIGINT, + NO_ID_LIST_CHAIN4_KEY BIGINT, + PRIMARY KEY (NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY), + FOREIGN KEY (NO_ID_LIST_CHAIN4) REFERENCES NO_ID_LIST_CHAIN4 (FOUR) ); CREATE TABLE NO_ID_LIST_CHAIN2 ( - TWO_VALUE VARCHAR(20), - NO_ID_LIST_CHAIN4 BIGINT, - NO_ID_LIST_CHAIN4_KEY BIGINT, - NO_ID_LIST_CHAIN3_KEY BIGINT, - PRIMARY KEY (NO_ID_LIST_CHAIN4, - NO_ID_LIST_CHAIN4_KEY, - NO_ID_LIST_CHAIN3_KEY), - FOREIGN KEY ( - NO_ID_LIST_CHAIN4, - NO_ID_LIST_CHAIN4_KEY - ) REFERENCES NO_ID_LIST_CHAIN3 ( - NO_ID_LIST_CHAIN4, - NO_ID_LIST_CHAIN4_KEY - ) + TWO_VALUE VARCHAR(20), + NO_ID_LIST_CHAIN4 BIGINT, + NO_ID_LIST_CHAIN4_KEY BIGINT, + NO_ID_LIST_CHAIN3_KEY BIGINT, + PRIMARY KEY (NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY), + FOREIGN KEY ( + NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY + ) REFERENCES NO_ID_LIST_CHAIN3 ( + NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY + ) ); CREATE TABLE NO_ID_LIST_CHAIN1 ( - ONE_VALUE VARCHAR(20), - NO_ID_LIST_CHAIN4 BIGINT, - NO_ID_LIST_CHAIN4_KEY BIGINT, - NO_ID_LIST_CHAIN3_KEY BIGINT, - NO_ID_LIST_CHAIN2_KEY BIGINT, - PRIMARY KEY (NO_ID_LIST_CHAIN4, - NO_ID_LIST_CHAIN4_KEY, - NO_ID_LIST_CHAIN3_KEY, - NO_ID_LIST_CHAIN2_KEY), - FOREIGN KEY ( - NO_ID_LIST_CHAIN4, - NO_ID_LIST_CHAIN4_KEY, - NO_ID_LIST_CHAIN3_KEY - ) REFERENCES NO_ID_LIST_CHAIN2 ( - NO_ID_LIST_CHAIN4, - NO_ID_LIST_CHAIN4_KEY, - NO_ID_LIST_CHAIN3_KEY - ) + ONE_VALUE VARCHAR(20), + NO_ID_LIST_CHAIN4 BIGINT, + NO_ID_LIST_CHAIN4_KEY BIGINT, + NO_ID_LIST_CHAIN3_KEY BIGINT, + NO_ID_LIST_CHAIN2_KEY BIGINT, + PRIMARY KEY (NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY, + NO_ID_LIST_CHAIN2_KEY), + FOREIGN KEY ( + NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY + ) REFERENCES NO_ID_LIST_CHAIN2 ( + NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY + ) ); CREATE TABLE NO_ID_LIST_CHAIN0 ( - ZERO_VALUE VARCHAR(20), - NO_ID_LIST_CHAIN4 BIGINT, - NO_ID_LIST_CHAIN4_KEY BIGINT, - NO_ID_LIST_CHAIN3_KEY BIGINT, - NO_ID_LIST_CHAIN2_KEY BIGINT, - NO_ID_LIST_CHAIN1_KEY BIGINT, - PRIMARY KEY (NO_ID_LIST_CHAIN4, - NO_ID_LIST_CHAIN4_KEY, - NO_ID_LIST_CHAIN3_KEY, - NO_ID_LIST_CHAIN2_KEY, - NO_ID_LIST_CHAIN1_KEY), - FOREIGN KEY ( - NO_ID_LIST_CHAIN4, - NO_ID_LIST_CHAIN4_KEY, - NO_ID_LIST_CHAIN3_KEY, - NO_ID_LIST_CHAIN2_KEY - ) REFERENCES NO_ID_LIST_CHAIN1 ( - NO_ID_LIST_CHAIN4, - NO_ID_LIST_CHAIN4_KEY, - NO_ID_LIST_CHAIN3_KEY, - NO_ID_LIST_CHAIN2_KEY - ) + ZERO_VALUE VARCHAR(20), + NO_ID_LIST_CHAIN4 BIGINT, + NO_ID_LIST_CHAIN4_KEY BIGINT, + NO_ID_LIST_CHAIN3_KEY BIGINT, + NO_ID_LIST_CHAIN2_KEY BIGINT, + NO_ID_LIST_CHAIN1_KEY BIGINT, + PRIMARY KEY (NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY, + NO_ID_LIST_CHAIN2_KEY, + NO_ID_LIST_CHAIN1_KEY), + FOREIGN KEY ( + NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY, + NO_ID_LIST_CHAIN2_KEY + ) REFERENCES NO_ID_LIST_CHAIN1 ( + NO_ID_LIST_CHAIN4, + NO_ID_LIST_CHAIN4_KEY, + NO_ID_LIST_CHAIN3_KEY, + NO_ID_LIST_CHAIN2_KEY + ) ); - CREATE TABLE NO_ID_MAP_CHAIN4 ( - FOUR BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 40) PRIMARY KEY, - FOUR_VALUE VARCHAR(20) + FOUR BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 40) PRIMARY KEY, + FOUR_VALUE VARCHAR(20) ); CREATE TABLE NO_ID_MAP_CHAIN3 ( - THREE_VALUE VARCHAR(20), - NO_ID_MAP_CHAIN4 BIGINT, - NO_ID_MAP_CHAIN4_KEY VARCHAR(20), - PRIMARY KEY (NO_ID_MAP_CHAIN4, - NO_ID_MAP_CHAIN4_KEY), - FOREIGN KEY (NO_ID_MAP_CHAIN4) REFERENCES NO_ID_MAP_CHAIN4 (FOUR) + THREE_VALUE VARCHAR(20), + NO_ID_MAP_CHAIN4 BIGINT, + NO_ID_MAP_CHAIN4_KEY VARCHAR(20), + PRIMARY KEY (NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY), + FOREIGN KEY (NO_ID_MAP_CHAIN4) REFERENCES NO_ID_MAP_CHAIN4 (FOUR) ); CREATE TABLE NO_ID_MAP_CHAIN2 ( - TWO_VALUE VARCHAR(20), - NO_ID_MAP_CHAIN4 BIGINT, - NO_ID_MAP_CHAIN4_KEY VARCHAR(20), - NO_ID_MAP_CHAIN3_KEY VARCHAR(20), - PRIMARY KEY (NO_ID_MAP_CHAIN4, - NO_ID_MAP_CHAIN4_KEY, - NO_ID_MAP_CHAIN3_KEY), - FOREIGN KEY ( - NO_ID_MAP_CHAIN4, - NO_ID_MAP_CHAIN4_KEY - ) REFERENCES NO_ID_MAP_CHAIN3 ( - NO_ID_MAP_CHAIN4, - NO_ID_MAP_CHAIN4_KEY - ) + TWO_VALUE VARCHAR(20), + NO_ID_MAP_CHAIN4 BIGINT, + NO_ID_MAP_CHAIN4_KEY VARCHAR(20), + NO_ID_MAP_CHAIN3_KEY VARCHAR(20), + PRIMARY KEY (NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY), + FOREIGN KEY ( + NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY + ) REFERENCES NO_ID_MAP_CHAIN3 ( + NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY + ) ); CREATE TABLE NO_ID_MAP_CHAIN1 ( - ONE_VALUE VARCHAR(20), - NO_ID_MAP_CHAIN4 BIGINT, - NO_ID_MAP_CHAIN4_KEY VARCHAR(20), - NO_ID_MAP_CHAIN3_KEY VARCHAR(20), - NO_ID_MAP_CHAIN2_KEY VARCHAR(20), - PRIMARY KEY (NO_ID_MAP_CHAIN4, - NO_ID_MAP_CHAIN4_KEY, - NO_ID_MAP_CHAIN3_KEY, - NO_ID_MAP_CHAIN2_KEY), - FOREIGN KEY ( - NO_ID_MAP_CHAIN4, - NO_ID_MAP_CHAIN4_KEY, - NO_ID_MAP_CHAIN3_KEY - ) REFERENCES NO_ID_MAP_CHAIN2 ( - NO_ID_MAP_CHAIN4, - NO_ID_MAP_CHAIN4_KEY, - NO_ID_MAP_CHAIN3_KEY - ) + ONE_VALUE VARCHAR(20), + NO_ID_MAP_CHAIN4 BIGINT, + NO_ID_MAP_CHAIN4_KEY VARCHAR(20), + NO_ID_MAP_CHAIN3_KEY VARCHAR(20), + NO_ID_MAP_CHAIN2_KEY VARCHAR(20), + PRIMARY KEY (NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY, + NO_ID_MAP_CHAIN2_KEY), + FOREIGN KEY ( + NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY + ) REFERENCES NO_ID_MAP_CHAIN2 ( + NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY + ) ); CREATE TABLE NO_ID_MAP_CHAIN0 ( - ZERO_VALUE VARCHAR(20), - NO_ID_MAP_CHAIN4 BIGINT, - NO_ID_MAP_CHAIN4_KEY VARCHAR(20), - NO_ID_MAP_CHAIN3_KEY VARCHAR(20), - NO_ID_MAP_CHAIN2_KEY VARCHAR(20), - NO_ID_MAP_CHAIN1_KEY VARCHAR(20), - PRIMARY KEY (NO_ID_MAP_CHAIN4, - NO_ID_MAP_CHAIN4_KEY, - NO_ID_MAP_CHAIN3_KEY, - NO_ID_MAP_CHAIN2_KEY, - NO_ID_MAP_CHAIN1_KEY), - FOREIGN KEY ( - NO_ID_MAP_CHAIN4, - NO_ID_MAP_CHAIN4_KEY, - NO_ID_MAP_CHAIN3_KEY, - NO_ID_MAP_CHAIN2_KEY - ) REFERENCES NO_ID_MAP_CHAIN1 ( - NO_ID_MAP_CHAIN4, - NO_ID_MAP_CHAIN4_KEY, - NO_ID_MAP_CHAIN3_KEY, - NO_ID_MAP_CHAIN2_KEY - ) -); - -CREATE TABLE WITH_READ_ONLY ( - ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 40) PRIMARY KEY, - NAME VARCHAR(200), + ZERO_VALUE VARCHAR(20), + NO_ID_MAP_CHAIN4 BIGINT, + NO_ID_MAP_CHAIN4_KEY VARCHAR(20), + NO_ID_MAP_CHAIN3_KEY VARCHAR(20), + NO_ID_MAP_CHAIN2_KEY VARCHAR(20), + NO_ID_MAP_CHAIN1_KEY VARCHAR(20), + PRIMARY KEY (NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY, + NO_ID_MAP_CHAIN2_KEY, + NO_ID_MAP_CHAIN1_KEY), + FOREIGN KEY ( + NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY, + NO_ID_MAP_CHAIN2_KEY + ) REFERENCES NO_ID_MAP_CHAIN1 ( + NO_ID_MAP_CHAIN4, + NO_ID_MAP_CHAIN4_KEY, + NO_ID_MAP_CHAIN3_KEY, + NO_ID_MAP_CHAIN2_KEY + ) +); + +CREATE TABLE WITH_READ_ONLY +( + ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 40) PRIMARY KEY, + NAME VARCHAR(200), READ_ONLY VARCHAR(200) DEFAULT 'from-db' ); CREATE TABLE VERSIONED_AGGREGATE ( - ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, - VERSION BIGINT + ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, + VERSION BIGINT ); @@ -334,7 +334,7 @@ CREATE TABLE WITH_LOCAL_DATE_TIME CREATE TABLE WITH_INSERT_ONLY ( - ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, + ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, INSERT_ONLY VARCHAR(100) ); @@ -345,30 +345,30 @@ CREATE TABLE WITH_ID_ONLY CREATE TABLE MULTIPLE_COLLECTIONS ( - ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, + ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, NAME VARCHAR(100) ); CREATE TABLE SET_ELEMENT ( MULTIPLE_COLLECTIONS BIGINT, - NAME VARCHAR(100) + NAME VARCHAR(100) ); CREATE TABLE LIST_ELEMENT ( - MULTIPLE_COLLECTIONS BIGINT, + MULTIPLE_COLLECTIONS BIGINT, MULTIPLE_COLLECTIONS_KEY INT, - NAME VARCHAR(100) + NAME VARCHAR(100) ); CREATE TABLE MAP_ELEMENT ( - MULTIPLE_COLLECTIONS BIGINT, + MULTIPLE_COLLECTIONS BIGINT, MULTIPLE_COLLECTIONS_KEY VARCHAR(10), - ENUM_MAP_OWNER BIGINT, - ENUM_MAP_OWNER_KEY VARCHAR(10), - NAME VARCHAR(100) + ENUM_MAP_OWNER BIGINT, + ENUM_MAP_OWNER_KEY VARCHAR(10), + NAME VARCHAR(100) ); CREATE TABLE AUTHOR @@ -379,12 +379,12 @@ CREATE TABLE AUTHOR CREATE TABLE BOOK ( AUTHOR BIGINT, - NAME VARCHAR(100) + NAME VARCHAR(100) ); CREATE TABLE ENUM_MAP_OWNER ( - ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, + ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, NAME VARCHAR(100) ); @@ -397,7 +397,7 @@ CREATE TABLE WITH_ONE_TO_ONE CREATE TABLE REFERENCED ( "renamed" VARCHAR(100), - ID BIGINT + ID BIGINT ); CREATE TABLE FIRST @@ -416,7 +416,13 @@ CREATE TABLE SEC CREATE TABLE THIRD ( - SEC BIGINT NOT NULL, - NAME VARCHAR(20) NOT NULL, + SEC BIGINT NOT NULL, + NAME VARCHAR(20) NOT NULL, FOREIGN KEY (SEC) REFERENCES SEC (ID) -); \ No newline at end of file +); + +CREATE TABLE SINGLE_EMBEDDED_ID_ENTITY +( + ID BIGINT GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, + NAME VARCHAR(100) +) \ No newline at end of file diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/AggregatePath.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/AggregatePath.java index 244ea452491..52a6a835eed 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/AggregatePath.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/AggregatePath.java @@ -16,12 +16,21 @@ package org.springframework.data.relational.core.mapping; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Stream; import java.util.stream.StreamSupport; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PersistentPropertyPath; +import org.springframework.data.mapping.PropertyHandler; import org.springframework.data.relational.core.sql.SqlIdentifier; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -34,7 +43,7 @@ * @author Jens Schauder * @author Mark Paluch */ -public interface AggregatePath extends Iterable { +public interface AggregatePath extends Iterable, Comparable { /** * Returns the path that has the same beginning but is one segment shorter than this path. @@ -52,6 +61,15 @@ public interface AggregatePath extends Iterable { */ AggregatePath append(RelationalPersistentProperty property); + /** + * Creates a new path by extending the current path by the path passed as an argument. + * + * @param path must not be {@literal null}. + * @return Guaranteed to be not {@literal null}. + * @since 3.5 + */ + AggregatePath append(AggregatePath path); + /** * @return {@literal true} if this is a root path for the underlying type. */ @@ -227,6 +245,9 @@ default Stream stream() { */ AggregatePath getIdDefiningParentPath(); + @Nullable + AggregatePath getTail(); + record TableInfo( /* @@ -240,7 +261,7 @@ record TableInfo( */ @Nullable SqlIdentifier tableAlias, - ColumnInfo reverseColumnInfo, + ColumnInfos reverseColumnInfos, /* * The column used for the list index or map key of the leaf property of this path. @@ -256,13 +277,7 @@ record TableInfo( /* * The column name of the id column of the ancestor path that represents an actual table. */ - SqlIdentifier idColumnName, - - /* - * If the table owning ancestor has an id the column name of that id property is returned. Otherwise the reverse - * column is returned. - */ - SqlIdentifier effectiveIdColumnName) { + ColumnInfos idColumnInfos) { static TableInfo of(AggregatePath path) { @@ -273,18 +288,7 @@ static TableInfo of(AggregatePath path) { SqlIdentifier tableAlias = tableOwner.isRoot() ? null : AggregatePathTableUtils.constructTableAlias(tableOwner); - ColumnInfo reverseColumnInfo = null; - if (!tableOwner.isRoot()) { - - AggregatePath idDefiningParentPath = tableOwner.getIdDefiningParentPath(); - RelationalPersistentProperty leafProperty = tableOwner.getRequiredLeafProperty(); - - SqlIdentifier reverseColumnName = leafProperty - .getReverseColumnName(idDefiningParentPath.getRequiredLeafEntity()); - - reverseColumnInfo = new ColumnInfo(reverseColumnName, - AggregatePathTableUtils.prefixWithTableAlias(path, reverseColumnName)); - } + ColumnInfos reverseColumnInfos = computeReverseColumnInfo(path); ColumnInfo qualifierColumnInfo = null; if (!path.isRoot()) { @@ -300,27 +304,114 @@ static TableInfo of(AggregatePath path) { qualifierColumnType = path.getRequiredLeafProperty().getQualifierColumnType(); } - SqlIdentifier idColumnName = leafEntity.hasIdProperty() ? leafEntity.getIdColumn() : null; + ColumnInfos idColumnInfos = computeIdColumnInfos(tableOwner, leafEntity); - SqlIdentifier effectiveIdColumnName = tableOwner.isRoot() ? idColumnName : reverseColumnInfo.name(); + return new TableInfo(qualifiedTableName, tableAlias, reverseColumnInfos, qualifierColumnInfo, qualifierColumnType, + idColumnInfos); - return new TableInfo(qualifiedTableName, tableAlias, reverseColumnInfo, qualifierColumnInfo, qualifierColumnType, - idColumnName, effectiveIdColumnName); + } + + private static ColumnInfos computeIdColumnInfos(AggregatePath tableOwner, + RelationalPersistentEntity leafEntity) { + ColumnInfos idColumnInfos = ColumnInfos.empty(tableOwner); + if (!leafEntity.hasIdProperty()) { + return idColumnInfos; + } + + RelationalPersistentProperty idProperty = leafEntity.getRequiredIdProperty(); + AggregatePath idPath = tableOwner.append(idProperty); + + if (idProperty.isEntity()) { + ColumInfosBuilder ciBuilder = new ColumInfosBuilder(idPath); + idPath.getRequiredLeafEntity().doWithProperties((PropertyHandler) p -> { + AggregatePath idElementPath = idPath.append(p); + ciBuilder.add(idElementPath, ColumnInfo.of(idElementPath)); + }); + return ciBuilder.build(); + } else { + ColumInfosBuilder ciBuilder = new ColumInfosBuilder(idPath.getParentPath()); + ciBuilder.add(idPath, ColumnInfo.of(idPath)); + return ciBuilder.build(); + } } - } - record ColumnInfo( + private static ColumnInfos computeReverseColumnInfo(AggregatePath path) { + + AggregatePath tableOwner = AggregatePathTraversal.getTableOwningPath(path); + + if (tableOwner.isRoot()) { + return ColumnInfos.empty(tableOwner); + } + + AggregatePath idDefiningParentPath = tableOwner.getIdDefiningParentPath(); + RelationalPersistentProperty leafProperty = tableOwner.getRequiredLeafProperty(); + + RelationalPersistentProperty idProperty = idDefiningParentPath.getLeafEntity().getIdProperty(); + + if (idProperty != null) { + if (idProperty.isEntity()) { + + AggregatePath idBasePath = idDefiningParentPath.append(idProperty); + ColumInfosBuilder ciBuilder = new ColumInfosBuilder(idBasePath); + + RelationalPersistentEntity idEntity = idBasePath.getRequiredLeafEntity(); + idEntity.doWithProperties((PropertyHandler) p -> { + AggregatePath idElementPath = idBasePath.append(p); + SqlIdentifier name = idElementPath.getColumnInfo().name(); + name = name.transform(n -> idDefiningParentPath.getTableInfo().qualifiedTableName.getReference() + "_" + n); + + ciBuilder.add(idElementPath, name, name); + }); + + return ciBuilder.build(); + + } else { + + ColumInfosBuilder ciBuilder = new ColumInfosBuilder(idDefiningParentPath); + SqlIdentifier reverseColumnName = leafProperty + .getReverseColumnName(idDefiningParentPath.getRequiredLeafEntity()); + + ciBuilder.add(idProperty, reverseColumnName, + AggregatePathTableUtils.prefixWithTableAlias(path, reverseColumnName)); + + return ciBuilder.build(); + } + } else { - /* The name of the column used to represent this property in the database. */ - SqlIdentifier name, /* The alias for the column used to represent this property in the database. */ - SqlIdentifier alias) { + ColumInfosBuilder ciBuilder = new ColumInfosBuilder(idDefiningParentPath); + SqlIdentifier reverseColumnName = leafProperty + .getReverseColumnName(idDefiningParentPath.getRequiredLeafEntity()); + + ciBuilder.add(idDefiningParentPath, reverseColumnName, + AggregatePathTableUtils.prefixWithTableAlias(path, reverseColumnName)); + + return ciBuilder.build(); + } + + } + + @Deprecated(forRemoval = true) + public ColumnInfo reverseColumnInfo() { + return reverseColumnInfos.unique(); + } + + public ColumnInfos effectiveIdColumnInfos() { + return reverseColumnInfos.columnInfos.isEmpty() ? idColumnInfos : reverseColumnInfos; + } + } + + /** + * @param name The name of the column used to represent this property in the database. + * @param alias The alias for the column used to represent this property in the database. + */ + record ColumnInfo(SqlIdentifier name, SqlIdentifier alias) { /** * Create a {@link ColumnInfo} from an aggregate path. ColumnInfo can be created for simple type single-value * properties only. * - * @param path + * @param path the path to the {@literal ColumnInfo} for. * @return the {@link ColumnInfo}. * @throws IllegalArgumentException if the path is {@link #isRoot()}, {@link #isEmbedded()} or * {@link #isMultiValued()}. @@ -338,4 +429,104 @@ static ColumnInfo of(AggregatePath path) { return new ColumnInfo(columnName, AggregatePathTableUtils.prefixWithTableAlias(path, columnName)); } } + + /** + * A group of {@link ColumnInfo} values referenced by there respective {@link AggregatePath}. This is relevant for + * composite ids and references to such ids. + **/ + class ColumnInfos { + + private final AggregatePath basePath; + private final Map columnInfos; + + /** + * @param basePath The path on which all other paths in the other argument are based on. For the typical case of a + * composite id, this would be the path to the composite ids. + * @param columnInfos A map, mapping {@literal AggregatePath} instances to the respective {@literal ColumnInfo} + */ + private ColumnInfos(AggregatePath basePath, Map columnInfos) { + + this.basePath = basePath; + this.columnInfos = columnInfos; + } + + public static ColumnInfos empty(AggregatePath base) { + return new ColumnInfos(base, new HashMap<>()); + } + + public ColumnInfo unique() { + + Collection values = columnInfos.values(); + Assert.state(values.size() == 1, "ColumnInfo is not unique"); + return values.iterator().next(); + } + + public ColumnInfo any() { + + Collection values = columnInfos.values(); + return values.iterator().next(); + } + + public boolean isEmpty() { + return columnInfos.isEmpty(); + } + + public List toList(Function mapper) { + return columnInfos.values().stream().map(mapper).toList(); + } + + public void forEach(BiConsumer consumer) { + columnInfos.forEach(consumer); + } + + public T any(BiFunction consumer) { + + Map.Entry any = columnInfos.entrySet().iterator().next(); + return consumer.apply(any.getKey(), any.getValue()); + } + + public ColumnInfo get(AggregatePath path) { + return columnInfos.get(path); + } + + public AggregatePath fullPath(AggregatePath ap) { + return basePath.append(ap); + } + + public int size() { + return columnInfos.size(); + } + } + + class ColumInfosBuilder { + private final AggregatePath basePath; + + private final Map columnInfoMap = new TreeMap<>(); + + public ColumInfosBuilder(AggregatePath basePath) { + this.basePath = basePath; + } + + void add(AggregatePath path, SqlIdentifier name, SqlIdentifier alias) { + add(path, new ColumnInfo(name, alias)); + } + + public void add(RelationalPersistentProperty property, SqlIdentifier name, SqlIdentifier alias) { + add(basePath.append(property), name, alias); + } + + ColumnInfos build() { + return new ColumnInfos(basePath, columnInfoMap); + } + + public void add(AggregatePath path, ColumnInfo columnInfo) { + AggregatePath substract = path.substract(basePath); + System.out.println(substract); + columnInfoMap.put(substract, columnInfo); + } + } + + @Nullable + AggregatePath substract(@Nullable AggregatePath basePath); + } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentEntity.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentEntity.java index c8d67cb1b25..bf39181b067 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 @@ -155,6 +155,7 @@ public SqlIdentifier getQualifiedTableName() { } @Override + @Deprecated(forRemoval = true) public SqlIdentifier getIdColumn() { return getRequiredIdProperty().getColumnName(); } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DefaultAggregatePath.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DefaultAggregatePath.java index 3808b2ba3cf..e8eeb6ae2fe 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DefaultAggregatePath.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DefaultAggregatePath.java @@ -21,6 +21,7 @@ import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.util.Lazy; +import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ConcurrentLruCache; @@ -97,6 +98,20 @@ public AggregatePath append(RelationalPersistentProperty property) { return nestedCache.get(property); } + @Override + public AggregatePath append(AggregatePath path) { + + if (path.isRoot()) { + return this; + } + + RelationalPersistentProperty baseProperty = path.getRequiredBaseProperty(); + AggregatePath appended = append(baseProperty); + AggregatePath tail = path.getTail(); + return tail == null ? appended : appended.append(tail); + + } + private AggregatePath doGetAggegatePath(RelationalPersistentProperty property) { PersistentPropertyPath newPath = isRoot() // @@ -194,6 +209,47 @@ public AggregatePath getIdDefiningParentPath() { return AggregatePathTraversal.getIdDefiningPath(this); } + @Override + public AggregatePath getTail() { + + if (getLength() <= 2) { + return null; + } + + AggregatePath tail = null; + for (RelationalPersistentProperty prop : this.path) { + if (tail == null) { + tail = context.getAggregatePath(context.getPersistentEntity(prop)); + } else { + tail = tail.append(prop); + } + } + return tail; + } + + @Override + @Nullable + public AggregatePath substract(@Nullable AggregatePath basePath) { + + if (basePath == null || basePath.isRoot()) { + return this; + } + + if (this.isRoot()) { + throw new IllegalStateException("Can't subtract from root path"); + } + + if (basePath.getRequiredBaseProperty().equals(getRequiredBaseProperty())) { + AggregatePath tail = this.getTail(); + if (tail == null) { + return null; + } + return tail.substract(basePath.getTail()); + } + + throw new IllegalStateException("Can't subtract [%s] from [%s]".formatted(basePath, this)); + } + /** * Finds and returns the longest path with ich identical or an ancestor to the current path and maps directly to a * table. @@ -240,7 +296,6 @@ public int hashCode() { return Objects.hash(context, rootType, path); } - @Override public String toString() { return "AggregatePath[" @@ -248,6 +303,11 @@ public String toString() { + ((isRoot()) ? "/" : path.toDotPath()); } + @Override + public int compareTo(@NonNull AggregatePath other) { + return toDotPath().compareTo(other.toDotPath()); + } + private static class AggregatePathIterator implements Iterator { private @Nullable AggregatePath current; diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/EmbeddedRelationalPersistentEntity.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/EmbeddedRelationalPersistentEntity.java index e5432499a79..d2ba4f79a29 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 @@ -50,6 +50,7 @@ public SqlIdentifier getTableName() { } @Override + @Deprecated(forRemoval = true) public SqlIdentifier getIdColumn() { throw new MappingException("Embedded entity does not have an id column"); } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java index a5e7e4c83df..b1e41c3ab73 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 f54587a19d5..f2c978b71e0 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 @@ -49,7 +49,9 @@ default SqlIdentifier getQualifiedTableName() { * Returns the column representing the identifier. * * @return will never be {@literal null}. + * @deprecated use {@code AggregatePath.getTableInfo().getIdColumnInfos()} instead. */ + @Deprecated(forRemoval = true) SqlIdentifier getIdColumn(); } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AnalyticFunction.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AnalyticFunction.java index 2abb3cfdf38..a14a4017c0d 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AnalyticFunction.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/AnalyticFunction.java @@ -16,6 +16,7 @@ package org.springframework.data.relational.core.sql; import java.util.Arrays; +import java.util.Collection; /** * Represents an analytic function, also known as windowing function @@ -44,18 +45,62 @@ private AnalyticFunction(SimpleFunction function, Partition partition, OrderBy o this.orderBy = orderBy; } + /** + * Specify the {@literal PARTITION BY} clause of an analytic function + * + * @param partitionBy Typically, column but other expressions are fine to. + * @return a new {@literal AnalyticFunction} is partitioned by the given expressions, overwriting any expression + * previously present. + */ public AnalyticFunction partitionBy(Expression... partitionBy) { - return new AnalyticFunction(function, new Partition(partitionBy), orderBy); } + /** + * Specify the {@literal PARTITION BY} clause of an analytic function + * + * @param partitionBy Typically, column but other expressions are fine to. + * @return a new {@literal AnalyticFunction} is partitioned by the given expressions, overwriting any expression + * previously present. + * @since 3.5 + */ + public AnalyticFunction partitionBy(Collection partitionBy) { + return partitionBy(partitionBy.toArray(new Expression[0])); + } + + /** + * Specify the {@literal ORDER BY} clause of an analytic function + * + * @param orderBy Typically, column but other expressions are fine to. + * @return a new {@literal AnalyticFunction} is ordered by the given expressions, overwriting any expression + * previously present. + */ public AnalyticFunction orderBy(OrderByField... orderBy) { return new AnalyticFunction(function, partition, new OrderBy(orderBy)); } - public AnalyticFunction orderBy(Expression... orderByExpression) { + /** + * Specify the {@literal ORDER BY} clause of an analytic function + * + * @param orderBy Typically, column but other expressions are fine to. + * @return a new {@literal AnalyticFunction} is ordered by the given expressions, overwriting any expression + * previously present. + * @since 3.5 + */ + public AnalyticFunction orderBy(Collection orderBy) { + return orderBy(orderBy.toArray(new Expression[0])); + } + + /** + * Specify the {@literal ORDER BY} clause of an analytic function + * + * @param orderBy array of {@link Expression}. Typically, column but other expressions are fine to. + * @return a new {@literal AnalyticFunction} is ordered by the given expressions, overwriting any expression + * previously present. + */ + public AnalyticFunction orderBy(Expression... orderBy) { - final OrderByField[] orderByFields = Arrays.stream(orderByExpression) // + final OrderByField[] orderByFields = Arrays.stream(orderBy) // .map(OrderByField::from) // .toArray(OrderByField[]::new); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Conditions.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Conditions.java index c4eb4a463f6..62dc6d86ba7 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Conditions.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/Conditions.java @@ -247,7 +247,7 @@ public static In in(Expression columnOrExpression, Expression... expressions) { * @param subselect the subselect. * @return the {@link In} condition. */ - public static In in(Column column, Select subselect) { + public static In in(Expression column, Select subselect) { Assert.notNull(column, "Column must not be null"); Assert.notNull(subselect, "Subselect must not be null"); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/TupleExpression.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/TupleExpression.java new file mode 100644 index 00000000000..b5894a749f7 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/TupleExpression.java @@ -0,0 +1,44 @@ +package org.springframework.data.relational.core.sql; + +import static java.util.stream.Collectors.*; + +import java.util.List; + +/** + * A tuple as used in conditions like + * + *
+ *   WHERE (one, two) IN (select x, y from some_table)
+ * 
+ * + * @author Jens Schauder + * @since 3.5 + */ +public class TupleExpression extends AbstractSegment implements Expression { + + private final List expressions; + + private static Segment[] children(List expressions) { + return expressions.toArray(new Segment[0]); + } + + private TupleExpression(List expressions) { + + super(children(expressions)); + + this.expressions = expressions; + } + + public static TupleExpression create(Expression... expressions) { + return new TupleExpression(List.of(expressions)); + } + + public static TupleExpression create(List expressions) { + return new TupleExpression(expressions); + } + + @Override + public String toString() { + return "(" + expressions.stream().map(Expression::toString).collect(joining(", ")) + ")"; + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ExpressionVisitor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/ExpressionVisitor.java index 65843cd3400..f4da4adc8d8 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 00000000000..fef8d8f6886 --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sql/render/TupleVisitor.java @@ -0,0 +1,72 @@ +/* + * Copyright 2019-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.relational.core.sql.render; + +import org.springframework.data.relational.core.sql.TupleExpression; +import org.springframework.data.relational.core.sql.Visitable; + +/** + * Visitor for rendering tuple expressions. + * + * @author Jens Schauder + * @since 3.5 + */ +class TupleVisitor extends TypedSingleConditionRenderSupport implements PartRenderer { + + private final StringBuilder part = new StringBuilder(); + private boolean needsComma = false; + + TupleVisitor(RenderContext context) { + super(context); + } + + @Override + Delegation leaveNested(Visitable segment) { + + if (hasDelegatedRendering()) { + + if (needsComma) { + part.append(", "); + } + + part.append(consumeRenderedPart()); + needsComma = true; + } + + return super.leaveNested(segment); + } + + @Override + Delegation enterMatched(TupleExpression segment) { + + part.append("("); + + return super.enterMatched(segment); + } + + @Override + Delegation leaveMatched(TupleExpression segment) { + + part.append(")"); + + return super.leaveMatched(segment); + } + + @Override + public CharSequence getRenderedPart() { + return part; + } +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sqlgeneration/SingleQuerySqlGenerator.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sqlgeneration/SingleQuerySqlGenerator.java index 6d65ce825e3..01f436c635a 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/sqlgeneration/SingleQuerySqlGenerator.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/sqlgeneration/SingleQuerySqlGenerator.java @@ -167,9 +167,13 @@ private QueryMeta createInlineQuery(AggregatePath basePath, @Nullable Condition columns.add(rownumber); String rowCountAlias = aliases.getRowCountAlias(basePath); - Expression count = basePath.isRoot() ? new AliasedExpression(SQL.literalOf(1), rowCountAlias) - : AnalyticFunction.create("count", Expressions.just("*")) - .partitionBy(table.column(basePath.getTableInfo().reverseColumnInfo().name())).as(rowCountAlias); + Expression count = basePath.isRoot() ? new AliasedExpression(SQL.literalOf(1), rowCountAlias) // + : AnalyticFunction.create("count", Expressions.just("*")) // + .partitionBy( // + basePath.getTableInfo().reverseColumnInfos().toList( // + ci -> table.column(ci.name()) // + ) // + ).as(rowCountAlias); columns.add(count); String backReferenceAlias = null; @@ -178,7 +182,7 @@ private QueryMeta createInlineQuery(AggregatePath basePath, @Nullable Condition if (!basePath.isRoot()) { backReferenceAlias = aliases.getBackReferenceAlias(basePath); - columns.add(table.column(basePath.getTableInfo().reverseColumnInfo().name()).as(backReferenceAlias)); + columns.add(table.column(basePath.getTableInfo().reverseColumnInfos().unique().name()).as(backReferenceAlias)); keyAlias = aliases.getKeyAlias(basePath); Expression keyExpression = basePath.isQualified() @@ -238,9 +242,10 @@ private String getIdentifierProperty(List paths) { private static AnalyticFunction createRowNumberExpression(AggregatePath basePath, Table table, String rowNumberAlias) { + AggregatePath.ColumnInfos reverseColumnInfos = basePath.getTableInfo().reverseColumnInfos(); return AnalyticFunction.create("row_number") // - .partitionBy(table.column(basePath.getTableInfo().reverseColumnInfo().name())) // - .orderBy(table.column(basePath.getTableInfo().reverseColumnInfo().name())) // + .partitionBy(reverseColumnInfos.toList(ci -> table.column(ci.name()))) // + .orderBy(reverseColumnInfos.toList(ci -> table.column(ci.name()))) // .as(rowNumberAlias); } diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentEntityUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentEntityUnitTests.java index 83f56e80121..fb6e822346a 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentEntityUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentEntityUnitTests.java @@ -175,8 +175,9 @@ private static class EntityWithSchemaAndName { @Id private Long id; } - @Table(schema = "#{T(org.springframework.data.relational.core.mapping." - + "BasicRelationalPersistentEntityUnitTests$EntityWithSchemaAndTableSpelExpression).desiredSchemaName}", + @Table( + schema = "#{T(org.springframework.data.relational.core.mapping." + + "BasicRelationalPersistentEntityUnitTests$EntityWithSchemaAndTableSpelExpression).desiredSchemaName}", name = "#{T(org.springframework.data.relational.core.mapping." + "BasicRelationalPersistentEntityUnitTests$EntityWithSchemaAndTableSpelExpression).desiredTableName}") private static class EntityWithSchemaAndTableSpelExpression { @@ -185,10 +186,11 @@ private static class EntityWithSchemaAndTableSpelExpression { public static String desiredSchemaName = "HELP_ME_OBI_WON"; } - @Table(schema = "#{T(org.springframework.data.relational.core.mapping." - + "BasicRelationalPersistentEntityUnitTests$LittleBobbyTables).desiredSchemaName}", + @Table( + schema = "#{T(org.springframework.data.relational.core.mapping." + + "BasicRelationalPersistentEntityUnitTests$LittleBobbyTables).desiredSchemaName}", name = "#{T(org.springframework.data.relational.core.mapping." - + "BasicRelationalPersistentEntityUnitTests$LittleBobbyTables).desiredTableName}") + + "BasicRelationalPersistentEntityUnitTests$LittleBobbyTables).desiredTableName}") private static class LittleBobbyTables { @Id private Long id; public static String desiredTableName = "Robert'); DROP TABLE students;--"; 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 00000000000..54f0bf04820 --- /dev/null +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/ColumnInfosUnitTests.java @@ -0,0 +1,102 @@ +/* + * Copyright 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.relational.core.mapping; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.NoSuchElementException; + +import org.junit.jupiter.api.Test; +import org.springframework.data.annotation.Id; +import org.springframework.data.relational.core.sql.SqlIdentifier; + +/** + * Unit tests for the construction of {@link org.springframework.data.relational.core.mapping.AggregatePath.ColumnInfos} + * + * @author Jens Schauder + */ +class ColumnInfosUnitTests { + + static final SqlIdentifier ID = SqlIdentifier.quoted("ID"); + RelationalMappingContext context = new RelationalMappingContext(); + + @Test // GH-574 + void emptyColumnInfos() { + + AggregatePath.ColumnInfos columnInfos = AggregatePath.ColumnInfos.empty(basePath(DummyEntity.class)); + + assertThat(columnInfos.isEmpty()).isTrue(); + assertThrows(NoSuchElementException.class, columnInfos::any); + assertThrows(IllegalStateException.class, columnInfos::unique); + assertThat(columnInfos.toList(ci -> { + throw new IllegalStateException("This should never get called"); + })).isEmpty(); + } + + @Test // GH-574 + void singleElementColumnInfos() { + + AggregatePath.ColumnInfos columnInfos = basePath(DummyEntity.class).getTableInfo().idColumnInfos(); + + assertThat(columnInfos.isEmpty()).isFalse(); + assertThat(columnInfos.any().name()).isEqualTo(ID); + assertThat(columnInfos.unique().name()).isEqualTo(ID); + assertThat(columnInfos.toList(ci -> ci.name())).containsExactly(ID); + } + + @Test // GH-574 + void multiElementColumnInfos() { + + AggregatePath.ColumnInfos columnInfos = basePath(DummyEntityWithCompositeId.class).getTableInfo().idColumnInfos(); + + assertThat(columnInfos.isEmpty()).isFalse(); + assertThat(columnInfos.any().name()).isEqualTo(SqlIdentifier.quoted("ONE")); + assertThrows(IllegalStateException.class, columnInfos::unique); + assertThat(columnInfos.toList(ci -> ci.name())) // + .containsExactly( // + SqlIdentifier.quoted("ONE"), // + SqlIdentifier.quoted("TWO") // + ); + + List collector = new ArrayList<>(); + columnInfos.forEach((ap, ci) -> collector.add(ap.toDotPath() + "+" + ci.name())); + assertThat(collector).containsExactly("one+\"ONE\"", "two+\"TWO\""); + + columnInfos.get(getPath(CompositeId.class, "one")); + + } + + private AggregatePath getPath(Class type, String name) { + return basePath(type).append(context.getPersistentEntity(type).getPersistentProperty(name)); + } + + private AggregatePath basePath(Class type) { + return context.getAggregatePath(context.getPersistentEntity(type)); + } + + record DummyEntity(@Id String id, String name) { + } + + record CompositeId(String one, String two) { + } + + record DummyEntityWithCompositeId(@Id @Embedded.Nullable CompositeId id, String name) { + } +} diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/DefaultAggregatePathUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/DefaultAggregatePathUnitTests.java index 837cec98328..4efea298a17 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/DefaultAggregatePathUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/DefaultAggregatePathUnitTests.java @@ -22,6 +22,8 @@ import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.TreeSet; import java.util.stream.Collectors; import org.junit.jupiter.api.Test; @@ -62,9 +64,9 @@ void getParentPath() { assertSoftly(softly -> { - softly.assertThat(path("second.third2.value").getParentPath()).isEqualTo(path("second.third2")); - softly.assertThat(path("second.third2").getParentPath()).isEqualTo(path("second")); - softly.assertThat(path("second").getParentPath()).isEqualTo(path()); + softly.assertThat((Object) path("second.third2.value").getParentPath()).isEqualTo(path("second.third2")); + softly.assertThat((Object) path("second.third2").getParentPath()).isEqualTo(path("second")); + softly.assertThat((Object) path("second").getParentPath()).isEqualTo(path()); softly.assertThatThrownBy(() -> path().getParentPath()).isInstanceOf(IllegalStateException.class); }); @@ -94,14 +96,15 @@ void idDefiningPath() { assertSoftly(softly -> { - softly.assertThat(path("second.third2.value").getIdDefiningParentPath()).isEqualTo(path()); - softly.assertThat(path("second.third.value").getIdDefiningParentPath()).isEqualTo(path()); - softly.assertThat(path("secondList.third2.value").getIdDefiningParentPath()).isEqualTo(path()); - softly.assertThat(path("secondList.third.value").getIdDefiningParentPath()).isEqualTo(path()); - softly.assertThat(path("second2.third2.value").getIdDefiningParentPath()).isEqualTo(path()); - softly.assertThat(path("second2.third.value").getIdDefiningParentPath()).isEqualTo(path()); - softly.assertThat(path("withId.second.third2.value").getIdDefiningParentPath()).isEqualTo(path("withId")); - softly.assertThat(path("withId.second.third.value").getIdDefiningParentPath()).isEqualTo(path("withId")); + softly.assertThat((Object) path("second.third2.value").getIdDefiningParentPath()).isEqualTo(path()); + softly.assertThat((Object) path("second.third.value").getIdDefiningParentPath()).isEqualTo(path()); + softly.assertThat((Object) path("secondList.third2.value").getIdDefiningParentPath()).isEqualTo(path()); + softly.assertThat((Object) path("secondList.third.value").getIdDefiningParentPath()).isEqualTo(path()); + softly.assertThat((Object) path("second2.third2.value").getIdDefiningParentPath()).isEqualTo(path()); + softly.assertThat((Object) path("second2.third.value").getIdDefiningParentPath()).isEqualTo(path()); + softly.assertThat((Object) path("withId.second.third2.value").getIdDefiningParentPath()) + .isEqualTo(path("withId")); + softly.assertThat((Object) path("withId.second.third.value").getIdDefiningParentPath()).isEqualTo(path("withId")); }); } @@ -121,13 +124,13 @@ void reverseColumnName() { assertSoftly(softly -> { - softly.assertThat(path("second.third2").getTableInfo().reverseColumnInfo().name()) + softly.assertThat((Object) path("second.third2").getTableInfo().reverseColumnInfo().name()) .isEqualTo(quoted("DUMMY_ENTITY")); - softly.assertThat(path("second.third").getTableInfo().reverseColumnInfo().name()) + softly.assertThat((Object) path("second.third").getTableInfo().reverseColumnInfo().name()) .isEqualTo(quoted("DUMMY_ENTITY")); - softly.assertThat(path("secondList.third2").getTableInfo().reverseColumnInfo().name()) + softly.assertThat((Object) path("secondList.third2").getTableInfo().reverseColumnInfo().name()) .isEqualTo(quoted("DUMMY_ENTITY")); - softly.assertThat(path("secondList.third").getTableInfo().reverseColumnInfo().name()) + softly.assertThat((Object) path("secondList.third").getTableInfo().reverseColumnInfo().name()) .isEqualTo(quoted("DUMMY_ENTITY")); softly.assertThat(path("second2.third").getTableInfo().reverseColumnInfo().name()) .isEqualTo(quoted("DUMMY_ENTITY")); @@ -140,6 +143,17 @@ void reverseColumnName() { }); } + @Test // GH-574 + void reverseColumnNames() { + + assertSoftly(softly -> { + softly.assertThat(path(CompoundIdEntity.class, "second").getTableInfo().reverseColumnInfos().toList(x -> x)) + .extracting(AggregatePath.ColumnInfo::name) + .containsExactlyInAnyOrder(quoted("COMPOUND_ID_ENTITY_ONE"), quoted("COMPOUND_ID_ENTITY_TWO")); + + }); + } + @Test // GH-1525 void getQualifierColumn() { @@ -172,8 +186,9 @@ void extendBy() { assertSoftly(softly -> { - softly.assertThat(path().append(entity.getRequiredPersistentProperty("withId"))).isEqualTo(path("withId")); - softly.assertThat(path("withId").append(path("withId").getRequiredIdProperty())) + softly.assertThat((Object) path().append(entity.getRequiredPersistentProperty("withId"))) + .isEqualTo(path("withId")); + softly.assertThat((Object) path("withId").append(path("withId").getRequiredIdProperty())) .isEqualTo(path("withId.withIdId")); }); } @@ -229,9 +244,9 @@ void isMultiValued() { softly.assertThat(path("second").isMultiValued()).isFalse(); softly.assertThat(path("second.third2").isMultiValued()).isFalse(); softly.assertThat(path("secondList.third2").isMultiValued()).isTrue(); // this seems wrong as third2 is an - // embedded path into Second, held by - // List (so the parent is - // multi-valued but not third2). + // embedded path into Second, held by + // List (so the parent is + // multi-valued but not third2). // TODO: This test fails because MultiValued considers parents. // softly.assertThat(path("secondList.third.value").isMultiValued()).isFalse(); softly.assertThat(path("secondList").isMultiValued()).isTrue(); @@ -306,13 +321,13 @@ void getTableAlias() { softly.assertThat(path("second.third2").getTableInfo().tableAlias()).isEqualTo(quoted("second")); softly.assertThat(path("second.third2.value").getTableInfo().tableAlias()).isEqualTo(quoted("second")); softly.assertThat(path("second.third").getTableInfo().tableAlias()).isEqualTo(quoted("second_third")); // missing - // _ + // _ softly.assertThat(path("second.third.value").getTableInfo().tableAlias()).isEqualTo(quoted("second_third")); // missing - // _ + // _ softly.assertThat(path("secondList.third2").getTableInfo().tableAlias()).isEqualTo(quoted("secondList")); softly.assertThat(path("secondList.third2.value").getTableInfo().tableAlias()).isEqualTo(quoted("secondList")); softly.assertThat(path("secondList.third").getTableInfo().tableAlias()).isEqualTo(quoted("secondList_third")); // missing - // _ + // _ softly.assertThat(path("secondList.third.value").getTableInfo().tableAlias()) .isEqualTo(quoted("secondList_third")); // missing _ softly.assertThat(path("secondList").getTableInfo().tableAlias()).isEqualTo(quoted("secondList")); @@ -416,20 +431,6 @@ void getBaseProperty() { }); } - @Test // GH-1525 - void getIdColumnName() { - - assertSoftly(softly -> { - - softly.assertThat(path().getTableInfo().idColumnName()).isEqualTo(quoted("ENTITY_ID")); - softly.assertThat(path("withId").getTableInfo().idColumnName()).isEqualTo(quoted("WITH_ID_ID")); - - softly.assertThat(path("second").getTableInfo().idColumnName()).isNull(); - softly.assertThat(path("second.third2").getTableInfo().idColumnName()).isNull(); - softly.assertThat(path("withId.second").getTableInfo().idColumnName()).isNull(); - }); - } - @Test // GH-1525 void toDotPath() { @@ -452,43 +453,89 @@ void getRequiredPersistentPropertyPath() { }); } - @Test // GH-1525 - void getEffectiveIdColumnName() { + @Test + // GH-1525 + void getLength() { assertSoftly(softly -> { + softly.assertThat(path().getLength()).isEqualTo(1); + softly.assertThat(path().stream().collect(Collectors.toList())).hasSize(1); - softly.assertThat(path().getTableInfo().effectiveIdColumnName()).isEqualTo(quoted("ENTITY_ID")); - softly.assertThat(path("second.third2").getTableInfo().effectiveIdColumnName()).isEqualTo(quoted("DUMMY_ENTITY")); - softly.assertThat(path("withId.second.third").getTableInfo().effectiveIdColumnName()) - .isEqualTo(quoted("WITH_ID")); - softly.assertThat(path("withId.second.third2.value").getTableInfo().effectiveIdColumnName()) - .isEqualTo(quoted("WITH_ID")); + softly.assertThat(path("second.third2").getLength()).isEqualTo(3); + softly.assertThat(path("second.third2").stream().collect(Collectors.toList())).hasSize(3); + + softly.assertThat(path("withId.second.third").getLength()).isEqualTo(4); + softly.assertThat(path("withId.second.third2.value").getLength()).isEqualTo(5); }); } - @Test // GH-1525 - void getLength() { + @Test // GH-574 + void getTail() { - assertThat(path().getLength()).isEqualTo(1); - assertThat(path().stream().collect(Collectors.toList())).hasSize(1); + assertSoftly(softly -> { - assertThat(path("second.third2").getLength()).isEqualTo(3); - assertThat(path("second.third2").stream().collect(Collectors.toList())).hasSize(3); + softly.assertThat((Object) path().getTail()).isEqualTo(null); + softly.assertThat((Object) path("second").getTail()).isEqualTo(null); + softly.assertThat(path("second.third").getTail().toDotPath()).isEqualTo("third"); + softly.assertThat(path("second.third.value").getTail().toDotPath()).isEqualTo("third.value"); + }); + } + + @Test // GH-74 + void append() { + + assertSoftly(softly -> { + + softly.assertThat(path("second").append(path()).toDotPath()).isEqualTo("second"); + softly.assertThat(path().append(path("second")).toDotPath()).isEqualTo("second"); + softly.assertThat(path().append(path("second.third")).toDotPath()).isEqualTo("second.third"); + AggregatePath value = path("second.third.value").getTail().getTail(); + softly.assertThat(path("second.third").append(value).toDotPath()).isEqualTo("second.third.value"); + }); + } + + @Test // GH-574 + void sortPaths() { + + Set sorted = new TreeSet<>(); + + AggregatePath alpha = path(); + AggregatePath as = path("second"); + AggregatePath ast = path("second.third"); + AggregatePath aw = path("withId"); + + sorted.add(aw); + sorted.add(ast); + sorted.add(as); + sorted.add(alpha); + + assertThat(sorted).containsExactly(alpha, as, ast, aw); - assertThat(path("withId.second.third").getLength()).isEqualTo(4); - assertThat(path("withId.second.third2.value").getLength()).isEqualTo(5); } private AggregatePath path() { return context.getAggregatePath(entity); } + private AggregatePath path(RelationalPersistentEntity entity) { + return context.getAggregatePath(entity); + } + + private AggregatePath path(Class entityType, String path) { + return context.getAggregatePath(createSimplePath(entityType, path)); + } + private AggregatePath path(String path) { return context.getAggregatePath(createSimplePath(path)); } PersistentPropertyPath createSimplePath(String path) { - return PersistentPropertyPathTestUtils.getPath(context, path, DummyEntity.class); + return createSimplePath(entity.getType(), path); + } + + PersistentPropertyPath createSimplePath(Class entityType, String path) { + + return PersistentPropertyPathTestUtils.getPath(context, path, entityType); } @SuppressWarnings("unused") @@ -502,6 +549,12 @@ static class DummyEntity { WithId withId; } + record CompoundId(Long one, String two) { + } + + record CompoundIdEntity(@Id CompoundId id, Second second) { + } + @SuppressWarnings("unused") static class Second { Third third; diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/RelationalMappingContextUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/mapping/RelationalMappingContextUnitTests.java index 14316048e41..f2b04d70b7f 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/render/SelectRendererUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/sql/render/SelectRendererUnitTests.java index 2ec941640ab..ad0ac531abd 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 b547aae17d0..28da149fb17 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 c3bba01ca0d..c41b6cd42bb 100644 --- a/src/main/antora/modules/ROOT/pages/jdbc/mapping.adoc +++ b/src/main/antora/modules/ROOT/pages/jdbc/mapping.adoc @@ -106,6 +106,9 @@ Also, the type of that aggregate is encoded in a type parameter. All references in an aggregate result in a foreign key relationship in the opposite direction in the database. By default, the name of the foreign key column is the table name of the referencing entity. +If the referenced id is an `@Embedded` id, the back reference consists of multiple columns, each named by a concatenation of + `_` + . +E.g. the back reference to a `Person` entity, with a composite id with the properties `firstName` and `lastName` will consist of the two columns `PERSON_FIRST_NAME` and `PERSON_LAST_NAME`. + Alternatively you may choose to have them named by the entity name of the referencing entity ignoring `@Table` annotations. You activate this behaviour by calling `setForeignKeyNaming(ForeignKeyNaming.IGNORE_RENAMING)` on the `RelationalMappingContext`. diff --git a/src/main/antora/modules/ROOT/partials/mapping.adoc b/src/main/antora/modules/ROOT/partials/mapping.adoc index 7e864516e2a..ed80c37fabf 100644 --- a/src/main/antora/modules/ROOT/partials/mapping.adoc +++ b/src/main/antora/modules/ROOT/partials/mapping.adoc @@ -149,6 +149,13 @@ Embedded entities containing a `Collection` or a `Map` will always be considered Such an entity will therefore never be `null` even when using @Embedded(onEmpty = USE_NULL). endif::[] +[[entity-persistence.embedded-ids]] +=== Embedded Ids + +Entities may be annotated with `@Id` and `@Embedded`, resulting in a composite id on the database side. +The full embedded entity is considered the id, and therefore the check for determining if an aggregate is considered a new aggregate requiring an insert or an existing one, asking for an update is based on that entity, not its elements. +Most use cases will require a custom `BeforeConvertCallback` to set the id for new aggregate. + [[entity-persistence.read-only-properties]] == Read Only Properties