{
this.rootIdValueSource = IdValueSource.forInstance(root,
context.getRequiredPersistentEntity(aggregateChange.getEntityType()));
this.paths = context.findPersistentPropertyPaths(entityType, (p) -> p.isEntity() && !p.isEmbedded()) //
- .filter(PersistentPropertyPathExtension::isWritable).toList();
+ .filter(AggregatePathUtil::isWritable).toList();
}
/**
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/AggregatePath.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/AggregatePath.java
new file mode 100644
index 0000000000..c1ecdb6009
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/AggregatePath.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2023 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.data.relational.core.mapping;
+
+import java.util.function.Predicate;
+
+import org.springframework.data.mapping.PersistentPropertyPath;
+import org.springframework.lang.Nullable;
+
+/**
+ * Represents a path within an aggregate starting from the aggregate root.
+ *
+ * The path implements {@link Iterable} to iterate over all path segments including the path root.
+ *
+ * @since 3.2
+ * @author Jens Schauder
+ * @author Mark Paluch
+ */
+public interface AggregatePath extends Iterable {
+
+ /**
+ * Returns {@code true} if the current path is a root path element (i.e. {@link #getLength()} equals zero) or
+ * {@code false} if the path points to a leaf property.
+ *
+ * @return {@code true} if the current path is a root path element or {@code false} if the path points to a leaf
+ * property.
+ */
+ boolean isRoot();
+
+ /**
+ * Returns the path that has the same beginning but is one segment shorter than this path.
+ *
+ * @return the parent path. Guaranteed to be not {@literal null}.
+ * @throws IllegalStateException when called on an empty path.
+ */
+ AggregatePath getParentPath();
+
+ /**
+ * The {@link RelationalPersistentEntity} associated with the leaf of this path.
+ *
+ * @return Might return {@literal null} when called on a path that does not represent an entity.
+ */
+ @Nullable
+ RelationalPersistentEntity> getLeafEntity();
+
+ /**
+ * The {@link RelationalPersistentEntity} associated with the leaf of this path or throw {@link IllegalStateException}
+ * if the leaf cannot be resolved.
+ *
+ * @return the required {@link RelationalPersistentEntity} associated with the leaf of this path.
+ * @throws IllegalStateException if the persistent entity cannot be resolved.
+ */
+ RelationalPersistentEntity> getRequiredLeafEntity();
+
+ RelationalPersistentProperty getRequiredIdProperty();
+
+ int getLength();
+
+ /**
+ * Returns {@literal true} exactly when the path is non-empty and the leaf property an embedded one.
+ *
+ * @return if the leaf property is embedded.
+ */
+ boolean isEmbedded();
+
+ /**
+ * @return {@literal true} when this is an empty path or the path references an entity.
+ */
+ boolean isEntity();
+
+ String toDotPath();
+
+ /**
+ * Returns {@literal true} if there are multiple values for this path, i.e. if the path contains at least one element
+ * that is a collection and array or a map.
+ *
+ * @return {@literal true} if the path contains a multivalued element.
+ */
+ boolean isMultiValued();
+
+ /**
+ * @return {@literal true} if the leaf property of this path is a {@link java.util.Map}.
+ * @see RelationalPersistentProperty#isMap()
+ */
+ boolean isMap();
+
+ /**
+ * @return {@literal true} when this is references a {@link java.util.List} or {@link java.util.Map}.
+ */
+ boolean isQualified();
+
+ RelationalPersistentProperty getRequiredLeafProperty();
+
+ RelationalPersistentProperty getBaseProperty();
+
+ /**
+ * @return {@literal true} when this is references a {@link java.util.Collection} or an array.
+ */
+ boolean isCollectionLike();
+
+ /**
+ * @return whether the leaf end of the path is ordered, i.e. the data to populate must be ordered.
+ * @see RelationalPersistentProperty#isOrdered()
+ */
+ boolean isOrdered();
+
+ /**
+ * Creates a new path by extending the current path by the property passed as an argument.
+ *
+ * @param property must not be {@literal null}.
+ * @return Guaranteed to be not {@literal null}.
+ */
+ AggregatePath append(RelationalPersistentProperty property);
+
+ PersistentPropertyPath extends RelationalPersistentProperty> getRequiredPersistentPropertyPath();
+
+ /**
+ * Filter the {@link AggregatePath} hierarchy by walking all path segment from the leaf to {@link #isRoot() root}
+ * applying the given filter {@link Predicate}. Returns a matching {@link AggregatePath} or {@literal null} if the
+ * filter predicate does not match any path segment.
+ *
+ * @param predicate
+ * @return the matched aggregate path element or {@code null} if the filter predicate does not match any path segment.
+ */
+ @Nullable
+ default AggregatePath filter(Predicate predicate) {
+
+ AggregatePath path = this;
+ while (!predicate.test(path)) {
+
+ if (path.isRoot()) {
+ path = null;
+ break;
+ }
+ path = path.getParentPath();
+ }
+
+ return path;
+ }
+
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/AggregatePathUtil.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/AggregatePathUtil.java
new file mode 100644
index 0000000000..bc6cb15106
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/AggregatePathUtil.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2023 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.data.relational.core.mapping;
+
+import org.springframework.data.mapping.PersistentPropertyPath;
+import org.springframework.data.relational.core.sql.SqlIdentifier;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+public final class AggregatePathUtil {
+
+ private AggregatePathUtil() {
+ throw new IllegalStateException("This class should never get instantiated");
+ }
+
+ public static boolean hasIdProperty(AggregatePath path) {
+
+ RelationalPersistentEntity> leafEntity = path.getLeafEntity();
+ return leafEntity != null && leafEntity.hasIdProperty();
+ }
+
+ /**
+ * Returns the longest ancestor path that has an {@link org.springframework.data.annotation.Id} property.
+ *
+ * @return A path that starts just as this path but is shorter. Guaranteed to be not {@literal null}.
+ */
+ public static AggregatePath getIdDefiningParentPath(AggregatePath path) {
+
+ // TODO: What if the path is a root and the filter method returns null?
+ return path.getParentPath().filter(AggregatePathUtil::hasIdProperty);
+ }
+
+ /**
+ * If the table owning ancestor has an id the column name of that id property is returned. Otherwise the reverse
+ * column is returned.
+ */
+ public static SqlIdentifier getEffectiveIdColumnName(AggregatePath path) {
+
+ AggregatePath owner = getTableOwningAncestor(path);
+ return owner.isRoot() ? owner.getRequiredLeafEntity().getIdColumn() : getReverseColumnName(owner);
+ }
+
+ /**
+ * The alias used in select for the column used to reference the id in the parent table.
+ *
+ * @throws IllegalStateException when called on an empty path.
+ */
+ public static SqlIdentifier getReverseColumnNameAlias(AggregatePath path) {
+
+ return prefixWithTableAlias(path, getReverseColumnName(path));
+ }
+
+ /**
+ * The name of the column used to reference the id in the parent table.
+ *
+ * @throws IllegalStateException when called on an empty path.
+ */
+ public static SqlIdentifier getReverseColumnName(AggregatePath path) {
+
+ Assert.state(!path.isRoot(), "Empty paths don't have a reverse column name");
+
+ return path.getRequiredLeafProperty().getReverseColumnName(path);
+ }
+
+ /**
+ * Finds and returns the longest path with ich identical or an ancestor to the current path and maps directly to a
+ * table.
+ *
+ * @return a path. Guaranteed to be not {@literal null}.
+ */
+ private static AggregatePath getTableOwningAncestor(AggregatePath path) {
+ return path.isEntity() && !path.isEmbedded() ? path : getTableOwningAncestor(path.getParentPath());
+ }
+
+ @Nullable
+ private static SqlIdentifier assembleTableAlias(AggregatePath path) {
+
+ Assert.state(!path.isRoot(), "Path is null");
+
+ RelationalPersistentProperty leafProperty = path.getRequiredLeafProperty();
+ String prefix;
+ if (path.isEmbedded()) {
+ prefix = leafProperty.getEmbeddedPrefix();
+
+ } else {
+ prefix = leafProperty.getName();
+ }
+
+ if (path.getLength() == 1) {
+ Assert.notNull(prefix, "Prefix mus not be null");
+ return StringUtils.hasText(prefix) ? SqlIdentifier.quoted(prefix) : null;
+ }
+
+ AggregatePath parentPath = path.getParentPath();
+ SqlIdentifier sqlIdentifier = assembleTableAlias(parentPath);
+
+ if (sqlIdentifier != null) {
+
+ return parentPath.isEmbedded() ? sqlIdentifier.transform(name -> name.concat(prefix))
+ : sqlIdentifier.transform(name -> name + "_" + prefix);
+ }
+ return SqlIdentifier.quoted(prefix);
+
+ }
+
+ /**
+ * The alias used for the table on which this path is based.
+ *
+ * @return a table alias, {@literal null} if the table owning path is the empty path.
+ */
+ @Nullable
+ private static SqlIdentifier findTableAlias(AggregatePath path) {
+
+ AggregatePath tableOwner = getTableOwningAncestor(path);
+
+ return tableOwner.isRoot() ? null : assembleTableAlias(tableOwner);
+
+ }
+
+ private static SqlIdentifier assembleColumnName(AggregatePath path, SqlIdentifier suffix) {
+
+ Assert.state(!path.isRoot(), "Path is null");
+
+ if (path.getLength() <= 1) {
+ return suffix;
+ }
+
+ PersistentPropertyPath extends RelationalPersistentProperty> parentPath = path.getParentPath()
+ .getRequiredPersistentPropertyPath();
+ RelationalPersistentProperty parentLeaf = parentPath.getLeafProperty();
+
+ if (!parentLeaf.isEmbedded()) {
+ return suffix;
+ }
+
+ String embeddedPrefix = parentLeaf.getEmbeddedPrefix();
+
+ return assembleColumnName(path.getParentPath(), suffix.transform(embeddedPrefix::concat));
+ }
+
+ private static SqlIdentifier prefixWithTableAlias(AggregatePath path, SqlIdentifier columnName) {
+
+ SqlIdentifier tableAlias = findTableAlias(path);
+ return tableAlias == null ? columnName : columnName.transform(name -> tableAlias.getReference() + "_" + name);
+ }
+
+ public static boolean isWritable(@Nullable PersistentPropertyPath extends RelationalPersistentProperty> path) {
+ return path == null || path.getLeafProperty().isWritable() && isWritable(path.getParentPath());
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java
index f15abc92ef..ce1f3d6219 100644
--- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java
@@ -225,6 +225,12 @@ public SqlIdentifier getReverseColumnName(PersistentPropertyPathExtension path)
return createSqlIdentifier(expressionEvaluator.evaluate(collectionIdColumnNameExpression));
}
+ @Override
+ public SqlIdentifier getReverseColumnName(AggregatePath path) {
+ return collectionIdColumnName.get()
+ .orElseGet(() -> createDerivedSqlIdentifier(this.namingStrategy.getReverseColumnName(path)));
+ }
+
@Override
public SqlIdentifier getKeyColumn() {
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/CachingNamingStrategy.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/CachingNamingStrategy.java
index 496fa30e0c..56ee07bb35 100644
--- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/CachingNamingStrategy.java
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/CachingNamingStrategy.java
@@ -62,6 +62,10 @@ public String getTableName(Class> type) {
return tableNames.computeIfAbsent(type, delegate::getTableName);
}
+ @Override
+ public String getReverseColumnName(AggregatePath path) {
+ return delegate.getReverseColumnName(path);
+ }
@Override
public String getReverseColumnName(PersistentPropertyPathExtension path) {
return delegate.getReverseColumnName(path);
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/ColumnDetector.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/ColumnDetector.java
new file mode 100644
index 0000000000..fd9cd1b031
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/ColumnDetector.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2023 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.relational.core.mapping;
+
+import org.springframework.data.mapping.PersistentPropertyPath;
+import org.springframework.data.relational.core.sql.SqlIdentifier;
+import org.springframework.util.Assert;
+
+/**
+ * @author Mark Paluch
+ */
+public class ColumnDetector extends TableAccessor {
+
+ private final AggregatePath path;
+
+ private ColumnDetector(AggregatePath path) {
+ super(path);
+ this.path = path;
+ }
+
+ public static ColumnDetector of(AggregatePath path) {
+ return new ColumnDetector(path);
+ }
+
+ public static ColumnDetector of(TableAccessor tableOwner) {
+ if (tableOwner instanceof ColumnDetector) {
+ return (ColumnDetector) tableOwner;
+ }
+
+ return new ColumnDetector(tableOwner.getPath());
+ }
+
+ @Override
+ ColumnDetector createTableAccessor(AggregatePath path) {
+ return of(path);
+ }
+
+ @Override
+ public ColumnDetector getTableOwner() {
+ return (ColumnDetector) super.getTableOwner();
+ }
+
+ /**
+ * The column name of the id column of the ancestor path that represents an actual table.
+ */
+ public SqlIdentifier getIdColumnName() {
+ return getTableOwner().getPath().getRequiredLeafEntity().getIdColumn();
+ }
+
+ /**
+ * If the table owning ancestor has an id the column name of that id property is returned. Otherwise the reverse
+ * column is returned.
+ */
+ public SqlIdentifier getEffectiveIdColumnName() {
+
+ AggregatePath owner = getTableOwner().getPath();
+ return owner.isRoot() ? owner.getRequiredLeafEntity().getIdColumn()
+ : SingleColumnAggregatePath.of(owner).getReverseColumnName();
+ }
+
+ /**
+ * The name of the column used to represent this property in the database.
+ *
+ * @throws IllegalStateException when called on an empty path.
+ */
+ public SqlIdentifier getColumnName() {
+
+ Assert.state(!path.isRoot(), "Path is null");
+
+ return assembleColumnName(path, path.getRequiredLeafProperty().getColumnName());
+ }
+
+ /**
+ * The alias for the column used to represent this property in the database.
+ */
+ public SqlIdentifier getColumnAlias() {
+ return prefixWithTableAlias(getColumnName());
+ }
+
+ private static SqlIdentifier assembleColumnName(AggregatePath path, SqlIdentifier suffix) {
+
+ if (path.getLength() <= 1) {
+ return suffix;
+ }
+
+ PersistentPropertyPath extends RelationalPersistentProperty> parentPath = path.getParentPath()
+ .getRequiredPersistentPropertyPath();
+ RelationalPersistentProperty parentLeaf = parentPath.getLeafProperty();
+
+ if (!parentLeaf.isEmbedded()) {
+ return suffix;
+ }
+
+ String embeddedPrefix = parentLeaf.getEmbeddedPrefix();
+
+ return assembleColumnName(path.getParentPath(), suffix.transform(embeddedPrefix::concat));
+ }
+
+}
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
new file mode 100644
index 0000000000..7f4c952d1b
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DefaultAggregatePath.java
@@ -0,0 +1,308 @@
+/*
+ * Copyright 2023 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.data.relational.core.mapping;
+
+import java.util.Iterator;
+import java.util.Objects;
+
+import org.springframework.data.mapping.PersistentPropertyPath;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+
+/**
+ * Represents a path within an aggregate starting from the aggregate root.
+ *
+ * @since 3.2
+ * @author Jens Schauder
+ */
+class DefaultAggregatePath implements AggregatePath {
+
+ private final RelationalMappingContext context;
+
+ private final @Nullable RelationalPersistentEntity> rootType;
+
+ private final @Nullable PersistentPropertyPath extends RelationalPersistentProperty> path;
+
+ DefaultAggregatePath(RelationalMappingContext context,
+ PersistentPropertyPath extends RelationalPersistentProperty> path) {
+
+ Assert.notNull(context, "context must not be null");
+ Assert.notNull(path, "path must not be null");
+
+ this.context = context;
+ this.path = path;
+
+ this.rootType = null;
+ }
+
+ DefaultAggregatePath(RelationalMappingContext context, RelationalPersistentEntity> rootType) {
+
+ Assert.notNull(context, "context must not be null");
+ Assert.notNull(rootType, "rootType must not be null");
+
+ this.context = context;
+ this.rootType = rootType;
+
+ this.path = null;
+ }
+
+ public static boolean isWritable(@Nullable PersistentPropertyPath extends RelationalPersistentProperty> path) {
+ return path == null || path.getLeafProperty().isWritable() && isWritable(path.getParentPath());
+ }
+
+ @Override
+ public boolean isRoot() {
+ return path == null;
+ }
+
+ /**
+ * Returns the path that has the same beginning but is one segment shorter than this path.
+ *
+ * @return the parent path. Guaranteed to be not {@literal null}.
+ * @throws IllegalStateException when called on an empty path.
+ */
+ @Override
+ public AggregatePath getParentPath() {
+
+ if (isRoot()) {
+ throw new IllegalStateException("The parent path of a root path is not defined.");
+ }
+
+ if (path.getLength() == 1) {
+ return context.getAggregatePath(path.getLeafProperty().getOwner());
+ }
+
+ return context.getAggregatePath(path.getParentPath());
+ }
+
+ /**
+ * The {@link RelationalPersistentEntity} associated with the leaf of this path.
+ *
+ * @return Might return {@literal null} when called on a path that does not represent an entity.
+ */
+ @Override
+ @Nullable
+ public RelationalPersistentEntity> getLeafEntity() {
+ return isRoot() ? rootType : context.getPersistentEntity(getRequiredLeafProperty().getActualType());
+ }
+
+ /**
+ * The {@link RelationalPersistentEntity} associated with the leaf of this path or throw {@link IllegalStateException}
+ * if the leaf cannot be resolved.
+ *
+ * @return the required {@link RelationalPersistentEntity} associated with the leaf of this path.
+ * @throws IllegalStateException if the persistent entity cannot be resolved.
+ */
+ @Override
+ public RelationalPersistentEntity> getRequiredLeafEntity() {
+
+ RelationalPersistentEntity> entity = getLeafEntity();
+
+ if (entity == null) {
+
+ throw new IllegalStateException(
+ String.format("Couldn't resolve leaf PersistentEntity for type %s", path.getLeafProperty().getActualType()));
+ }
+
+ return entity;
+ }
+
+ @Override
+ public RelationalPersistentProperty getRequiredIdProperty() {
+ return isRoot() ? rootType.getRequiredIdProperty() : getRequiredLeafEntity().getRequiredIdProperty();
+
+ }
+
+ @Override
+ public int getLength() {
+ return isRoot() ? 0 : path.getLength();
+ }
+
+ @Override
+ public Iterator iterator() {
+
+ return new Iterator<>() {
+
+ AggregatePath current = DefaultAggregatePath.this;
+
+ @Override
+ public boolean hasNext() {
+ return current != null;
+ }
+
+ @Override
+ public AggregatePath next() {
+ AggregatePath current = this.current;
+
+ if (!current.isRoot()) {
+ this.current = current.getParentPath();
+ } else {
+ this.current = null;
+ }
+
+ return current;
+ }
+ };
+ }
+
+ /**
+ * Returns {@literal true} exactly when the path is non-empty and the leaf property an embedded one.
+ *
+ * @return if the leaf property is embedded.
+ */
+ @Override
+ public boolean isEmbedded() {
+ return !isRoot() && getRequiredLeafProperty().isEmbedded();
+ }
+
+ /**
+ * @return {@literal true} when this is an empty path or the path references an entity.
+ */
+ @Override
+ public boolean isEntity() {
+ return isRoot() || getRequiredLeafProperty().isEntity();
+ }
+
+ @Override
+ public String toString() {
+ return "AggregatePath["
+ + (rootType == null ? path.getBaseProperty().getOwner().getType().getName() : rootType.getName()) + "]"
+ + ((isRoot()) ? "/" : path.toDotPath());
+ }
+
+ @Override
+ public String toDotPath() {
+ return isRoot() ? "" : path.toDotPath();
+ }
+
+ /**
+ * Returns {@literal true} if there are multiple values for this path, i.e. if the path contains at least one element
+ * that is a collection and array or a map.
+ *
+ * @return {@literal true} if the path contains a multivalued element.
+ */
+ @Override
+ public boolean isMultiValued() {
+
+ if (isRoot()) {
+ return false;
+ }
+
+ RelationalPersistentProperty property = getRequiredLeafProperty();
+
+ return property.isCollectionLike() //
+ || property.isQualified() //
+ || getParentPath().isMultiValued();
+ }
+
+ /**
+ * @return {@literal true} if the leaf property of this path is a {@link java.util.Map}.
+ * @see RelationalPersistentProperty#isMap()
+ */
+ @Override
+ public boolean isMap() {
+ return !isRoot() && getRequiredLeafProperty().isMap();
+ }
+
+ /**
+ * @return {@literal true} when this is references a {@link java.util.List} or {@link java.util.Map}.
+ */
+ @Override
+ public boolean isQualified() {
+ return !isRoot() && getRequiredLeafProperty().isQualified();
+ }
+
+ @Override
+ public RelationalPersistentProperty getRequiredLeafProperty() {
+
+ if (isRoot()) {
+ throw new IllegalStateException("Root path does not have a leaf property");
+ }
+
+ return path.getLeafProperty();
+ }
+
+ @Override
+ public RelationalPersistentProperty getBaseProperty() {
+
+ if (isRoot()) {
+ throw new IllegalStateException("Root path does not have a base property");
+ }
+
+ return path.getBaseProperty();
+ }
+
+ /**
+ * @return {@literal true} when this is references a {@link java.util.Collection} or an array.
+ */
+ @Override
+ public boolean isCollectionLike() {
+ return !isRoot() && getRequiredLeafProperty().isCollectionLike();
+ }
+
+ /**
+ * @return whether the leaf end of the path is ordered, i.e. the data to populate must be ordered.
+ * @see RelationalPersistentProperty#isOrdered()
+ */
+ @Override
+ public boolean isOrdered() {
+ return !isRoot() && getRequiredLeafProperty().isOrdered();
+ }
+
+ /**
+ * Creates a new path by extending the current path by the property passed as an argument.
+ *
+ * @param property must not be {@literal null}.
+ * @return Guaranteed to be not {@literal null}.
+ */
+ @Override
+ public AggregatePath append(RelationalPersistentProperty property) {
+
+ PersistentPropertyPath extends RelationalPersistentProperty> newPath = isRoot() //
+ ? context.getPersistentPropertyPath(property.getName(), rootType.getType()) //
+ : context.getPersistentPropertyPath(path.toDotPath() + "." + property.getName(),
+ path.getBaseProperty().getOwner().getType());
+
+ return context.getAggregatePath(newPath);
+ }
+
+ @Override
+ public PersistentPropertyPath extends RelationalPersistentProperty> getRequiredPersistentPropertyPath() {
+
+ Assert.state(!isRoot(), "Required path is not present");
+
+ return path;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+
+ if (this == o)
+ return true;
+ if (o == null || getClass() != o.getClass())
+ return false;
+ DefaultAggregatePath that = (DefaultAggregatePath) o;
+ return Objects.equals(context, that.context) && Objects.equals(rootType, that.rootType)
+ && Objects.equals(path, that.path);
+ }
+
+ @Override
+ public int hashCode() {
+
+ return Objects.hash(context, rootType, path);
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DefaultNamingStrategy.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DefaultNamingStrategy.java
index 95898e80d9..b86fc3d02b 100644
--- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DefaultNamingStrategy.java
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/DefaultNamingStrategy.java
@@ -61,6 +61,14 @@ public String getReverseColumnName(PersistentPropertyPathExtension path) {
return getColumnNameReferencing(leafEntity);
}
+ @Override
+ public String getReverseColumnName(AggregatePath path) {
+
+ RelationalPersistentEntity> leafEntity = AggregatePathUtil.getIdDefiningParentPath(path).getRequiredLeafEntity();
+
+ return getColumnNameReferencing(leafEntity);
+ }
+
private String getColumnNameReferencing(RelationalPersistentEntity> leafEntity) {
if (foreignKeyNaming == ForeignKeyNaming.IGNORE_RENAMING) {
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/ForeignTableDetector.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/ForeignTableDetector.java
new file mode 100644
index 0000000000..d83abe6eb1
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/ForeignTableDetector.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2023 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.relational.core.mapping;
+
+import org.springframework.data.relational.core.sql.SqlIdentifier;
+import org.springframework.util.Assert;
+
+/**
+ * @author Mark Paluch
+ */
+public class ForeignTableDetector extends TableAccessor{
+
+ private final AggregatePath path;
+
+ ForeignTableDetector(AggregatePath path) {
+ super(path);
+ this.path = path;
+ }
+
+ public static ForeignTableDetector of(AggregatePath path) {
+
+ Assert.notNull(path, "AggregatePath must not be null");
+
+ if (path.isRoot()) {
+ throw new IllegalStateException("Root path does not map to a single column");
+ }
+
+ if (path.isEmbedded()) {
+ throw new IllegalStateException(String.format("Embedded property %s does not map to a foreign table", path));
+ }
+
+ if (!path.isQualified()) {
+ throw new IllegalStateException(String.format("Property %s does not map to a foreign table", path));
+ }
+
+ return new ForeignTableDetector(path);
+ }
+
+ /**
+ * The column name used for the list index or map key of the leaf property of this path.
+ *
+ * @throws IllegalStateException if the key column cannot be determined for the current path.
+ */
+ public SqlIdentifier getQualifierColumn() {
+
+ SqlIdentifier keyColumn = path.getRequiredLeafProperty().getKeyColumn();
+
+ if (keyColumn == null) {
+ throw new IllegalStateException("Cannot determine key column for %s".formatted(path));
+ }
+ return keyColumn;
+ }
+
+ /**
+ * The type of the qualifier column of the leaf property of this path or {@literal null} if this is not applicable.
+ *
+ * @return may be {@literal null}.
+ */
+ public Class> getQualifierColumnType() {
+
+ RelationalPersistentProperty property = path.getRequiredLeafProperty();
+
+ return property.getQualifierColumnType();
+ }
+
+ /**
+ * The name of the column used to reference the id in the parent table.
+ *
+ * @throws IllegalStateException when called on an empty path.
+ */
+ public SqlIdentifier getReverseColumnName() {
+ return path.getRequiredLeafProperty().getReverseColumnName(path);
+ }
+
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/NamingStrategy.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/NamingStrategy.java
index 8cbaadc47b..ed255d9ee6 100644
--- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/NamingStrategy.java
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/NamingStrategy.java
@@ -15,7 +15,6 @@
*/
package org.springframework.data.relational.core.mapping;
-import org.springframework.data.relational.core.sql.IdentifierProcessing;
import org.springframework.data.util.ParsingUtils;
import org.springframework.util.Assert;
@@ -88,8 +87,25 @@ default String getReverseColumnName(RelationalPersistentProperty property) {
return property.getOwner().getTableName().getReference();
}
+ /**
+ * @deprecated use {@link #getReverseColumnName(AggregatePath)} instead.
+ */
+ @Deprecated(since = "3.2", forRemoval = true)
default String getReverseColumnName(PersistentPropertyPathExtension path) {
- return getTableName(path.getIdDefiningParentPath().getRequiredLeafEntity().getType());
+ return getReverseColumnName(path.getAggregatePath());
+ }
+
+ /**
+ * provides the name of the column referencing the parent entity.
+ *
+ * @param path the path for which the reverse column name should get determined. Must not be null.
+ * @return a column name.
+ * @since 3.2
+ */
+ default String getReverseColumnName(AggregatePath path) {
+
+ AggregatePath idDefiningParentPath = AggregatePathUtil.getIdDefiningParentPath(path);
+ return getTableName(idDefiningParentPath.getRequiredLeafEntity().getType());
}
/**
@@ -104,4 +120,5 @@ default String getKeyColumn(RelationalPersistentProperty property) {
return getReverseColumnName(property) + "_key";
}
+
}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/PersistentPropertyPathExtension.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/PersistentPropertyPathExtension.java
index 96358884fb..11277bd11c 100644
--- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/PersistentPropertyPathExtension.java
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/PersistentPropertyPathExtension.java
@@ -20,7 +20,6 @@
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.mapping.context.MappingContext;
-import org.springframework.data.relational.core.sql.IdentifierProcessing;
import org.springframework.data.relational.core.sql.SqlIdentifier;
import org.springframework.data.util.Lazy;
import org.springframework.lang.Nullable;
@@ -35,7 +34,9 @@
* @author Daniil Razorenov
* @author Kurt Niemi
* @since 1.1
+ * @deprecated use {@link AggregatePath} instead
*/
+@Deprecated(since = "3.2", forRemoval = true)
public class PersistentPropertyPathExtension {
private final RelationalPersistentEntity> entity;
@@ -155,8 +156,8 @@ public RelationalPersistentEntity> getRequiredLeafEntity() {
if (this.path == null) {
throw new IllegalStateException("Couldn't resolve leaf PersistentEntity absent path");
}
- throw new IllegalStateException(String.format("Couldn't resolve leaf PersistentEntity for type %s",
- path.getLeafProperty().getActualType()));
+ throw new IllegalStateException(
+ String.format("Couldn't resolve leaf PersistentEntity for type %s", path.getLeafProperty().getActualType()));
}
return entity;
@@ -387,14 +388,6 @@ public Class> getActualType() {
: path.getLeafProperty().getActualType();
}
- /**
- * @return whether the leaf end of the path is ordered, i.e. the data to populate must be ordered.
- * @see RelationalPersistentProperty#isOrdered()
- */
- public boolean isOrdered() {
- return path != null && path.getLeafProperty().isOrdered();
- }
-
/**
* @return {@literal true} if the leaf property of this path is a {@link java.util.Map}.
* @see RelationalPersistentProperty#isMap()
@@ -481,8 +474,7 @@ private SqlIdentifier assembleColumnName(SqlIdentifier suffix) {
private SqlIdentifier prefixWithTableAlias(SqlIdentifier columnName) {
SqlIdentifier tableAlias = getTableAlias();
- return tableAlias == null ? columnName
- : columnName.transform(name -> tableAlias.getReference() + "_" + name);
+ return tableAlias == null ? columnName : columnName.transform(name -> tableAlias.getReference() + "_" + name);
}
@Override
@@ -500,4 +492,13 @@ public boolean equals(@Nullable Object o) {
public int hashCode() {
return Objects.hash(entity, path);
}
+
+ public AggregatePath getAggregatePath() {
+ if (path != null) {
+
+ return ((RelationalMappingContext) context).getAggregatePath(path);
+ } else {
+ return ((RelationalMappingContext) context).getAggregatePath(entity);
+ }
+ }
}
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 1c70375cc3..9bfad5d904 100644
--- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalMappingContext.java
@@ -15,8 +15,12 @@
*/
package org.springframework.data.relational.core.mapping;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
+import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.mapping.context.AbstractMappingContext;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.Property;
@@ -39,6 +43,8 @@ public class RelationalMappingContext
extends AbstractMappingContext, RelationalPersistentProperty> {
private final NamingStrategy namingStrategy;
+ private final Map