From 41b37a6bd8d1f1f981cbc9ca9eea09f971a7049c Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 18 Sep 2023 10:00:00 +0200 Subject: [PATCH] Add support for multi-level projections using EntityProjection. We now support multi-level projections by introspecting the result and the domain type and read projections directly into a DTO or a backing map for interface projections. Original pull request #1618 Closes #1554 --- .../conversion/BasicRelationalConverter.java | 17 ++ .../MappingRelationalConverter.java | 235 +++++++++++++++++- .../core/conversion/RelationalConverter.java | 36 +++ .../core/conversion/RowDocumentAccessor.java | 32 ++- .../BasicRelationalPersistentProperty.java | 9 + .../EmbeddedRelationalPersistentProperty.java | 5 + .../mapping/PersistentPropertyTranslator.java | 92 +++++++ .../mapping/RelationalPersistentProperty.java | 8 + .../data/relational/domain/RowDocument.java | 17 +- .../MappingRelationalConverterUnitTests.java | 98 ++++++++ 10 files changed, 540 insertions(+), 9 deletions(-) create mode 100644 spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/PersistentPropertyTranslator.java diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/BasicRelationalConverter.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/BasicRelationalConverter.java index b9fb1f254a0..00c435e49a3 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/BasicRelationalConverter.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/BasicRelationalConverter.java @@ -39,6 +39,8 @@ import org.springframework.data.mapping.model.EntityInstantiators; import org.springframework.data.mapping.model.ParameterValueProvider; import org.springframework.data.mapping.model.SimpleTypeHolder; +import org.springframework.data.projection.EntityProjection; +import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; @@ -126,6 +128,21 @@ public PersistentPropertyPathAccessor getPropertyAccessor(PersistentEntit return new ConvertingPropertyAccessor<>(accessor, conversionService); } + @Override + public EntityProjection introspectProjection(Class resultType, Class entityType) { + throw new UnsupportedOperationException(); + } + + @Override + public ProjectionFactory getProjectionFactory() { + throw new UnsupportedOperationException(); + } + + @Override + public R project(EntityProjection descriptor, RowDocument document) { + throw new UnsupportedOperationException(); + } + @Override public R read(Class type, RowDocument source) { throw new UnsupportedOperationException(); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MappingRelationalConverter.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MappingRelationalConverter.java index 82185320dc4..66b42a8d525 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MappingRelationalConverter.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MappingRelationalConverter.java @@ -17,10 +17,14 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.function.Predicate; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; import org.springframework.core.CollectionFactory; import org.springframework.core.convert.ConversionService; import org.springframework.data.convert.CustomConversions; @@ -28,6 +32,7 @@ import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.Parameter; import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.ConvertingPropertyAccessor; @@ -39,12 +44,19 @@ import org.springframework.data.mapping.model.SpELContext; import org.springframework.data.mapping.model.SpELExpressionEvaluator; import org.springframework.data.mapping.model.SpELExpressionParameterValueProvider; +import org.springframework.data.projection.EntityProjection; +import org.springframework.data.projection.EntityProjectionIntrospector; +import org.springframework.data.projection.EntityProjectionIntrospector.ProjectionPredicate; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.relational.core.mapping.Embedded; import org.springframework.data.relational.core.mapping.Embedded.OnEmpty; +import org.springframework.data.relational.core.mapping.PersistentPropertyTranslator; 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.domain.RowDocument; +import org.springframework.data.util.Predicates; import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -57,10 +69,14 @@ * @author Mark Paluch * @since 3.2 */ -public class MappingRelationalConverter extends BasicRelationalConverter { +public class MappingRelationalConverter extends BasicRelationalConverter implements ApplicationContextAware { private SpELContext spELContext; + private final SpelAwareProxyProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory(); + + private final EntityProjectionIntrospector introspector; + /** * Creates a new {@link MappingRelationalConverter} given the new {@link RelationalMappingContext}. * @@ -71,6 +87,7 @@ public MappingRelationalConverter(RelationalMappingContext context) { super(context); this.spELContext = new SpELContext(DocumentPropertyAccessor.INSTANCE); + this.introspector = createIntrospector(projectionFactory, getConversions(), getMappingContext()); } /** @@ -85,6 +102,29 @@ public MappingRelationalConverter(RelationalMappingContext context, CustomConver super(context, conversions); this.spELContext = new SpELContext(DocumentPropertyAccessor.INSTANCE); + this.introspector = createIntrospector(projectionFactory, getConversions(), getMappingContext()); + + } + + private static EntityProjectionIntrospector createIntrospector(ProjectionFactory projectionFactory, + CustomConversions conversions, MappingContext mappingContext) { + + return EntityProjectionIntrospector.create(projectionFactory, + ProjectionPredicate.typeHierarchy().and((target, underlyingType) -> !conversions.isSimpleType(target)), + mappingContext); + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + + this.spELContext = new SpELContext(this.spELContext, applicationContext); + this.projectionFactory.setBeanFactory(applicationContext); + this.projectionFactory.setBeanClassLoader(applicationContext.getClassLoader()); + } + + @Override + public ProjectionFactory getProjectionFactory() { + return this.projectionFactory; } /** @@ -100,6 +140,128 @@ protected ConversionContext getConversionContext(ObjectPath path) { this::readMap, this::getPotentiallyConvertedSimpleRead); } + @Override + public EntityProjection introspectProjection(Class resultType, Class entityType) { + + RelationalPersistentEntity persistentEntity = getMappingContext().getPersistentEntity(entityType); + if (persistentEntity == null && !resultType.isInterface() + || ClassUtils.isAssignable(RowDocument.class, resultType)) { + return (EntityProjection) EntityProjection.nonProjecting(resultType); + } + return introspector.introspect(resultType, entityType); + } + + @Override + public R project(EntityProjection projection, RowDocument document) { + + if (!projection.isProjection()) { // backed by real object + + TypeInformation typeToRead = projection.getMappedType().getType().isInterface() ? projection.getDomainType() + : projection.getMappedType(); + return (R) read(typeToRead, document); + } + + ProjectingConversionContext context = new ProjectingConversionContext(this, getConversions(), ObjectPath.ROOT, + this::readCollectionOrArray, this::readMap, this::getPotentiallyConvertedSimpleRead, projection); + + return doReadProjection(context, document, projection); + } + + @SuppressWarnings("unchecked") + private R doReadProjection(ConversionContext context, RowDocument document, EntityProjection projection) { + + RelationalPersistentEntity entity = getMappingContext() + .getRequiredPersistentEntity(projection.getActualDomainType()); + TypeInformation mappedType = projection.getActualMappedType(); + RelationalPersistentEntity mappedEntity = (RelationalPersistentEntity) getMappingContext() + .getPersistentEntity(mappedType); + SpELExpressionEvaluator evaluator = new DefaultSpELExpressionEvaluator(document, spELContext); + + boolean isInterfaceProjection = mappedType.getType().isInterface(); + if (isInterfaceProjection) { + + PersistentPropertyTranslator propertyTranslator = PersistentPropertyTranslator.create(mappedEntity); + RowDocumentAccessor documentAccessor = new RowDocumentAccessor(document); + PersistentPropertyAccessor accessor = new MapPersistentPropertyAccessor(); + + PersistentPropertyAccessor convertingAccessor = PropertyTranslatingPropertyAccessor + .create(new ConvertingPropertyAccessor<>(accessor, getConversionService()), propertyTranslator); + RelationalPropertyValueProvider valueProvider = new RelationalPropertyValueProvider(context, documentAccessor, + evaluator, spELContext); + + readProperties(context, entity, convertingAccessor, documentAccessor, valueProvider, Predicates.isTrue()); + return (R) projectionFactory.createProjection(mappedType.getType(), accessor.getBean()); + } + + // DTO projection + if (mappedEntity == null) { + throw new MappingException(String.format("No mapping metadata found for %s", mappedType.getType().getName())); + } + + // create target instance, merge metadata from underlying DTO type + PersistentPropertyTranslator propertyTranslator = PersistentPropertyTranslator.create(entity, + Predicates.negate(RelationalPersistentProperty::hasExplicitColumnName)); + RowDocumentAccessor documentAccessor = new RowDocumentAccessor(document) { + + @Override + String getColumnName(RelationalPersistentProperty prop) { + return propertyTranslator.translate(prop).getColumnName().getReference(); + } + }; + + InstanceCreatorMetadata instanceCreatorMetadata = mappedEntity + .getInstanceCreatorMetadata(); + ParameterValueProvider provider = instanceCreatorMetadata != null + && instanceCreatorMetadata.hasParameters() + ? getParameterProvider(context, mappedEntity, documentAccessor, evaluator) + : NoOpParameterValueProvider.INSTANCE; + + EntityInstantiator instantiator = getEntityInstantiators().getInstantiatorFor(mappedEntity); + R instance = instantiator.createInstance(mappedEntity, provider); + PersistentPropertyAccessor accessor = mappedEntity.getPropertyAccessor(instance); + + populateProperties(context, mappedEntity, documentAccessor, evaluator, instance); + + PersistentPropertyAccessor convertingAccessor = new ConvertingPropertyAccessor<>(accessor, + getConversionService()); + RelationalPropertyValueProvider valueProvider = new RelationalPropertyValueProvider(context, documentAccessor, + evaluator, spELContext); + + readProperties(context, mappedEntity, convertingAccessor, documentAccessor, valueProvider, Predicates.isTrue()); + + return accessor.getBean(); + } + + private Object doReadOrProject(ConversionContext context, RowDocument source, TypeInformation typeHint, + EntityProjection typeDescriptor) { + + if (typeDescriptor.isProjection()) { + return doReadProjection(context, source, typeDescriptor); + } + + return readAggregate(context, source, typeHint); + } + + static class MapPersistentPropertyAccessor implements PersistentPropertyAccessor> { + + Map map = new LinkedHashMap<>(); + + @Override + public void setProperty(PersistentProperty persistentProperty, Object o) { + map.put(persistentProperty.getName(), o); + } + + @Override + public Object getProperty(PersistentProperty persistentProperty) { + return map.get(persistentProperty.getName()); + } + + @Override + public Map getBean() { + return map; + } + } + /** * Read a {@link RowDocument} into the requested {@link Class aggregate type}. * @@ -295,15 +457,14 @@ private S populateProperties(ConversionContext context, RelationalPersistent evaluator, spELContext); Predicate propertyFilter = isConstructorArgument(entity).negate(); - readProperties(contextToUse, entity, accessor, documentAccessor, valueProvider, evaluator, propertyFilter); + readProperties(contextToUse, entity, accessor, documentAccessor, valueProvider, propertyFilter); return accessor.getBean(); } private void readProperties(ConversionContext context, RelationalPersistentEntity entity, PersistentPropertyAccessor accessor, RowDocumentAccessor documentAccessor, - RelationalPropertyValueProvider valueProvider, SpELExpressionEvaluator evaluator, - Predicate propertyFilter) { + RelationalPropertyValueProvider valueProvider, Predicate propertyFilter) { for (RelationalPersistentProperty prop : entity) { @@ -475,6 +636,44 @@ interface ContainerValueConverter { } + /** + * @since 3.4.3 + */ + class ProjectingConversionContext extends DefaultConversionContext { + + private final EntityProjection returnedTypeDescriptor; + + ProjectingConversionContext(RelationalConverter sourceConverter, CustomConversions customConversions, + ObjectPath path, ContainerValueConverter> collectionConverter, + ContainerValueConverter> mapConverter, ValueConverter elementConverter, + EntityProjection projection) { + super(sourceConverter, customConversions, path, + (context, source, typeHint) -> doReadOrProject(context, source, typeHint, projection), + + collectionConverter, mapConverter, elementConverter); + this.returnedTypeDescriptor = projection; + } + + @Override + public ConversionContext forProperty(String name) { + + EntityProjection property = returnedTypeDescriptor.findProperty(name); + if (property == null) { + return new DefaultConversionContext(sourceConverter, conversions, objectPath, + MappingRelationalConverter.this::readAggregate, collectionConverter, mapConverter, elementConverter); + } + + return new ProjectingConversionContext(sourceConverter, conversions, objectPath, collectionConverter, + mapConverter, elementConverter, property); + } + + @Override + public ConversionContext withPath(ObjectPath currentPath) { + return new ProjectingConversionContext(sourceConverter, conversions, currentPath, collectionConverter, + mapConverter, elementConverter, returnedTypeDescriptor); + } + } + /** * Conversion context defining an interface for graph-traversal-based conversion of row documents. Entrypoint for * recursive conversion of {@link RowDocument} and other types. @@ -633,4 +832,32 @@ protected T potentiallyConvertSpelValue(Object object, Parameter (PersistentPropertyAccessor delegate, + PersistentPropertyTranslator propertyTranslator) implements PersistentPropertyAccessor { + + static PersistentPropertyAccessor create(PersistentPropertyAccessor delegate, + PersistentPropertyTranslator propertyTranslator) { + return new PropertyTranslatingPropertyAccessor<>(delegate, propertyTranslator); + } + + @Override + public void setProperty(PersistentProperty property, @Nullable Object value) { + delegate.setProperty(translate(property), value); + } + + @Override + public Object getProperty(PersistentProperty property) { + return delegate.getProperty(translate(property)); + } + + @Override + public T getBean() { + return delegate.getBean(); + } + + private RelationalPersistentProperty translate(PersistentProperty property) { + return propertyTranslator.translate((RelationalPersistentProperty) property); + } + } + } 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 9cff6dc7acb..c78d9ea61e3 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 @@ -25,6 +25,9 @@ import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.EntityInstantiators; import org.springframework.data.mapping.model.ParameterValueProvider; +import org.springframework.data.projection.EntityProjection; +import org.springframework.data.projection.EntityProjectionIntrospector; +import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.domain.RowDocument; @@ -54,6 +57,39 @@ public interface RelationalConverter { */ ConversionService getConversionService(); + /** + * Returns the {@link ProjectionFactory} for this converter. + * + * @return will never be {@literal null}. + * @since 3.2 + */ + ProjectionFactory getProjectionFactory(); + + /** + * Introspect the given {@link Class result type} in the context of the {@link Class entity type} whether the returned + * type is a projection and what property paths are participating in the projection. + * + * @param resultType the type to project on. Must not be {@literal null}. + * @param entityType the source domain type. Must not be {@literal null}. + * @return the introspection result. + * @since 3.2 + * @see EntityProjectionIntrospector#introspect(Class, Class) + */ + EntityProjection introspectProjection(Class resultType, Class entityType); + + /** + * Apply a projection to {@link RowDocument} and return the projection return type {@code R}. + * {@link EntityProjection#isProjection() Non-projecting} descriptors fall back to {@link #read(Class, RowDocument) + * regular object materialization}. + * + * @param descriptor the projection descriptor, must not be {@literal null}. + * @param document must not be {@literal null}. + * @param + * @return a new instance of the projection return type {@code R}. + * @since 3.2 + */ + R project(EntityProjection descriptor, RowDocument document); + /** * Read a {@link RowDocument} into the requested {@link Class aggregate type}. * 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 index 74568aac855..4aa62c76940 100644 --- 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 @@ -15,6 +15,8 @@ */ package org.springframework.data.relational.core.conversion; +import java.util.Objects; + import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.domain.RowDocument; import org.springframework.lang.Nullable; @@ -28,16 +30,19 @@ * @author Mark Paluch * @since 3.2 */ -record RowDocumentAccessor(RowDocument document) { +class RowDocumentAccessor { + + private final 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 { + RowDocumentAccessor(RowDocument document) { Assert.notNull(document, "Document must not be null"); + this.document = document; } /** @@ -105,4 +110,27 @@ String getColumnName(RelationalPersistentProperty prop) { return prop.getColumnName().getReference(); } + public RowDocument document() { + return document; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) + return true; + if (obj == null || obj.getClass() != this.getClass()) + return false; + var that = (RowDocumentAccessor) obj; + return Objects.equals(this.document, that.document); + } + + @Override + public int hashCode() { + return Objects.hash(document); + } + + @Override + public String toString() { + return "RowDocumentAccessor[" + "document=" + document + ']'; + } } 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 b5f335c8c81..42d10c5d173 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 @@ -50,12 +50,14 @@ public class BasicRelationalPersistentProperty extends AnnotationBasedPersistent private static final SpelExpressionParser PARSER = new SpelExpressionParser(); private final Lazy columnName; + private final boolean hasExplicitColumnName; private final @Nullable Expression columnNameExpression; private final Lazy> collectionIdColumnName; private final @Nullable Expression collectionIdColumnNameExpression; private final Lazy collectionKeyColumnName; private final @Nullable Expression collectionKeyColumnNameExpression; private final boolean isEmbedded; + private final String embeddedPrefix; private final NamingStrategy namingStrategy; private boolean forceQuote = true; @@ -128,6 +130,7 @@ public BasicRelationalPersistentProperty(Property property, PersistentEntity StringUtils.hasText(column.value()) ? createSqlIdentifier(column.value()) : createDerivedSqlIdentifier(namingStrategy.getColumnName(this))); @@ -138,6 +141,7 @@ public BasicRelationalPersistentProperty(Property property, PersistentEntity createDerivedSqlIdentifier(namingStrategy.getColumnName(this))); this.columnNameExpression = null; } @@ -208,6 +212,11 @@ public SqlIdentifier getColumnName() { return createSqlIdentifier(expressionEvaluator.evaluate(columnNameExpression)); } + @Override + public boolean hasExplicitColumnName() { + return hasExplicitColumnName; + } + @Override public RelationalPersistentEntity getOwner() { return (RelationalPersistentEntity) super.getOwner(); 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 index a63f4335ad2..cb39e3dafea 100644 --- 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 @@ -59,6 +59,11 @@ public SqlIdentifier getColumnName() { return delegate.getColumnName().transform(context::withEmbeddedPrefix); } + @Override + public boolean hasExplicitColumnName() { + return delegate.hasExplicitColumnName(); + } + @Override public RelationalPersistentEntity getOwner() { return delegate.getOwner(); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/PersistentPropertyTranslator.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/PersistentPropertyTranslator.java new file mode 100644 index 00000000000..bcd90d6ce2f --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/PersistentPropertyTranslator.java @@ -0,0 +1,92 @@ +/* + * 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.util.Predicates; +import org.springframework.lang.Nullable; + +/** + * Utility to translate a {@link RelationalPersistentProperty} into a corresponding property from a different + * {@link RelationalPersistentEntity} by looking it up by name. + *

