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
This commit is contained in:
Mark Paluch
2023-09-18 10:00:00 +02:00
committed by Jens Schauder
parent 9c36eb970b
commit 41b37a6bd8
10 changed files with 540 additions and 9 deletions

View File

@@ -39,6 +39,8 @@ import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
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 class BasicRelationalConverter implements RelationalConverter {
return new ConvertingPropertyAccessor<>(accessor, conversionService);
}
@Override
public <M, D> EntityProjection<M, D> introspectProjection(Class<M> resultType, Class<D> entityType) {
throw new UnsupportedOperationException();
}
@Override
public ProjectionFactory getProjectionFactory() {
throw new UnsupportedOperationException();
}
@Override
public <R> R project(EntityProjection<R, ?> descriptor, RowDocument document) {
throw new UnsupportedOperationException();
}
@Override
public <R> R read(Class<R> type, RowDocument source) {
throw new UnsupportedOperationException();

View File

@@ -17,10 +17,14 @@ package org.springframework.data.relational.core.conversion;
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.InstanceCreatorMetadata;
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.PropertyValueProvider;
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 @@ import org.springframework.util.ClassUtils;
* @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 class MappingRelationalConverter extends BasicRelationalConverter {
super(context);
this.spELContext = new SpELContext(DocumentPropertyAccessor.INSTANCE);
this.introspector = createIntrospector(projectionFactory, getConversions(), getMappingContext());
}
/**
@@ -85,6 +102,29 @@ public class MappingRelationalConverter extends BasicRelationalConverter {
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 @@ public class MappingRelationalConverter extends BasicRelationalConverter {
this::readMap, this::getPotentiallyConvertedSimpleRead);
}
@Override
public <M, D> EntityProjection<M, D> introspectProjection(Class<M> resultType, Class<D> 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> R project(EntityProjection<R, ?> 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> R doReadProjection(ConversionContext context, RowDocument document, EntityProjection<R, ?> projection) {
RelationalPersistentEntity<?> entity = getMappingContext()
.getRequiredPersistentEntity(projection.getActualDomainType());
TypeInformation<?> mappedType = projection.getActualMappedType();
RelationalPersistentEntity<R> mappedEntity = (RelationalPersistentEntity<R>) 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<RelationalPersistentProperty> instanceCreatorMetadata = mappedEntity
.getInstanceCreatorMetadata();
ParameterValueProvider<RelationalPersistentProperty> provider = instanceCreatorMetadata != null
&& instanceCreatorMetadata.hasParameters()
? getParameterProvider(context, mappedEntity, documentAccessor, evaluator)
: NoOpParameterValueProvider.INSTANCE;
EntityInstantiator instantiator = getEntityInstantiators().getInstantiatorFor(mappedEntity);
R instance = instantiator.createInstance(mappedEntity, provider);
PersistentPropertyAccessor<R> 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<String, Object>> {
Map<String, Object> 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<String, Object> getBean() {
return map;
}
}
/**
* Read a {@link RowDocument} into the requested {@link Class aggregate type}.
*
@@ -295,15 +457,14 @@ public class MappingRelationalConverter extends BasicRelationalConverter {
evaluator, spELContext);
Predicate<RelationalPersistentProperty> 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<RelationalPersistentProperty> propertyFilter) {
RelationalPropertyValueProvider valueProvider, Predicate<RelationalPersistentProperty> propertyFilter) {
for (RelationalPersistentProperty prop : entity) {
@@ -475,6 +636,44 @@ public class MappingRelationalConverter extends BasicRelationalConverter {
}
/**
* @since 3.4.3
*/
class ProjectingConversionContext extends DefaultConversionContext {
private final EntityProjection<?, ?> returnedTypeDescriptor;
ProjectingConversionContext(RelationalConverter sourceConverter, CustomConversions customConversions,
ObjectPath path, ContainerValueConverter<Collection<?>> collectionConverter,
ContainerValueConverter<Map<?, ?>> mapConverter, ValueConverter<Object> 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 @@ public class MappingRelationalConverter extends BasicRelationalConverter {
}
}
private record PropertyTranslatingPropertyAccessor<T> (PersistentPropertyAccessor<T> delegate,
PersistentPropertyTranslator propertyTranslator) implements PersistentPropertyAccessor<T> {
static <T> PersistentPropertyAccessor<T> create(PersistentPropertyAccessor<T> 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);
}
}
}

View File

