parameter) {
+ return context.convert(object, parameter.getType());
+ }
+ }
+
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/ObjectPath.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/ObjectPath.java
new file mode 100644
index 0000000000..8fc2248e6f
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/ObjectPath.java
@@ -0,0 +1,109 @@
+/*
+ * 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.conversion;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+import org.springframework.util.ObjectUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * A path of objects nested into each other. The type allows access to all parent objects currently in creation even
+ * when resolving more nested objects. This allows to avoid re-resolving object instances that are logically equivalent
+ * to already resolved ones.
+ *
+ * An immutable ordered set of target objects for {@link org.springframework.data.relational.domain.RowDocument} to
+ * {@link Object} conversions. Object paths can be extended via
+ * {@link #push(Object, org.springframework.data.relational.core.mapping.RelationalPersistentEntity)}.
+ *
+ * @author Mark Paluch
+ * @since 3.2
+ */
+class ObjectPath {
+
+ static final ObjectPath ROOT = new ObjectPath();
+
+ private final @Nullable ObjectPath parent;
+ private final @Nullable Object object;
+
+ private ObjectPath() {
+
+ this.parent = null;
+ this.object = null;
+ }
+
+ /**
+ * Creates a new {@link ObjectPath} from the given parent {@link ObjectPath} and adding the provided path values.
+ *
+ * @param parent must not be {@literal null}.
+ * @param object must not be {@literal null}.
+ */
+ private ObjectPath(ObjectPath parent, Object object) {
+
+ this.parent = parent;
+ this.object = object;
+ }
+
+ /**
+ * Returns a copy of the {@link ObjectPath} with the given {@link Object} as current object.
+ *
+ * @param object must not be {@literal null}.
+ * @param entity must not be {@literal null}.
+ * @return new instance of {@link ObjectPath}.
+ */
+ ObjectPath push(Object object, RelationalPersistentEntity> entity) {
+
+ Assert.notNull(object, "Object must not be null");
+ Assert.notNull(entity, "RelationalPersistentEntity must not be null");
+
+ return new ObjectPath(this, object);
+ }
+
+ /**
+ * Returns the current object of the {@link ObjectPath} or {@literal null} if the path is empty.
+ *
+ * @return
+ */
+ @Nullable
+ Object getCurrentObject() {
+ return getObject();
+ }
+
+ @Nullable
+ private Object getObject() {
+ return object;
+ }
+
+ @Override
+ public String toString() {
+
+ if (parent == null) {
+ return "[empty]";
+ }
+
+ List strings = new ArrayList<>();
+
+ for (ObjectPath current = this; current != null; current = current.parent) {
+ strings.add(ObjectUtils.nullSafeToString(current.getObject()));
+ }
+
+ return StringUtils.collectionToDelimitedString(strings, " -> ");
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalConverter.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalConverter.java
index 55c9cf4b09..9cff6dc7ac 100644
--- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalConverter.java
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RelationalConverter.java
@@ -27,6 +27,7 @@
import org.springframework.data.mapping.model.ParameterValueProvider;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
+import org.springframework.data.relational.domain.RowDocument;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
@@ -53,6 +54,17 @@ public interface RelationalConverter {
*/
ConversionService getConversionService();
+ /**
+ * Read a {@link RowDocument} into the requested {@link Class aggregate type}.
+ *
+ * @param type target aggregate type.
+ * @param source source {@link RowDocument}.
+ * @return the converted object.
+ * @param aggregate type.
+ * @since 3.2
+ */
+ R read(Class type, RowDocument source);
+
/**
* Create a new instance of {@link PersistentEntity} given {@link ParameterValueProvider} to obtain constructor
* properties.
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RowDocumentAccessor.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RowDocumentAccessor.java
new file mode 100644
index 0000000000..ae6fcb59d2
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/RowDocumentAccessor.java
@@ -0,0 +1,109 @@
+/*
+ * 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.conversion;
+
+import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
+import org.springframework.data.relational.domain.RowDocument;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+
+/**
+ * Wrapper value object for a {@link RowDocument} to be able to access raw values by
+ * {@link RelationalPersistentProperty} references. The accessors will transparently resolve nested document values that
+ * a {@link RelationalPersistentProperty} might refer to through a path expression in field names.
+ *
+ * @author Mark Paluch
+ * @since 3.2
+ */
+record RowDocumentAccessor(RowDocument document) {
+
+ /**
+ * Creates a new {@link RowDocumentAccessor} for the given {@link RowDocument}.
+ *
+ * @param document must be a {@link RowDocument} effectively, must not be {@literal null}.
+ */
+ RowDocumentAccessor {
+
+ Assert.notNull(document, "Document must not be null");
+ }
+
+ /**
+ * @return the underlying {@link RowDocument document}.
+ */
+ public RowDocument getDocument() {
+ return this.document;
+ }
+
+ /**
+ * Copies all mappings from the given {@link RowDocument} to the underlying target {@link RowDocument}. These mappings
+ * will replace any mappings that the target document had for any of the keys currently in the specified map.
+ *
+ * @param source
+ */
+ public void putAll(RowDocument source) {
+
+ document.putAll(source);
+ }
+
+ /**
+ * Puts the given value into the backing {@link RowDocument} based on the coordinates defined through the given
+ * {@link RelationalPersistentProperty}. By default, this will be the plain field name. But field names might also
+ * consist of path traversals so we might need to create intermediate {@link RowDocument}s.
+ *
+ * @param prop must not be {@literal null}.
+ * @param value can be {@literal null}.
+ */
+ public void put(RelationalPersistentProperty prop, @Nullable Object value) {
+
+ Assert.notNull(prop, "RelationalPersistentProperty must not be null");
+ String fieldName = getColumnName(prop);
+
+ document.put(fieldName, value);
+ }
+
+ /**
+ * Returns the value the given {@link RelationalPersistentProperty} refers to. By default, this will be a direct field
+ * but the method will also transparently resolve nested values the {@link RelationalPersistentProperty} might refer
+ * to through a path expression in the field name metadata.
+ *
+ * @param property must not be {@literal null}.
+ * @return can be {@literal null}.
+ */
+ @Nullable
+ public Object get(RelationalPersistentProperty property) {
+ return document.get(getColumnName(property));
+ }
+
+ /**
+ * Returns whether the underlying {@link RowDocument} has a value ({@literal null} or non-{@literal null}) for the
+ * given {@link RelationalPersistentProperty}.
+ *
+ * @param property must not be {@literal null}.
+ * @return {@literal true} if no non {@literal null} value present.
+ */
+ @SuppressWarnings("unchecked")
+ public boolean hasValue(RelationalPersistentProperty property) {
+
+ Assert.notNull(property, "Property must not be null");
+
+ return document.containsKey(getColumnName(property));
+ }
+
+ String getColumnName(RelationalPersistentProperty prop) {
+ return prop.getColumnName().getReference();
+ }
+
+}
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 0ad660f907..3c5f67794b 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
@@ -334,9 +334,8 @@ static ColumnInfo of(AggregatePath path) {
// TODO: Multi-valued paths cannot be represented with a single column
// Assert.isTrue(!path.isMultiValued(), () -> "Cannot obtain ColumnInfo for multi-valued path");
- SqlIdentifier name = AggregatePathTableUtils.assembleColumnName(path,
- path.getRequiredLeafProperty().getColumnName());
- return new ColumnInfo(name, AggregatePathTableUtils.prefixWithTableAlias(path, name));
+ SqlIdentifier columnName = path.getRequiredLeafProperty().getColumnName();
+ return new ColumnInfo(columnName, AggregatePathTableUtils.prefixWithTableAlias(path, columnName));
}
}
}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/AggregatePathTableUtils.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/AggregatePathTableUtils.java
index fa02220f8e..a8f9eeaeb6 100644
--- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/AggregatePathTableUtils.java
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/AggregatePathTableUtils.java
@@ -33,18 +33,6 @@
*/
class AggregatePathTableUtils {
- public static SqlIdentifier assembleColumnName(AggregatePath path, SqlIdentifier suffix) {
- return suffix.transform(constructEmbeddedPrefix(path)::concat);
- }
-
- private static String constructEmbeddedPrefix(AggregatePath path) {
-
- return path.stream() //
- .filter(p -> p != path) //
- .takeWhile(AggregatePath::isEmbedded).map(p -> p.getRequiredLeafProperty().getEmbeddedPrefix()) //
- .collect(new ReverseJoinCollector());
- }
-
public static SqlIdentifier prefixWithTableAlias(AggregatePath path, SqlIdentifier columnName) {
AggregatePath tableOwner = AggregatePathTraversal.getTableOwningPath(path);
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/AggregatePathTraversal.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/AggregatePathTraversal.java
index b462a299e1..3a696a0410 100644
--- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/AggregatePathTraversal.java
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/AggregatePathTraversal.java
@@ -37,9 +37,9 @@ public static AggregatePath getIdDefiningPath(AggregatePath aggregatePath) {
public static AggregatePath getTableOwningPath(AggregatePath aggregatePath) {
- Predicate idDefiningPathFilter = ap -> ap.isEntity() && !ap.isEmbedded();
+ Predicate tableOwningPathFilter = ap -> ap.isEntity() && !ap.isEmbedded();
- AggregatePath result = aggregatePath.filter(idDefiningPathFilter);
+ AggregatePath result = aggregatePath.filter(tableOwningPathFilter);
if (result == null) {
throw new NoSuchElementException();
}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/EmbeddedContext.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/EmbeddedContext.java
new file mode 100644
index 0000000000..d5f2307335
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/EmbeddedContext.java
@@ -0,0 +1,45 @@
+/*
+ * 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;
+
+/**
+ * Holder for an embedded {@link RelationalPersistentProperty}
+ *
+ * @author Mark Paluch
+ * @since 3.2
+ */
+record EmbeddedContext(RelationalPersistentProperty ownerProperty) {
+
+ EmbeddedContext {
+ }
+
+ public String getEmbeddedPrefix() {
+ return ownerProperty.getEmbeddedPrefix();
+ }
+
+ public String withEmbeddedPrefix(String name) {
+
+ if (!ownerProperty.isEmbedded()) {
+ return name;
+ }
+ String embeddedPrefix = ownerProperty.getEmbeddedPrefix();
+ if (embeddedPrefix != null) {
+ return embeddedPrefix + name;
+ }
+
+ return name;
+ }
+}
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
new file mode 100644
index 0000000000..3bca90a2e6
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/EmbeddedRelationalPersistentEntity.java
@@ -0,0 +1,257 @@
+/*
+ * 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.lang.annotation.Annotation;
+import java.util.Iterator;
+
+import org.jetbrains.annotations.NotNull;
+import org.springframework.data.mapping.*;
+import org.springframework.data.mapping.model.PersistentPropertyAccessorFactory;
+import org.springframework.data.relational.core.sql.SqlIdentifier;
+import org.springframework.data.spel.EvaluationContextProvider;
+import org.springframework.data.util.Streamable;
+import org.springframework.data.util.TypeInformation;
+import org.springframework.lang.Nullable;
+
+/**
+ * Embedded entity extension for a {@link Embedded entity}.
+ *
+ * @author Mark Paluch
+ * @since 3.2
+ */
+class EmbeddedRelationalPersistentEntity implements RelationalPersistentEntity {
+
+ private final RelationalPersistentEntity delegate;
+
+ private final EmbeddedContext context;
+
+ public EmbeddedRelationalPersistentEntity(RelationalPersistentEntity delegate, EmbeddedContext context) {
+ this.delegate = delegate;
+ this.context = context;
+ }
+
+ @Override
+ public SqlIdentifier getTableName() {
+ throw new MappingException("Cannot map embedded entity to table");
+ }
+
+ @Override
+ public SqlIdentifier getIdColumn() {
+ throw new MappingException("Embedded entity does not have an id column");
+ }
+
+ @Override
+ public void addPersistentProperty(RelationalPersistentProperty property) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void addAssociation(Association association) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void verify() throws MappingException {}
+
+ @NotNull
+ @Override
+ public Iterator iterator() {
+
+ Iterator iterator = delegate.iterator();
+
+ return new Iterator<>() {
+ @Override
+ public boolean hasNext() {
+ return iterator.hasNext();
+ }
+
+ @Override
+ public RelationalPersistentProperty next() {
+ return wrap(iterator.next());
+ }
+ };
+ }
+
+ @Override
+ public void setPersistentPropertyAccessorFactory(PersistentPropertyAccessorFactory factory) {
+ delegate.setPersistentPropertyAccessorFactory(factory);
+ }
+
+ @Override
+ public void setEvaluationContextProvider(EvaluationContextProvider provider) {
+ delegate.setEvaluationContextProvider(provider);
+ }
+
+ @Override
+ public String getName() {
+ return delegate.getName();
+ }
+
+ @Override
+ @Deprecated
+ @Nullable
+ public PreferredConstructor getPersistenceConstructor() {
+ return delegate.getPersistenceConstructor();
+ }
+
+ @Override
+ @Nullable
+ public InstanceCreatorMetadata getInstanceCreatorMetadata() {
+ return delegate.getInstanceCreatorMetadata();
+ }
+
+ @Override
+ public boolean isCreatorArgument(PersistentProperty> property) {
+ return delegate.isCreatorArgument(property);
+ }
+
+ @Override
+ public boolean isIdProperty(PersistentProperty> property) {
+ return delegate.isIdProperty(property);
+ }
+
+ @Override
+ public boolean isVersionProperty(PersistentProperty> property) {
+ return delegate.isVersionProperty(property);
+ }
+
+ @Override
+ @Nullable
+ public RelationalPersistentProperty getIdProperty() {
+ return wrap(delegate.getIdProperty());
+ }
+
+ @Override
+ @Nullable
+ public RelationalPersistentProperty getVersionProperty() {
+ return wrap(delegate.getVersionProperty());
+ }
+
+ @Override
+ @Nullable
+ public RelationalPersistentProperty getPersistentProperty(String name) {
+ return wrap(delegate.getPersistentProperty(name));
+ }
+
+ @Override
+ public Iterable getPersistentProperties(Class extends Annotation> annotationType) {
+ return Streamable.of(delegate.getPersistentProperties(annotationType)).map(this::wrap);
+ }
+
+ @Override
+ public boolean hasIdProperty() {
+ return delegate.hasIdProperty();
+ }
+
+ @Override
+ public boolean hasVersionProperty() {
+ return delegate.hasVersionProperty();
+ }
+
+ @Override
+ public Class getType() {
+ return delegate.getType();
+ }
+
+ @Override
+ public Alias getTypeAlias() {
+ return delegate.getTypeAlias();
+ }
+
+ @Override
+ public TypeInformation getTypeInformation() {
+ return delegate.getTypeInformation();
+ }
+
+ @Override
+ public void doWithProperties(PropertyHandler handler) {
+ delegate.doWithProperties((PropertyHandler) persistentProperty -> {
+ handler.doWithPersistentProperty(wrap(persistentProperty));
+ });
+ }
+
+ @Override
+ public void doWithProperties(SimplePropertyHandler handler) {
+ delegate.doWithProperties((SimplePropertyHandler) property -> handler
+ .doWithPersistentProperty(wrap((RelationalPersistentProperty) property)));
+ }
+
+ @Override
+ public void doWithAssociations(AssociationHandler handler) {
+ delegate.doWithAssociations(handler);
+ }
+
+ @Override
+ public void doWithAssociations(SimpleAssociationHandler handler) {
+ delegate.doWithAssociations(handler);
+ }
+
+ @Override
+ @Nullable
+ public A findAnnotation(Class annotationType) {
+ return delegate.findAnnotation(annotationType);
+ }
+
+ @Override
+ public boolean isAnnotationPresent(Class annotationType) {
+ return delegate.isAnnotationPresent(annotationType);
+ }
+
+ @Override
+ public PersistentPropertyAccessor getPropertyAccessor(B bean) {
+ return delegate.getPropertyAccessor(bean);
+ }
+
+ @Override
+ public PersistentPropertyPathAccessor getPropertyPathAccessor(B bean) {
+ return delegate.getPropertyPathAccessor(bean);
+ }
+
+ @Override
+ public IdentifierAccessor getIdentifierAccessor(Object bean) {
+ return delegate.getIdentifierAccessor(bean);
+ }
+
+ @Override
+ public boolean isNew(Object bean) {
+ return delegate.isNew(bean);
+ }
+
+ @Override
+ public boolean isImmutable() {
+ return delegate.isImmutable();
+ }
+
+ @Override
+ public boolean requiresPropertyPopulation() {
+ return delegate.requiresPropertyPopulation();
+ }
+
+ @Nullable
+ private RelationalPersistentProperty wrap(@Nullable RelationalPersistentProperty source) {
+
+ if (source == null) {
+ return null;
+ }
+ return new EmbeddedRelationalPersistentProperty(source, context);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("EmbeddedRelationalPersistentEntity<%s>", getType());
+ }
+}
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/EmbeddedRelationalPersistentProperty.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/EmbeddedRelationalPersistentProperty.java
new file mode 100644
index 0000000000..a63f4335ad
--- /dev/null
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/EmbeddedRelationalPersistentProperty.java
@@ -0,0 +1,312 @@
+/*
+ * 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.lang.annotation.Annotation;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+
+import org.springframework.data.mapping.Association;
+import org.springframework.data.relational.core.sql.SqlIdentifier;
+import org.springframework.data.util.TypeInformation;
+import org.springframework.lang.Nullable;
+import org.springframework.util.ObjectUtils;
+
+/**
+ * Embedded property extension to a {@link RelationalPersistentProperty}
+ *
+ * @author Mark Paluch
+ * @since 3.2
+ */
+class EmbeddedRelationalPersistentProperty implements RelationalPersistentProperty {
+
+ private final RelationalPersistentProperty delegate;
+
+ private final EmbeddedContext context;
+
+ public EmbeddedRelationalPersistentProperty(RelationalPersistentProperty delegate, EmbeddedContext context) {
+
+ this.delegate = delegate;
+ this.context = context;
+ }
+
+ @Override
+ public boolean isEmbedded() {
+ return delegate.isEmbedded();
+ }
+
+ @Nullable
+ @Override
+ public String getEmbeddedPrefix() {
+ return context.withEmbeddedPrefix(delegate.getEmbeddedPrefix());
+ }
+
+ @Override
+ public SqlIdentifier getColumnName() {
+ return delegate.getColumnName().transform(context::withEmbeddedPrefix);
+ }
+
+ @Override
+ public RelationalPersistentEntity> getOwner() {
+ return delegate.getOwner();
+ }
+
+ @Override
+ @Deprecated(since = "3.2", forRemoval = true)
+ public SqlIdentifier getReverseColumnName(PersistentPropertyPathExtension path) {
+ return delegate.getReverseColumnName(path);
+ }
+
+ @Override
+ public SqlIdentifier getReverseColumnName(RelationalPersistentEntity> owner) {
+ return delegate.getReverseColumnName(owner);
+ }
+
+ @Override
+ @Nullable
+ public SqlIdentifier getKeyColumn() {
+ return delegate.getKeyColumn();
+ }
+
+ @Override
+ public boolean isQualified() {
+ return delegate.isQualified();
+ }
+
+ @Override
+ public Class> getQualifierColumnType() {
+ return delegate.getQualifierColumnType();
+ }
+
+ @Override
+ public boolean isOrdered() {
+ return delegate.isOrdered();
+ }
+
+ @Override
+ public boolean shouldCreateEmptyEmbedded() {
+ return delegate.shouldCreateEmptyEmbedded();
+ }
+
+ @Override
+ public boolean isInsertOnly() {
+ return delegate.isInsertOnly();
+ }
+
+ @Override
+ public String getName() {
+ return delegate.getName();
+ }
+
+ @Override
+ public Class> getType() {
+ return delegate.getType();
+ }
+
+ @Override
+ public TypeInformation> getTypeInformation() {
+ return delegate.getTypeInformation();
+ }
+
+ @Override
+ public Iterable extends TypeInformation>> getPersistentEntityTypeInformation() {
+ return delegate.getPersistentEntityTypeInformation();
+ }
+
+ @Override
+ @Nullable
+ public Method getGetter() {
+ return delegate.getGetter();
+ }
+
+ @Override
+ @Nullable
+ public Method getSetter() {
+ return delegate.getSetter();
+ }
+
+ @Override
+ @Nullable
+ public Method getWither() {
+ return delegate.getWither();
+ }
+
+ @Override
+ @Nullable
+ public Field getField() {
+ return delegate.getField();
+ }
+
+ @Override
+ @Nullable
+ public String getSpelExpression() {
+ return delegate.getSpelExpression();
+ }
+
+ @Override
+ @Nullable
+ public Association getAssociation() {
+ return delegate.getAssociation();
+ }
+
+ @Override
+ public Association getRequiredAssociation() {
+ return delegate.getRequiredAssociation();
+ }
+
+ @Override
+ public boolean isEntity() {
+ return delegate.isEntity();
+ }
+
+ @Override
+ public boolean isIdProperty() {
+ return delegate.isIdProperty();
+ }
+
+ @Override
+ public boolean isVersionProperty() {
+ return delegate.isVersionProperty();
+ }
+
+ @Override
+ public boolean isCollectionLike() {
+ return delegate.isCollectionLike();
+ }
+
+ @Override
+ public boolean isMap() {
+ return delegate.isMap();
+ }
+
+ @Override
+ public boolean isArray() {
+ return delegate.isArray();
+ }
+
+ @Override
+ public boolean isTransient() {
+ return delegate.isTransient();
+ }
+
+ @Override
+ public boolean isWritable() {
+ return delegate.isWritable();
+ }
+
+ @Override
+ public boolean isReadable() {
+ return delegate.isReadable();
+ }
+
+ @Override
+ public boolean isImmutable() {
+ return delegate.isImmutable();
+ }
+
+ @Override
+ public boolean isAssociation() {
+ return delegate.isAssociation();
+ }
+
+ @Override
+ @Nullable
+ public Class> getComponentType() {
+ return delegate.getComponentType();
+ }
+
+ @Override
+ public Class> getRawType() {
+ return delegate.getRawType();
+ }
+
+ @Override
+ @Nullable
+ public Class> getMapValueType() {
+ return delegate.getMapValueType();
+ }
+
+ @Override
+ public Class> getActualType() {
+ return delegate.getActualType();
+ }
+
+ @Override
+ @Nullable
+ public A findAnnotation(Class annotationType) {
+ return delegate.findAnnotation(annotationType);
+ }
+
+ @Override
+ @Nullable
+ public A findPropertyOrOwnerAnnotation(Class annotationType) {
+ return delegate.findPropertyOrOwnerAnnotation(annotationType);
+ }
+
+ @Override
+ public boolean isAnnotationPresent(Class extends Annotation> annotationType) {
+ return delegate.isAnnotationPresent(annotationType);
+ }
+
+ @Override
+ public boolean usePropertyAccess() {
+ return delegate.usePropertyAccess();
+ }
+
+ @Override
+ public boolean hasActualTypeAnnotation(Class extends Annotation> annotationType) {
+ return delegate.hasActualTypeAnnotation(annotationType);
+ }
+
+ @Override
+ @Nullable
+ public Class> getAssociationTargetType() {
+ return delegate.getAssociationTargetType();
+ }
+
+ @Override
+ @Nullable
+ public TypeInformation> getAssociationTargetTypeInformation() {
+ return delegate.getAssociationTargetTypeInformation();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o)
+ return true;
+ if (o == null || getClass() != o.getClass())
+ return false;
+
+ EmbeddedRelationalPersistentProperty that = (EmbeddedRelationalPersistentProperty) o;
+
+ if (!ObjectUtils.nullSafeEquals(delegate, that.delegate)) {
+ return false;
+ }
+ return ObjectUtils.nullSafeEquals(context, that.context);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = ObjectUtils.nullSafeHashCode(delegate);
+ result = 31 * result + ObjectUtils.nullSafeHashCode(context);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return delegate.toString();
+ }
+}
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 eb7be56d0d..28dd8c124d 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
@@ -214,7 +214,7 @@ public SqlIdentifier getColumnName() {
Assert.state(path != null, "Path is null");
- return assembleColumnName(path.getLeafProperty().getColumnName());
+ return path.getLeafProperty().getColumnName();
}
/**
@@ -451,26 +451,6 @@ private SqlIdentifier assembleTableAlias() {
}
- private SqlIdentifier assembleColumnName(SqlIdentifier suffix) {
-
- Assert.state(path != null, "Path is null");
-
- if (path.getLength() <= 1) {
- return suffix;
- }
-
- PersistentPropertyPath extends RelationalPersistentProperty> parentPath = path.getParentPath();
- RelationalPersistentProperty parentLeaf = parentPath.getLeafProperty();
-
- if (!parentLeaf.isEmbedded()) {
- return suffix;
- }
-
- String embeddedPrefix = parentLeaf.getEmbeddedPrefix();
-
- return getParentPath().assembleColumnName(suffix.transform(embeddedPrefix::concat));
- }
-
private SqlIdentifier prefixWithTableAlias(SqlIdentifier columnName) {
SqlIdentifier tableAlias = getTableAlias();
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 e237e56eeb..ac453b4a55 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
@@ -28,6 +28,7 @@
import org.springframework.data.spel.EvaluationContextProvider;
import org.springframework.data.spel.ExtensionAwareEvaluationContextProvider;
import org.springframework.data.util.TypeInformation;
+import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
@@ -110,6 +111,24 @@ public void setApplicationContext(ApplicationContext applicationContext) throws
this.expressionEvaluator.setProvider(new ExtensionAwareEvaluationContextProvider(applicationContext));
}
+ @Nullable
+ @Override
+ public RelationalPersistentEntity> getPersistentEntity(RelationalPersistentProperty persistentProperty) {
+
+ boolean embeddedDelegation = false;
+ if (persistentProperty instanceof EmbeddedRelationalPersistentProperty) {
+ embeddedDelegation = true;
+ }
+
+ RelationalPersistentEntity> entity = super.getPersistentEntity(persistentProperty);
+
+ if (entity != null && (persistentProperty.isEmbedded() || embeddedDelegation)) {
+ return new EmbeddedRelationalPersistentEntity<>(entity, new EmbeddedContext(persistentProperty));
+ }
+
+ return entity;
+ }
+
@Override
protected RelationalPersistentEntity createPersistentEntity(TypeInformation typeInformation) {
diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/domain/RowDocument.java b/spring-data-relational/src/main/java/org/springframework/data/relational/domain/RowDocument.java
index 2f6c2fe2f1..f2217766d7 100644
--- a/spring-data-relational/src/main/java/org/springframework/data/relational/domain/RowDocument.java
+++ b/spring-data-relational/src/main/java/org/springframework/data/relational/domain/RowDocument.java
@@ -41,11 +41,15 @@ public RowDocument() {
this.delegate = new LinkedCaseInsensitiveMap<>();
}
- public RowDocument(Map map) {
+ public RowDocument(Map map) {
this.delegate = new LinkedCaseInsensitiveMap<>();
this.delegate.putAll(delegate);
}
+ public static Object of(String field, Object value) {
+ return new RowDocument().append(field, value);
+ }
+
/**
* Retrieve the value at {@code key} as {@link List}.
*
diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/MappingRelationalConverterUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/MappingRelationalConverterUnitTests.java
new file mode 100644
index 0000000000..640b717307
--- /dev/null
+++ b/spring-data-relational/src/test/java/org/springframework/data/relational/core/conversion/MappingRelationalConverterUnitTests.java
@@ -0,0 +1,233 @@
+/*
+ * 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.conversion;
+
+import static org.assertj.core.api.Assertions.*;
+
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.convert.ConverterBuilder;
+import org.springframework.data.convert.ConverterBuilder.ConverterAware;
+import org.springframework.data.convert.CustomConversions;
+import org.springframework.data.convert.CustomConversions.StoreConversions;
+import org.springframework.data.mapping.model.SimpleTypeHolder;
+import org.springframework.data.relational.core.mapping.Embedded;
+import org.springframework.data.relational.core.mapping.RelationalMappingContext;
+import org.springframework.data.relational.domain.RowDocument;
+
+/**
+ * Unit tests for {@link MappingRelationalConverter}.
+ *
+ * @author Mark Paluch
+ */
+class MappingRelationalConverterUnitTests {
+
+ MappingRelationalConverter converter = new MappingRelationalConverter(new RelationalMappingContext());
+
+ @Test // GH-1586
+ void shouldReadSimpleType() {
+
+ RowDocument document = new RowDocument().append("id", "1").append("name", "John").append("age", 30);
+
+ SimpleType result = converter.read(SimpleType.class, document);
+
+ assertThat(result.id).isEqualTo("1");
+ assertThat(result.name).isEqualTo("John");
+ assertThat(result.age).isEqualTo(30);
+ }
+
+ @Test // GH-1586
+ void shouldReadSimpleRecord() {
+
+ RowDocument document = new RowDocument().append("id", "1").append("name", "John").append("age", 30);
+
+ SimpleRecord result = converter.read(SimpleRecord.class, document);
+
+ assertThat(result.id).isEqualTo("1");
+ assertThat(result.name).isEqualTo("John");
+ assertThat(result.age).isEqualTo(30);
+ }
+
+ @Test // GH-1586
+ void shouldEvaluateExpression() {
+
+ RowDocument document = new RowDocument().append("myprop", "bar");
+
+ WithExpression result = converter.read(WithExpression.class, document);
+
+ assertThat(result.name).isEqualTo("bar");
+ }
+
+ @Test // GH-1586
+ void shouldReadNonstaticInner() {
+
+ RowDocument document = new RowDocument().append("name", "John").append("child",
+ new RowDocument().append("name", "Johnny"));
+
+ Parent result = converter.read(Parent.class, document);
+
+ assertThat(result.name).isEqualTo("John");
+ assertThat(result.child.name).isEqualTo("Johnny");
+ }
+
+ @Test // GH-1586
+ void shouldReadEmbedded() {
+
+ RowDocument document = new RowDocument().append("simple_id", "1").append("simple_name", "John").append("simple_age",
+ 30);
+
+ WithEmbedded result = converter.read(WithEmbedded.class, document);
+
+ assertThat(result.simple).isNotNull();
+ assertThat(result.simple.id).isEqualTo("1");
+ assertThat(result.simple.name).isEqualTo("John");
+ assertThat(result.simple.age).isEqualTo(30);
+ }
+
+ @Test // GH-1586
+ void shouldReadWithLists() {
+
+ RowDocument nested = new RowDocument().append("id", "1").append("name", "John").append("age", 30);
+
+ RowDocument document = new RowDocument().append("strings", List.of("one", "two"))
+ .append("states", List.of("NEW", "USED")).append("entities", List.of(nested));
+
+ WithCollection result = converter.read(WithCollection.class, document);
+
+ assertThat(result.strings).containsExactly("one", "two");
+ assertThat(result.states).containsExactly(State.NEW, State.USED);
+ assertThat(result.entities).hasSize(1);
+
+ assertThat(result.entities.get(0).id).isEqualTo("1");
+ assertThat(result.entities.get(0).name).isEqualTo("John");
+ }
+
+ @Test // GH-1586
+ void shouldReadWithMaps() {
+
+ RowDocument nested = new RowDocument().append("id", "1").append("name", "John").append("age", 30);
+
+ RowDocument document = new RowDocument().append("strings", Map.of(1, "one", 2, "two")).append("entities",
+ Map.of(1, nested));
+
+ WithMap result = converter.read(WithMap.class, document);
+
+ assertThat(result.strings).hasSize(2).containsEntry(1, "one").containsEntry(2, "two");
+
+ assertThat(result.entities).hasSize(1);
+ assertThat(result.entities.get(1).id).isEqualTo("1");
+ assertThat(result.entities.get(1).name).isEqualTo("John");
+ }
+
+ @Test // GH-1586
+ void shouldApplyConverters() {
+
+ ConverterAware converterAware = ConverterBuilder.reading(String.class, Money.class, s -> {
+ String[] s1 = s.split(" ");
+ return new Money(Integer.parseInt(s1[0]), s1[1]);
+ }).andWriting(money -> money.amount + " " + money.currency);
+
+ CustomConversions conversions = new CustomConversions(StoreConversions.of(SimpleTypeHolder.DEFAULT),
+ List.of(converterAware));
+ RelationalMappingContext mappingContext = new RelationalMappingContext();
+ mappingContext.setSimpleTypeHolder(conversions.getSimpleTypeHolder());
+ mappingContext.afterPropertiesSet();
+
+ MappingRelationalConverter converter = new MappingRelationalConverter(mappingContext, conversions);
+
+ RowDocument document = new RowDocument().append("money", "1 USD");
+
+ WithMoney result = converter.read(WithMoney.class, document);
+
+ assertThat(result.money.amount).isEqualTo(1);
+ assertThat(result.money.currency).isEqualTo("USD");
+ }
+
+ static class SimpleType {
+
+ @Id String id;
+ String name;
+ int age;
+
+ }
+
+ record SimpleRecord(@Id String id, String name, int age) {
+ }
+
+ static class Parent {
+ String name;
+
+ Child child;
+
+ class Child {
+
+ String name;
+ }
+ }
+
+ static class WithCollection {
+
+ List strings;
+ EnumSet states;
+ List entities;
+ }
+
+ static class WithMap {
+
+ Map strings;
+ Map entities;
+ }
+
+ enum State {
+ NEW, USED, UNKNOWN
+ }
+
+ static class WithMoney {
+
+ Money money;
+
+ }
+
+ static class Money {
+ final int amount;
+ final String currency;
+
+ public Money(int amount, String currency) {
+ this.amount = amount;
+ this.currency = currency;
+ }
+ }
+
+ static class WithExpression {
+
+ private final String name;
+
+ public WithExpression(@Value("#root.myprop") String foo) {
+ this.name = foo;
+ }
+ }
+
+ static class WithEmbedded {
+
+ @Embedded.Nullable(prefix = "simple_") SimpleType simple;
+ }
+
+}
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 5c5b9df991..54178be9af 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
@@ -200,6 +200,7 @@ void isEmbedded() {
softly.assertThat(path().isEmbedded()).isFalse();
softly.assertThat(path("withId").isEmbedded()).isFalse();
softly.assertThat(path("second2.third").isEmbedded()).isFalse();
+ softly.assertThat(path("second2.third2").isEmbedded()).isTrue();
softly.assertThat(path("second2").isEmbedded()).isTrue();
});
@@ -390,8 +391,9 @@ void getRequiredLeafProperty() {
assertSoftly(softly -> {
- softly.assertThat(path("second.third2.value").getRequiredLeafProperty())
- .isEqualTo(context.getRequiredPersistentEntity(Third.class).getPersistentProperty("value"));
+ RelationalPersistentProperty prop = path("second.third2.value").getRequiredLeafProperty();
+ softly.assertThat(prop.getName()).isEqualTo("value");
+ softly.assertThat(prop.getOwner().getType()).isEqualTo(Third.class);
softly.assertThat(path("second.third").getRequiredLeafProperty())
.isEqualTo(context.getRequiredPersistentEntity(Second.class).getPersistentProperty("third"));
softly.assertThat(path("secondList").getRequiredLeafProperty())
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 c76b3a80b4..c0377fdca7 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
@@ -26,6 +26,7 @@
import org.springframework.data.annotation.Id;
import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.mapping.model.SimpleTypeHolder;
+import org.springframework.data.relational.core.sql.SqlIdentifier;
/**
* Unit tests for {@link RelationalMappingContext}.
@@ -87,8 +88,37 @@ public void rootAggregatePathsGetCached() {
assertThat(one).isSameAs(two);
}
+ @Test // GH-1586
+ void correctlyCascadesPrefix() {
+
+ RelationalPersistentEntity> entity = context.getRequiredPersistentEntity(WithEmbedded.class);
+
+ RelationalPersistentProperty parent = entity.getRequiredPersistentProperty("parent");
+ RelationalPersistentEntity> parentEntity = context.getRequiredPersistentEntity(parent);
+ RelationalPersistentProperty child = parentEntity.getRequiredPersistentProperty("child");
+ RelationalPersistentEntity> childEntity = context.getRequiredPersistentEntity(child);
+ RelationalPersistentProperty name = childEntity.getRequiredPersistentProperty("name");
+
+ assertThat(parent.getEmbeddedPrefix()).isEqualTo("prnt_");
+ assertThat(child.getEmbeddedPrefix()).isEqualTo("prnt_chld_");
+ assertThat(name.getColumnName()).isEqualTo(SqlIdentifier.quoted("PRNT_CHLD_NAME"));
+ }
+
static class EntityWithUuid {
@Id UUID uuid;
}
+ static class WithEmbedded {
+ @Embedded.Empty(prefix = "prnt_") Parent parent;
+ }
+
+ static class Parent {
+
+ @Embedded.Empty(prefix = "chld_") Child child;
+ }
+
+ static class Child {
+ String name;
+ }
+
}