+ * Mainly used within the framework. + * + * @author Mark Paluch + * @since 3.2 + */ +public class PersistentPropertyTranslator { + + /** + * Translate a {@link RelationalPersistentProperty} into a corresponding property from a different + * {@link RelationalPersistentEntity}. + * + * @param property must not be {@literal null}. + * @return the translated property. Can be the original {@code property}. + */ + public RelationalPersistentProperty translate(RelationalPersistentProperty property) { + return property; + } + + /** + * Create a new {@link PersistentPropertyTranslator}. + * + * @param targetEntity must not be {@literal null}. + * @return the property translator to use. + */ + public static PersistentPropertyTranslator create(@Nullable RelationalPersistentEntity targetEntity) { + return create(targetEntity, Predicates.isTrue()); + } + + /** + * Create a new {@link PersistentPropertyTranslator} accepting a {@link Predicate filter predicate} whether the + * translation should happen at all. + * + * @param targetEntity must not be {@literal null}. + * @param translationFilter must not be {@literal null}. + * @return the property translator to use. + */ + public static PersistentPropertyTranslator create(@Nullable RelationalPersistentEntity targetEntity, + Predicate translationFilter) { + return targetEntity != null ? new EntityPropertyTranslator(targetEntity, translationFilter) + : new PersistentPropertyTranslator(); + } + + private static class EntityPropertyTranslator extends PersistentPropertyTranslator { + + private final RelationalPersistentEntity targetEntity; + private final Predicate translationFilter; + + EntityPropertyTranslator(RelationalPersistentEntity targetEntity, + Predicate translationFilter) { + this.targetEntity = targetEntity; + this.translationFilter = translationFilter; + } + + @Override + public RelationalPersistentProperty translate(RelationalPersistentProperty property) { + + if (!translationFilter.test(property)) { + return property; + } + + RelationalPersistentProperty targetProperty = targetEntity.getPersistentProperty(property.getName()); + return targetProperty != null ? targetProperty : property; + } + } + +} diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentProperty.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentProperty.java index 1b736f299ab..afb5ede2c1b 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentProperty.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/RelationalPersistentProperty.java @@ -35,6 +35,14 @@ public interface RelationalPersistentProperty extends PersistentProperty getOwner(); 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 b2295fbe525..38d77b2adf4 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 @@ -24,6 +24,7 @@ import java.util.function.Function; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; import org.springframework.util.LinkedCaseInsensitiveMap; import org.springframework.util.ObjectUtils; @@ -47,7 +48,14 @@ public RowDocument(Map map) { this.delegate.putAll(delegate); } - public static Object of(String field, Object value) { + /** + * Factory method to create a RowDocument from a field and value. + * + * @param field the file name to use. + * @param value the value to use, can be {@literal null}. + * @return + */ + public static RowDocument of(String field, @Nullable Object value) { return new RowDocument().append(field, value); } @@ -131,7 +139,10 @@ public Object get(Object key) { @Nullable @Override - public Object put(String key, Object value) { + public Object put(String key, @Nullable Object value) { + + Assert.notNull(key, "Key must not be null!"); + return delegate.put(key, value); } @@ -142,7 +153,7 @@ public Object put(String key, Object value) { * @param value * @return */ - public RowDocument append(String key, Object value) { + public RowDocument append(String key, @Nullable Object value) { put(key, value); return this; 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 index 640b7173074..b869fc1ee31 100644 --- 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 @@ -17,18 +17,25 @@ import static org.assertj.core.api.Assertions.*; +import java.util.Collections; +import java.util.Date; import java.util.EnumSet; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Set; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.PersistenceCreator; 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.projection.EntityProjection; +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.domain.RowDocument; @@ -161,6 +168,30 @@ void shouldApplyConverters() { assertThat(result.money.currency).isEqualTo("USD"); } + @Test // GH-1554 + void projectShouldReadNestedProjection() { + + RowDocument source = RowDocument.of("addresses", Collections.singletonList(RowDocument.of("s", "hwy"))); + + EntityProjection projection = converter + .introspectProjection(WithNestedProjection.class, Person.class); + WithNestedProjection person = converter.project(projection, source); + + assertThat(person.getAddresses()).extracting(AddressProjection::getStreet).hasSize(1).containsOnly("hwy"); + } + + @Test // GH-1554 + void projectShouldReadProjectionWithNestedEntity() { + + RowDocument source = RowDocument.of("addresses", Collections.singletonList(RowDocument.of("s", "hwy"))); + + EntityProjection projection = converter + .introspectProjection(ProjectionWithNestedEntity.class, Person.class); + ProjectionWithNestedEntity person = converter.project(projection, source); + + assertThat(person.getAddresses()).extracting(Address::getStreet).hasSize(1).containsOnly("hwy"); + } + static class SimpleType { @Id String id; @@ -230,4 +261,71 @@ static class WithEmbedded { @Embedded.Nullable(prefix = "simple_") SimpleType simple; } + static class Person { + + @Id String id; + + Date birthDate; + + @Column("foo") String firstname; + String lastname; + + Set

addresses; + + Person() { + + } + + @PersistenceCreator + public Person(Set
addresses) { + this.addresses = addresses; + } + } + + static class Address { + + @Column("s") String street; + String city; + + public String getStreet() { + return street; + } + + public String getCity() { + return city; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Address address = (Address) o; + return Objects.equals(street, address.street) && Objects.equals(city, address.city); + } + + @Override + public int hashCode() { + return Objects.hash(street, city); + } + } + + interface WithNestedProjection { + + Set getAddresses(); + } + + interface ProjectionWithNestedEntity { + + Set
getAddresses(); + } + + interface AddressProjection { + + String getStreet(); + } + }