@@ -25,6 +25,9 @@ import org.springframework.data.mapping.PersistentPropertyPathAccessor;
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)
*/
<M, D> EntityProjection<M, D> introspectProjection(Class<M> resultType, Class<D> 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 <R>
* @return a new instance of the projection return type {@code R}.
* @since 3.2
*/
<R> R project(EntityProjection<R, ?> descriptor, RowDocument document);
/**
* Read a {@link RowDocument} into the requested {@link Class aggregate type}.
*

View File

@@ -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 @@ import org.springframework.util.Assert;
* @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 @@ record RowDocumentAccessor(RowDocument document) {
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 + ']';
}
}

View File

@@ -50,12 +50,14 @@ public class BasicRelationalPersistentProperty extends AnnotationBasedPersistent
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
private final Lazy<SqlIdentifier> columnName;
private final boolean hasExplicitColumnName;
private final @Nullable Expression columnNameExpression;
private final Lazy<Optional<SqlIdentifier>> collectionIdColumnName;
private final @Nullable Expression collectionIdColumnNameExpression;
private final Lazy<SqlIdentifier> 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 class BasicRelationalPersistentProperty extends AnnotationBasedPersistent
if (isAnnotationPresent(Column.class)) {
Column column = getRequiredAnnotation(Column.class);
this.hasExplicitColumnName = StringUtils.hasText(column.value());
this.columnName = Lazy.of(() -> StringUtils.hasText(column.value()) ? createSqlIdentifier(column.value())
: createDerivedSqlIdentifier(namingStrategy.getColumnName(this)));
@@ -138,6 +141,7 @@ public class BasicRelationalPersistentProperty extends AnnotationBasedPersistent
}
} else {
this.hasExplicitColumnName = false;
this.columnName = Lazy.of(() -> createDerivedSqlIdentifier(namingStrategy.getColumnName(this)));
this.columnNameExpression = null;
}
@@ -208,6 +212,11 @@ public class BasicRelationalPersistentProperty extends AnnotationBasedPersistent
return createSqlIdentifier(expressionEvaluator.evaluate(columnNameExpression));
}
@Override
public boolean hasExplicitColumnName() {
return hasExplicitColumnName;
}
@Override
public RelationalPersistentEntity<?> getOwner() {
return (RelationalPersistentEntity<?>) super.getOwner();

View File

@@ -59,6 +59,11 @@ class EmbeddedRelationalPersistentProperty implements RelationalPersistentProper
return delegate.getColumnName().transform(context::withEmbeddedPrefix);
}
@Override
public boolean hasExplicitColumnName() {
return delegate.hasExplicitColumnName();
}
@Override
public RelationalPersistentEntity<?> getOwner() {
return delegate.getOwner();

View File

@@ -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.
* <p>
* 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<RelationalPersistentProperty> translationFilter) {
return targetEntity != null ? new EntityPropertyTranslator(targetEntity, translationFilter)
: new PersistentPropertyTranslator();
}
private static class EntityPropertyTranslator extends PersistentPropertyTranslator {
private final RelationalPersistentEntity<?> targetEntity;
private final Predicate<RelationalPersistentProperty> translationFilter;
EntityPropertyTranslator(RelationalPersistentEntity<?> targetEntity,
Predicate<RelationalPersistentProperty> 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;
}
}
}

View File

@@ -35,6 +35,14 @@ public interface RelationalPersistentProperty extends PersistentProperty<Relatio
*/
SqlIdentifier getColumnName();
/**
* Returns whether the property uses an annotated field name through {@link Column}.
*
* @return
* @since 3.2
*/
boolean hasExplicitColumnName();
@Override
RelationalPersistentEntity<?> getOwner();

View File

@@ -24,6 +24,7 @@ import java.util.function.BiFunction;
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 class RowDocument implements Map<String, Object> {
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 class RowDocument implements Map<String, Object> {
@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 class RowDocument implements Map<String, Object> {
* @param value
* @return
*/
public RowDocument append(String key, Object value) {
public RowDocument append(String key, @Nullable Object value) {
put(key, value);
return this;

View File

@@ -17,18 +17,25 @@ package org.springframework.data.relational.core.conversion;
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 @@ class MappingRelationalConverterUnitTests {
assertThat(result.money.currency).isEqualTo("USD");
}
@Test // GH-1554
void projectShouldReadNestedProjection() {
RowDocument source = RowDocument.of("addresses", Collections.singletonList(RowDocument.of("s", "hwy")));
EntityProjection<WithNestedProjection, Person> 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<ProjectionWithNestedEntity, Person> 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 @@ class MappingRelationalConverterUnitTests {
@Embedded.Nullable(prefix = "simple_") SimpleType simple;
}
static class Person {
@Id String id;
Date birthDate;
@Column("foo") String firstname;
String lastname;
Set<Address> addresses;
Person() {
}
@PersistenceCreator
public Person(Set<Address> 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<AddressProjection> getAddresses();
}
interface ProjectionWithNestedEntity {
Set<Address> getAddresses();
}
interface AddressProjection {
String getStreet();
}
}