Browse Source

Add support for properties using deep map-in-map/list-in-map nesting.

Original Pull Request: #2420
pull/2524/head
Mark Paluch 4 years ago committed by Christoph Strobl
parent
commit
a63774e6ec
No known key found for this signature in database
GPG Key ID: 8CC1AB53391458C8
  1. 100
      src/main/java/org/springframework/data/mapping/context/EntityProjection.java
  2. 64
      src/main/java/org/springframework/data/mapping/context/EntityProjectionIntrospector.java
  3. 41
      src/test/java/org/springframework/data/mapping/context/EntityProjectionIntrospectorUnitTests.java

100
src/main/java/org/springframework/data/mapping/context/EntityProjection.java

@ -15,12 +15,16 @@ @@ -15,12 +15,16 @@
*/
package org.springframework.data.mapping.context;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.Streamable;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
@ -32,7 +36,7 @@ import org.springframework.lang.Nullable; @@ -32,7 +36,7 @@ import org.springframework.lang.Nullable;
* @param <D> the domain type.
* @since 2.7
*/
public class EntityProjection<M, D> {
public class EntityProjection<M, D> implements Streamable<EntityProjection.PropertyProjection<?, ?>> {
private final TypeInformation<M> mappedType;
private final TypeInformation<D> domainType;
@ -86,6 +90,34 @@ public class EntityProjection<M, D> { @@ -86,6 +90,34 @@ public class EntityProjection<M, D> {
return new EntityProjection<>(typeInformation, typeInformation, Collections.emptyList(), false, false);
}
/**
* Performs the given action for each element of the {@link Streamable} recursively until all elements of the graph
* have been processed or the action throws an exception. Unless otherwise specified by the implementing class,
* actions are performed in the order of iteration (if an iteration order is specified). Exceptions thrown by the
* action are relayed to the caller.
*
* @param action
*/
public void forEachRecursive(Consumer<? super PropertyProjection<?, ?>> action) {
for (PropertyProjection<?, ?> descriptor : properties) {
if (descriptor instanceof ContainerPropertyProjection) {
action.accept(descriptor);
descriptor.forEachRecursive(action);
} else if (descriptor.getProperties().isEmpty()) {
action.accept(descriptor);
} else {
descriptor.forEachRecursive(action);
}
}
}
@Override
public Iterator<PropertyProjection<?, ?>> iterator() {
return properties.iterator();
}
/**
* @return the mapped type used by this type view.
*/
@ -134,24 +166,6 @@ public class EntityProjection<M, D> { @@ -134,24 +166,6 @@ public class EntityProjection<M, D> {
return properties;
}
/**
* Perform the given {@code action} for each element of the {@code ReturnedTypeDescriptor} until all elements have
* been processed or the action throws an exception.
*
* @param action the action to be performed for each element
*/
public void forEach(Consumer<PropertyPath> action) {
for (PropertyProjection<?, ?> descriptor : properties) {
if (descriptor.getProperties().isEmpty()) {
action.accept(descriptor.getPropertyPath());
} else {
descriptor.forEach(action);
}
}
}
/**
* Return a {@link EntityProjection} for a property identified by {@code name}.
*
@ -236,5 +250,53 @@ public class EntityProjection<M, D> { @@ -236,5 +250,53 @@ public class EntityProjection<M, D> {
public String toString() {
return String.format("%s AS %s", propertyPath.toDotPath(), getActualMappedType().getType().getName());
}
}
/**
* Descriptor for a property-level type along its potential projection that is held within a {@link Collection}-like
* or {@link Map}-like container. Property paths within containers use the deeply unwrapped actual type of the
* container as root type and as they cannot be tied immediately to the root entity.
*
* @param <M> the mapped type acting as view onto the domain type.
* @param <D> the domain type.
*/
public static class ContainerPropertyProjection<M, D> extends PropertyProjection<M, D> {
ContainerPropertyProjection(PropertyPath propertyPath, TypeInformation<M> mappedType, TypeInformation<D> domainType,
List<PropertyProjection<?, ?>> properties, boolean projecting, boolean closedProjection) {
super(propertyPath, mappedType, domainType, properties, projecting, closedProjection);
}
/**
* Create a projecting variant of a mapped type.
*
* @param propertyPath
* @param mappedType
* @param domainType
* @param properties
* @return
*/
public static <M, D> ContainerPropertyProjection<M, D> projecting(PropertyPath propertyPath,
TypeInformation<M> mappedType, TypeInformation<D> domainType, List<PropertyProjection<?, ?>> properties,
boolean closedProjection) {
return new ContainerPropertyProjection<>(propertyPath, mappedType, domainType, properties, true,
closedProjection);
}
/**
* Create a non-projecting variant of a mapped type.
*
* @param propertyPath
* @param mappedType
* @param domainType
* @return
*/
public static <M, D> ContainerPropertyProjection<M, D> nonProjecting(PropertyPath propertyPath,
TypeInformation<M> mappedType, TypeInformation<D> domainType) {
return new ContainerPropertyProjection<>(propertyPath, mappedType, domainType, Collections.emptyList(), false,
false);
}
}
}

64
src/main/java/org/springframework/data/mapping/context/EntityProjectionIntrospector.java

@ -78,10 +78,14 @@ public class EntityProjectionIntrospector { @@ -78,10 +78,14 @@ public class EntityProjectionIntrospector {
* <p>
* Nested properties (direct types, within maps, collections) are introspected for nested projections and contain
* property paths for closed projections.
* <p>
* Deeply nested types (e.g. {@code Map&lt;?, List&lt;Person&gt;&gt;}) are represented with a property path that uses
* the unwrapped type and no longer the root domain type {@code D}.
*
* @param mappedType
* @param domainType
* @return
* @param mappedType must not be {@literal null}.
* @param domainType must not be {@literal null}.
* @return the introspection result.
* @see org.springframework.data.mapping.context.EntityProjection.ContainerPropertyProjection
*/
public <M, D> EntityProjection<M, D> introspect(Class<M> mappedType, Class<D> domainType) {
@ -103,8 +107,7 @@ public class EntityProjectionIntrospector { @@ -103,8 +107,7 @@ public class EntityProjectionIntrospector {
PersistentEntity<?, ?> persistentEntity = mappingContext.getRequiredPersistentEntity(domainType);
List<EntityProjection.PropertyProjection<?, ?>> propertyDescriptors = getProperties(null, projectionInformation,
returnedTypeInformation,
persistentEntity, null);
returnedTypeInformation, persistentEntity, null);
return EntityProjection.projecting(returnedTypeInformation, domainTypeInformation, propertyDescriptors, true);
}
@ -127,44 +130,71 @@ public class EntityProjectionIntrospector { @@ -127,44 +130,71 @@ public class EntityProjectionIntrospector {
CycleGuard cycleGuardToUse = cycleGuard != null ? cycleGuard : new CycleGuard();
TypeInformation<?> property = projectionTypeInformation.getRequiredProperty(inputProperty.getName());
TypeInformation<?> actualType = property.getRequiredActualType();
boolean container = isContainer(actualType);
PropertyPath nestedPropertyPath = propertyPath == null
? PropertyPath.from(persistentProperty.getName(), persistentEntity.getTypeInformation())
: propertyPath.nested(persistentProperty.getName());
TypeInformation<?> returnedType = property.getRequiredActualType();
TypeInformation<?> domainType = persistentProperty.getTypeInformation().getRequiredActualType();
TypeInformation<?> unwrappedReturnedType = unwrapContainerType(property.getRequiredActualType());
TypeInformation<?> unwrappedDomainType = unwrapContainerType(
persistentProperty.getTypeInformation().getRequiredActualType());
if (isProjection(returnedType, domainType)) {
if (isProjection(unwrappedReturnedType, unwrappedDomainType)) {
List<EntityProjection.PropertyProjection<?, ?>> nestedPropertyDescriptors;
if (cycleGuardToUse.isCycleFree(persistentProperty)) {
nestedPropertyDescriptors = getProjectedProperties(nestedPropertyPath, returnedType, domainType,
cycleGuardToUse);
nestedPropertyDescriptors = getProjectedProperties(container ? null : nestedPropertyPath,
unwrappedReturnedType, unwrappedDomainType, cycleGuardToUse);
} else {
nestedPropertyDescriptors = Collections.emptyList();
}
propertyDescriptors.add(EntityProjection.PropertyProjection.projecting(nestedPropertyPath, property,
persistentProperty.getTypeInformation(),
nestedPropertyDescriptors, projectionInformation.isClosed()));
if (container) {
propertyDescriptors.add(EntityProjection.ContainerPropertyProjection.projecting(nestedPropertyPath, property,
persistentProperty.getTypeInformation(), nestedPropertyDescriptors, projectionInformation.isClosed()));
} else {
propertyDescriptors.add(EntityProjection.PropertyProjection.projecting(nestedPropertyPath, property,
persistentProperty.getTypeInformation(), nestedPropertyDescriptors, projectionInformation.isClosed()));
}
} else {
propertyDescriptors
.add(EntityProjection.PropertyProjection.nonProjecting(nestedPropertyPath, property,
persistentProperty.getTypeInformation()));
if (container) {
propertyDescriptors.add(EntityProjection.ContainerPropertyProjection.nonProjecting(nestedPropertyPath,
property, persistentProperty.getTypeInformation()));
} else {
propertyDescriptors.add(EntityProjection.PropertyProjection.nonProjecting(nestedPropertyPath, property,
persistentProperty.getTypeInformation()));
}
}
}
return propertyDescriptors;
}
private static TypeInformation<?> unwrapContainerType(TypeInformation<?> type) {
TypeInformation<?> unwrapped = type;
while (isContainer(unwrapped)) {
unwrapped = unwrapped.getRequiredActualType();
}
return unwrapped;
}
private static boolean isContainer(TypeInformation<?> actualType) {
return actualType.isCollectionLike() || actualType.isMap();
}
private boolean isProjection(TypeInformation<?> returnedType, TypeInformation<?> domainType) {
return projectionPredicate.test(returnedType.getRequiredActualType().getType(),
domainType.getRequiredActualType().getType());
}
private List<EntityProjection.PropertyProjection<?, ?>> getProjectedProperties(PropertyPath propertyPath,
private List<EntityProjection.PropertyProjection<?, ?>> getProjectedProperties(@Nullable PropertyPath propertyPath,
TypeInformation<?> returnedType, TypeInformation<?> domainType, CycleGuard cycleGuard) {
ProjectionInformation projectionInformation = projectionFactory.getProjectionInformation(returnedType.getType());

41
src/test/java/org/springframework/data/mapping/context/EntityProjectionIntrospectorUnitTests.java

@ -17,10 +17,12 @@ package org.springframework.data.mapping.context; @@ -17,10 +17,12 @@ package org.springframework.data.mapping.context;
import static org.assertj.core.api.Assertions.*;
import lombok.Getter;
import lombok.Value;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
@ -62,7 +64,7 @@ class EntityProjectionIntrospectorUnitTests { @@ -62,7 +64,7 @@ class EntityProjectionIntrospectorUnitTests {
assertThat(descriptor.isProjection()).isTrue();
List<PropertyPath> paths = new ArrayList<>();
descriptor.forEach(paths::add);
descriptor.forEachRecursive(it -> paths.add(it.getPropertyPath()));
assertThat(paths).hasSize(2).extracting(PropertyPath::toDotPath).containsOnly("id", "value");
}
@ -75,7 +77,7 @@ class EntityProjectionIntrospectorUnitTests { @@ -75,7 +77,7 @@ class EntityProjectionIntrospectorUnitTests {
assertThat(descriptor.isProjection()).isTrue();
List<PropertyPath> paths = new ArrayList<>();
descriptor.forEach(paths::add);
descriptor.forEachRecursive(it -> paths.add(it.getPropertyPath()));
assertThat(paths).hasSize(2).extracting(PropertyPath::toDotPath).containsOnly("id", "value");
}
@ -89,7 +91,7 @@ class EntityProjectionIntrospectorUnitTests { @@ -89,7 +91,7 @@ class EntityProjectionIntrospectorUnitTests {
assertThat(descriptor.isProjection()).isTrue();
List<PropertyPath> paths = new ArrayList<>();
descriptor.forEach(paths::add);
descriptor.forEachRecursive(it -> paths.add(it.getPropertyPath()));
assertThat(paths).hasSize(3).extracting(PropertyPath::toDotPath).containsOnly("domain.id", "domain.value",
"domain2");
@ -103,7 +105,7 @@ class EntityProjectionIntrospectorUnitTests { @@ -103,7 +105,7 @@ class EntityProjectionIntrospectorUnitTests {
assertThat(descriptor.isProjection()).isTrue();
List<PropertyPath> paths = new ArrayList<>();
descriptor.forEach(paths::add);
descriptor.forEachRecursive(it -> paths.add(it.getPropertyPath()));
assertThat(paths).isEmpty();
}
@ -116,7 +118,7 @@ class EntityProjectionIntrospectorUnitTests { @@ -116,7 +118,7 @@ class EntityProjectionIntrospectorUnitTests {
assertThat(descriptor.isProjection()).isTrue();
List<PropertyPath> paths = new ArrayList<>();
descriptor.forEach(paths::add);
descriptor.forEachRecursive(it -> paths.add(it.getPropertyPath()));
// cycles are tracked on a per-property root basis. Global tracking would not expand "secondaryAddress" into its
// components.
@ -133,11 +135,27 @@ class EntityProjectionIntrospectorUnitTests { @@ -133,11 +135,27 @@ class EntityProjectionIntrospectorUnitTests {
assertThat(descriptor.isProjection()).isTrue();
List<PropertyPath> paths = new ArrayList<>();
descriptor.forEach(paths::add);
descriptor.forEachRecursive(it -> paths.add(it.getPropertyPath()));
assertThat(paths).hasSize(2).extracting(PropertyPath::toDotPath).containsOnly("domains.id", "domains.value");
}
@Test // GH-2420
void considersPropertiesWithinContainers() {
EntityProjection<?, ?> descriptor = discoverer.introspect(WithMapOfCollectionProjection.class,
WithMapOfCollection.class);
assertThat(descriptor.isProjection()).isTrue();
List<PropertyPath> paths = new ArrayList<>();
descriptor.forEachRecursive(it -> {
paths.add(it.getPropertyPath());
});
assertThat(paths).hasSize(3).extracting(PropertyPath::toDotPath).containsOnly("domains", "id", "value");
}
interface SuperInterface {
}
@ -159,6 +177,17 @@ class EntityProjectionIntrospectorUnitTests { @@ -159,6 +177,17 @@ class EntityProjectionIntrospectorUnitTests {
List<DomainClass> domains;
}
static class WithMapOfCollection {
Map<String, List<DomainClass>> domains;
}
@Getter
static class WithMapOfCollectionProjection {
Map<String, List<DomainClassProjection>> domains;
}
interface WithCollectionProjection {
List<DomainClassProjection> getDomains();

Loading…
Cancel
Save