Skip to content

Provide saveAs overloads with a predicate for selecting projected attributes. #2454

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-neo4j</artifactId>
<version>6.3.0-SNAPSHOT</version>
<version>6.3.0-GH2420-SNAPSHOT</version>

<name>Spring Data Neo4j</name>
<description>Next generation Object-Graph-Mapping for Spring Data.</description>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.BiPredicate;

import org.apiguardian.api.API;
import org.neo4j.cypherdsl.core.Statement;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty;
import org.springframework.data.neo4j.repository.NoResultException;
import org.springframework.data.neo4j.repository.query.QueryFragmentsAndParameters;
Expand Down Expand Up @@ -191,6 +193,24 @@ public interface Neo4jOperations {
*/
<T> T save(T instance);

/**
* Saves an instance of an entity, using the provided predicate to shape the stored graph. One can think of the predicate
* as a dynamic projection. If you want to save or update properties of associations (aka related nodes), you must include
* the association property as well (meaning the predicate must return {@literal true} for that property, too).
* <p>
* Be careful when reusing the returned instance for further persistence operations, as it will most likely not be
* fully hydrated and without using a static or dynamic projection, you will most likely cause data loss.
*
* @param instance the entity to be saved. Must not be {@code null}.
* @param includeProperty A predicate to determine the properties to save.
* @param <T> the type of the entity.
* @return the saved instance.
* @since 6.3
*/
default <T> T saveAs(T instance, BiPredicate<PropertyPath, Neo4jPersistentProperty> includeProperty) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we accept it, the instance should be @Nullable. Would also make sense for the saveAllAs method and the reactive counter-parts. (Would need an additional null check in the saveAllAs/saveAllImpl).

throw new UnsupportedOperationException();
}

/**
* Saves an instance of an entity, including the properties and relationship defined by the projected {@code resultType}.
*
Expand All @@ -213,6 +233,24 @@ default <T, R> R saveAs(T instance, Class<R> resultType) {
*/
<T> List<T> saveAll(Iterable<T> instances);

/**
* Saves several instances of an entity, using the provided predicate to shape the stored graph. One can think of the predicate
* as a dynamic projection. If you want to save or update properties of associations (aka related nodes), you must include
* the association property as well (meaning the predicate must return {@literal true} for that property, too).
* <p>
* Be careful when reusing the returned instances for further persistence operations, as they will most likely not be
* fully hydrated and without using a static or dynamic projection, you will most likely cause data loss.
*
* @param instances the instances to be saved. Must not be {@code null}.
* @param includeProperty A predicate to determine the properties to save.
* @param <T> the type of the entity.
* @return the saved instances.
* @since 6.3
*/
default <T> List<T> saveAllAs(Iterable<T> instances, BiPredicate<PropertyPath, Neo4jPersistentProperty> includeProperty) {
throw new UnsupportedOperationException();
}

