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:
committed by
Jens Schauder
parent
9c36eb970b
commit
41b37a6bd8
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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}.
|
||||
*
|
||||
|
||||
@@ -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 + ']';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user