/**
* Saves an instance of an entity, including the properties and relationship defined by the project {@code resultType}.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import java.util.Optional;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
Expand Down Expand Up @@ -343,6 +344,16 @@ public <T> T save(T instance) {
return saveImpl(instance, Collections.emptyMap(), null);
}

@Override
public <T> T saveAs(T instance, BiPredicate<PropertyPath, Neo4jPersistentProperty> includeProperty) {

if (instance == null) {
return null;
}

return saveImpl(instance, TemplateSupport.computeIncludedPropertiesFromPredicate(this.neo4jMappingContext, instance.getClass(), includeProperty), null);
}

@Override
public <T, R> R saveAs(T instance, Class<R> resultType) {

Expand Down Expand Up @@ -451,10 +462,10 @@ private <T> DynamicLabels determineDynamicLabels(T entityToBeSaved, Neo4jPersist

@Override
public <T> List<T> saveAll(Iterable<T> instances) {
return saveAllImpl(instances, Collections.emptyMap());
return saveAllImpl(instances, Collections.emptyMap(), null);
}

private <T> List<T> saveAllImpl(Iterable<T> instances, Map<PropertyPath, Boolean> includedProperties) {
private <T> List<T> saveAllImpl(Iterable<T> instances, @Nullable Map<PropertyPath, Boolean> includedProperties, @Nullable BiPredicate<PropertyPath, Neo4jPersistentProperty> includeProperty) {

Set<Class<?>> types = new HashSet<>();
List<T> entities = new ArrayList<>();
Expand All @@ -469,13 +480,19 @@ private <T> List<T> saveAllImpl(Iterable<T> instances, Map<PropertyPath, Boolean

boolean heterogeneousCollection = types.size() > 1;
Class<?> domainClass = types.iterator().next();

Map<PropertyPath, Boolean> pps = includeProperty == null ?
includedProperties :
TemplateSupport.computeIncludedPropertiesFromPredicate(this.neo4jMappingContext, domainClass,
includeProperty);

Neo4jPersistentEntity<?> entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainClass);
if (heterogeneousCollection || entityMetaData.isUsingInternalIds() || entityMetaData.hasVersionProperty()
|| entityMetaData.getDynamicLabelsProperty().isPresent()) {
log.debug("Saving entities using single statements.");

NestedRelationshipProcessingStateMachine stateMachine = new NestedRelationshipProcessingStateMachine(neo4jMappingContext);
return entities.stream().map(e -> saveImpl(e, includedProperties, stateMachine)).collect(Collectors.toList());
return entities.stream().map(e -> saveImpl(e, pps, stateMachine)).collect(Collectors.toList());
}

class Tuple3<T> {
Expand All @@ -497,7 +514,7 @@ class Tuple3<T> {
// Save roots
@SuppressWarnings("unchecked") // We can safely assume here that we have a humongous collection with only one single type being either T or extending it
Function<T, Map<String, Object>> binderFunction = neo4jMappingContext.getRequiredBinderFunctionFor((Class<T>) domainClass);
binderFunction = TemplateSupport.createAndApplyPropertyFilter(includedProperties, entityMetaData, binderFunction);
binderFunction = TemplateSupport.createAndApplyPropertyFilter(pps, entityMetaData, binderFunction);
List<Map<String, Object>> entityList = entitiesToBeSaved.stream().map(h -> h.modifiedInstance).map(binderFunction)
.collect(Collectors.toList());
Map<Value, Long> idToInternalIdMapping = neo4jClient
Expand All @@ -515,10 +532,16 @@ class Tuple3<T> {
Neo4jPersistentProperty idProperty = entityMetaData.getRequiredIdProperty();
Object id = convertIdValues(idProperty, propertyAccessor.getProperty(idProperty));
Long internalId = idToInternalIdMapping.get(id);
return this.<T>processRelations(entityMetaData, propertyAccessor, t.wasNew, new NestedRelationshipProcessingStateMachine(neo4jMappingContext, t.originalInstance, internalId), TemplateSupport.computeIncludePropertyPredicate(includedProperties, entityMetaData));
return this.<T>processRelations(entityMetaData, propertyAccessor, t.wasNew, new NestedRelationshipProcessingStateMachine(neo4jMappingContext, t.originalInstance, internalId), TemplateSupport.computeIncludePropertyPredicate(pps, entityMetaData));
}).collect(Collectors.toList());
}

@Override
public <T> List<T> saveAllAs(Iterable<T> instances, BiPredicate<PropertyPath, Neo4jPersistentProperty> includeProperty) {

return saveAllImpl(instances, null, includeProperty);
}

@Override
public <T, R> List<R> saveAllAs(Iterable<T> instances, Class<R> resultType) {

Expand All @@ -537,7 +560,7 @@ public <T, R> List<R> saveAllAs(Iterable<T> instances, Class<R> resultType) {
Map<PropertyPath, Boolean> pps = PropertyFilterSupport.addPropertiesFrom(commonElementType, resultType,
projectionFactory, neo4jMappingContext);

List<T> savedInstances = saveAllImpl(instances, pps);
List<T> savedInstances = saveAllImpl(instances, pps, null);

if (projectionInformation.isClosed()) {
return savedInstances.stream().map(instance -> projectionFactory.createProjection(resultType, instance))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@
*/
package org.springframework.data.neo4j.core;

import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty;
import org.springframework.data.neo4j.repository.query.QueryFragmentsAndParameters;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.util.Map;
import java.util.function.BiPredicate;

import org.apiguardian.api.API;
import org.neo4j.cypherdsl.core.Statement;
Expand Down Expand Up @@ -190,6 +192,24 @@ public interface ReactiveNeo4jOperations {
*/
<T> Mono<T> save(T instance);

/**
* Saves an instance of an entity, using the provided predicate to shape the stored graph. One can think of the predicate
* as a dynamic projection. If you want to save or update properties of associations (aka related nodes), you must include
* the association property as well (meaning the predicate must return {@literal true} for that property, too).
* <p>
* Be careful when reusing the returned instance for further persistence operations, as it will most likely not be
* fully hydrated and without using a static or dynamic projection, you will most likely cause data loss.
*
* @param instance the entity to be saved. Must not be {@code null}.
* @param includeProperty A predicate to determine the properties to save.
* @param <T> the type of the entity.
* @return the saved instance.
* @since 6.3
*/
default <T> Mono<T> saveAs(T instance, BiPredicate<PropertyPath, Neo4jPersistentProperty> includeProperty) {
throw new UnsupportedOperationException();
}

/**
* Saves an instance of an entity, including the properties and relationship defined by the projected {@code resultType}.
*
Expand All @@ -212,6 +232,24 @@ default <T, R> Mono<R> saveAs(T instance, Class<R> resultType) {
*/
<T> Flux<T> saveAll(Iterable<T> instances);

/**
* Saves several instances of an entity, using the provided predicate to shape the stored graph. One can think of the predicate
* as a dynamic projection. If you want to save or update properties of associations (aka related nodes), you must include
* the association property as well (meaning the predicate must return {@literal true} for that property, too).
* <p>
* Be careful when reusing the returned instances for further persistence operations, as they will most likely not be
* fully hydrated and without using a static or dynamic projection, you will most likely cause data loss.
*
* @param instances the instances to be saved. Must not be {@code null}.
* @param includeProperty A predicate to determine the properties to save.
* @param <T> the type of the entity.
* @return the saved instances.
* @since 6.3
*/
default <T> Flux<T> saveAllAs(Iterable<T> instances, BiPredicate<PropertyPath, Neo4jPersistentProperty> includeProperty) {
throw new UnsupportedOperationException();
}

/**
* Saves several instances of an entity, including the properties and relationship defined by the project {@code resultType}.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiFunction;
import java.util.function.BiPredicate;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -327,6 +328,16 @@ public <T> Mono<T> save(T instance) {
return saveImpl(instance, Collections.emptyMap(), null);
}

@Override
public <T> Mono<T> saveAs(T instance, BiPredicate<PropertyPath, Neo4jPersistentProperty> includeProperty) {

if (instance == null) {
return null;
}

return saveImpl(instance, TemplateSupport.computeIncludedPropertiesFromPredicate(this.neo4jMappingContext, instance.getClass(), includeProperty), null);
}

@Override
public <T, R> Mono<R> saveAs(T instance, Class<R> resultType) {

Expand Down Expand Up @@ -463,7 +474,13 @@ private <T> Mono<Tuple2<T, DynamicLabels>> determineDynamicLabels(T entityToBeSa

@Override
public <T> Flux<T> saveAll(Iterable<T> instances) {
return saveAllImpl(instances, Collections.emptyMap());
return saveAllImpl(instances, Collections.emptyMap(), null);
}

@Override
public <T> Flux<T> saveAllAs(Iterable<T> instances, BiPredicate<PropertyPath, Neo4jPersistentProperty> includeProperty) {

return saveAllImpl(instances, null, includeProperty);
}

@Override
Expand All @@ -481,7 +498,7 @@ public <T, R> Flux<R> saveAllAs(Iterable<T> instances, Class<R> resultType) {
Map<PropertyPath, Boolean> pps = PropertyFilterSupport.addPropertiesFrom(commonElementType, resultType,
projectionFactory, neo4jMappingContext);

Flux<T> savedInstances = saveAllImpl(instances, pps);
Flux<T> savedInstances = saveAllImpl(instances, pps, null);
if (projectionInformation.isClosed()) {
return savedInstances.map(instance -> projectionFactory.createProjection(resultType, instance));
}
Expand All @@ -495,7 +512,7 @@ public <T, R> Flux<R> saveAllAs(Iterable<T> instances, Class<R> resultType) {
}).map(instance -> projectionFactory.createProjection(resultType, instance));
}

private <T> Flux<T> saveAllImpl(Iterable<T> instances, @Nullable Map<PropertyPath, Boolean> includedProperties) {
private <T> Flux<T> saveAllImpl(Iterable<T> instances, @Nullable Map<PropertyPath, Boolean> includedProperties, @Nullable BiPredicate<PropertyPath, Neo4jPersistentProperty> includeProperty) {

Set<Class<?>> types = new HashSet<>();
List<T> entities = new ArrayList<>();
Expand All @@ -510,18 +527,24 @@ private <T> Flux<T> saveAllImpl(Iterable<T> instances, @Nullable Map<PropertyPat

boolean heterogeneousCollection = types.size() > 1;
Class<?> domainClass = types.iterator().next();

Map<PropertyPath, Boolean> pps = includeProperty == null ?
includedProperties :
TemplateSupport.computeIncludedPropertiesFromPredicate(this.neo4jMappingContext, domainClass,
includeProperty);

Neo4jPersistentEntity<?> entityMetaData = neo4jMappingContext.getRequiredPersistentEntity(domainClass);
if (heterogeneousCollection || entityMetaData.isUsingInternalIds() || entityMetaData.hasVersionProperty()
|| entityMetaData.getDynamicLabelsProperty().isPresent()) {
log.debug("Saving entities using single statements.");

NestedRelationshipProcessingStateMachine stateMachine = new NestedRelationshipProcessingStateMachine(neo4jMappingContext);
return Flux.fromIterable(entities).concatMap(e -> this.saveImpl(e, includedProperties, stateMachine));
return Flux.fromIterable(entities).concatMap(e -> this.saveImpl(e, pps, stateMachine));
}

@SuppressWarnings("unchecked") // We can safely assume here that we have a humongous collection with only one single type being either T or extending it
Function<T, Map<String, Object>> binderFunction = TemplateSupport.createAndApplyPropertyFilter(
includedProperties, entityMetaData,
pps, entityMetaData,
neo4jMappingContext.getRequiredBinderFunctionFor((Class<T>) domainClass));
return Flux.fromIterable(entities)
// Map all entities into a tuple <Original, OriginalWasNew>
Expand Down Expand Up @@ -550,7 +573,7 @@ private <T> Flux<T> saveAllImpl(Iterable<T> instances, @Nullable Map<PropertyPat
Object id = convertIdValues(idProperty, propertyAccessor.getProperty(idProperty));
Long internalId = idToInternalIdMapping.get(id);
return processRelations(entityMetaData, propertyAccessor, t.getT2(), new NestedRelationshipProcessingStateMachine(neo4jMappingContext, t.getT1(), internalId),
TemplateSupport.computeIncludePropertyPredicate(includedProperties, entityMetaData));
TemplateSupport.computeIncludePropertyPredicate(pps, entityMetaData));
}))
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.util.Map;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.BiPredicate;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
Expand All @@ -45,8 +46,10 @@
import org.springframework.data.neo4j.core.mapping.EntityInstanceWithSource;
import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext;
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentEntity;
import org.springframework.data.neo4j.core.mapping.Neo4jPersistentProperty;
import org.springframework.data.neo4j.core.mapping.NodeDescription;
import org.springframework.data.neo4j.core.mapping.PropertyFilter;
import org.springframework.data.neo4j.core.mapping.PropertyTraverser;
import org.springframework.data.neo4j.repository.query.QueryFragments;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
Expand Down Expand Up @@ -272,6 +275,27 @@ static <T> FilteredBinderFunction<T> createAndApplyPropertyFilter(
}));
}

/**
* Helper function that computes the map of included properties for a dynamic projection as expected in 6.2, but
* for fully dynamic projection
*
* @param mappingContext The context to work on
* @param domainType The projected domain type
* @param predicate The predicate to compute the included columns
* @param <T> Type of the domain type
* @return A map as expected by the property filter.
*/
static <T> Map<PropertyPath, Boolean> computeIncludedPropertiesFromPredicate(Neo4jMappingContext mappingContext,
Class<T> domainType, @Nullable BiPredicate<PropertyPath, Neo4jPersistentProperty> predicate) {
if (predicate == null) {
return Collections.emptyMap();
}
Map<PropertyPath, Boolean> pps = new HashMap<>();
PropertyTraverser traverser = new PropertyTraverser(mappingContext);
traverser.traverse(domainType, predicate, (path, property) -> pps.put(path, false));
return Collections.unmodifiableMap(pps);
}

/**
* A wrapper around a {@link Function} from entity to {@link Map} which is filtered the {@link PropertyFilter} included as well.
*
Expand Down
Loading