From 85e7e808f218ec9ff1cfbea78601e14b9388ff9b Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Wed, 11 Nov 2020 19:00:54 +0100 Subject: [PATCH] DATAGRAPH-1429 - Allow the use of one top-level aggregate. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We have everything in place to call our entity reader several times. The biggest change here is to pass in the map accessor from the outside, not the record, which I should have done when I changed the entity reader to work on the map accessor in the first place. The prepared query now has a mapping function that knows if it had to unwrap a list before mapping and aggregating it back afterwards. If a list had been unwrapped, then allResults, both the imperative and reactive ones, will react accordingly. This change allows the flight scenario and a couple of other things to work. Custom maps can be returned as well, by using DTO based projections with additional properties (returning them as `[{node: n, p1: "p1", p1: "pn“}]` and than applying a custom streamable. --- .../data/neo4j/core/Neo4jTemplate.java | 48 ++++++++++------ .../data/neo4j/core/PreparedQuery.java | 41 +++++++++++++- .../neo4j/core/ReactiveNeo4jTemplate.java | 39 ++++++------- .../core/{mapping => }/RecordMapAccessor.java | 2 +- .../data/neo4j/core/mapping/Schema.java | 6 +- .../repository/query/AbstractNeo4jQuery.java | 6 +- .../query/AbstractReactiveNeo4jQuery.java | 4 +- .../query/DtoInstantiatingConverter.java | 4 +- .../query/EntityInstanceWithSource.java | 8 +-- .../repository/query/Neo4jQuerySupport.java | 8 +-- .../repository/query/PartTreeNeo4jQuery.java | 4 +- .../query/ReactivePartTreeNeo4jQuery.java | 4 +- .../query/ReactiveStringBasedNeo4jQuery.java | 4 +- .../query/StringBasedNeo4jQuery.java | 4 +- .../data/neo4j/core/Neo4jClientTest.java | 10 ++-- .../integration/imperative/RepositoryIT.java | 48 +++++++++++++++- .../repositories/PersonRepository.java | 55 +++++++++++++++++++ .../reactive/ReactiveRepositoryIT.java | 22 +++++++- .../ReactivePersonRepository.java | 3 + 19 files changed, 246 insertions(+), 74 deletions(-) rename src/main/java/org/springframework/data/neo4j/core/{mapping => }/RecordMapAccessor.java (97%) diff --git a/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java b/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java index c98f8b4a4..42ae4cdc4 100644 --- a/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java +++ b/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java @@ -45,18 +45,18 @@ import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.data.mapping.AssociationHandler; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.callback.EntityCallbacks; -import org.springframework.data.neo4j.core.mapping.MappingSupport; -import org.springframework.data.neo4j.core.mapping.NestedRelationshipContext; -import org.springframework.data.neo4j.core.mapping.NestedRelationshipProcessingStateMachine; -import org.springframework.data.neo4j.core.mapping.NestedRelationshipProcessingStateMachine.ProcessState; import org.springframework.data.neo4j.core.mapping.Constants; +import org.springframework.data.neo4j.core.mapping.CreateRelationshipStatementHolder; import org.springframework.data.neo4j.core.mapping.CypherGenerator; +import org.springframework.data.neo4j.core.mapping.MappingSupport; 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.NestedRelationshipContext; +import org.springframework.data.neo4j.core.mapping.NestedRelationshipProcessingStateMachine; +import org.springframework.data.neo4j.core.mapping.NestedRelationshipProcessingStateMachine.ProcessState; import org.springframework.data.neo4j.core.mapping.NodeDescription; import org.springframework.data.neo4j.core.mapping.RelationshipDescription; -import org.springframework.data.neo4j.core.mapping.CreateRelationshipStatementHolder; import org.springframework.data.neo4j.repository.NoResultException; import org.springframework.data.neo4j.repository.event.BeforeBindCallback; import org.springframework.data.util.ClassTypeInformation; @@ -118,7 +118,8 @@ public final class Neo4jTemplate implements Neo4jOperations, BeanFactoryAware { public long count(Class domainType) { Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getPersistentEntity(domainType); - Statement statement = cypherGenerator.prepareMatchOf(entityMetaData).returning(Functions.count(asterisk())).build(); + Statement statement = cypherGenerator.prepareMatchOf(entityMetaData).returning(Functions.count(asterisk())) + .build(); return count(statement); } @@ -189,7 +190,8 @@ public final class Neo4jTemplate implements Neo4jOperations, BeanFactoryAware { public Optional findById(Object id, Class domainType) { Neo4jPersistentEntity entityMetaData = neo4jMappingContext.getPersistentEntity(domainType); Statement statement = cypherGenerator - .prepareMatchOf(entityMetaData, entityMetaData.getIdExpression().isEqualTo(parameter(Constants.NAME_OF_ID))) + .prepareMatchOf(entityMetaData, + entityMetaData.getIdExpression().isEqualTo(parameter(Constants.NAME_OF_ID))) .returning(cypherGenerator.createReturnStatementForMatch(entityMetaData)).build(); return createExecutableQuery(domainType, statement, Collections .singletonMap(Constants.NAME_OF_ID, convertIdValues(entityMetaData.getRequiredIdProperty(), id))) @@ -211,7 +213,8 @@ public final class Neo4jTemplate implements Neo4jOperations, BeanFactoryAware { private Object convertIdValues(@Nullable Neo4jPersistentProperty idProperty, Object idValues) { return neo4jMappingContext.getConversionService().writeValue(idValues, - ClassTypeInformation.from(idValues.getClass()), idProperty == null ? null : idProperty.getOptionalWritingConverter()); + ClassTypeInformation.from(idValues.getClass()), + idProperty == null ? null : idProperty.getOptionalWritingConverter()); } @Override @@ -228,7 +231,8 @@ public final class Neo4jTemplate implements Neo4jOperations, BeanFactoryAware { DynamicLabels dynamicLabels = determineDynamicLabels(entityToBeSaved, entityMetaData, inDatabase); Optional optionalInternalId = neo4jClient - .query(() -> renderer.render(cypherGenerator.prepareSaveOf(entityMetaData, dynamicLabels))).in(inDatabase) + .query(() -> renderer.render(cypherGenerator.prepareSaveOf(entityMetaData, dynamicLabels))) + .in(inDatabase) .bind((T) entityToBeSaved) .with(neo4jMappingContext.getRequiredBinderFunctionFor((Class) entityToBeSaved.getClass())) .fetchAs(Long.class).one(); @@ -257,7 +261,8 @@ public final class Neo4jTemplate implements Neo4jOperations, BeanFactoryAware { Neo4jClient.RunnableSpecTightToDatabase runnableQuery = neo4jClient .query(() -> renderer.render(cypherGenerator.createStatementReturningDynamicLabels(entityMetaData))) .in(inDatabase).bind(propertyAccessor.getProperty(entityMetaData.getRequiredIdProperty())) - .to(Constants.NAME_OF_ID).bind(entityMetaData.getStaticLabels()).to(Constants.NAME_OF_STATIC_LABELS_PARAM); + .to(Constants.NAME_OF_ID).bind(entityMetaData.getStaticLabels()) + .to(Constants.NAME_OF_STATIC_LABELS_PARAM); if (entityMetaData.hasVersionProperty()) { runnableQuery = runnableQuery @@ -296,13 +301,16 @@ public final class Neo4jTemplate implements Neo4jOperations, BeanFactoryAware { return entities.stream().map(e -> saveImpl(e, databaseName)).collect(Collectors.toList()); } - List entitiesToBeSaved = entities.stream().map(eventSupport::maybeCallBeforeBind).collect(Collectors.toList()); + List entitiesToBeSaved = entities.stream().map(eventSupport::maybeCallBeforeBind) + .collect(Collectors.toList()); // Save roots Function> binderFunction = neo4jMappingContext.getRequiredBinderFunctionFor(domainClass); - List> entityList = entitiesToBeSaved.stream().map(binderFunction).collect(Collectors.toList()); + List> entityList = entitiesToBeSaved.stream().map(binderFunction) + .collect(Collectors.toList()); ResultSummary resultSummary = neo4jClient - .query(() -> renderer.render(cypherGenerator.prepareSaveOfMultipleInstancesOf(entityMetaData))).in(databaseName) + .query(() -> renderer.render(cypherGenerator.prepareSaveOfMultipleInstancesOf(entityMetaData))) + .in(databaseName) .bind(entityList).to(Constants.NAME_OF_ENTITY_LIST_PARAM).run(); // Save related @@ -385,7 +393,8 @@ public final class Neo4jTemplate implements Neo4jOperations, BeanFactoryAware { Assert.notNull(neo4jMappingContext.getPersistentEntity(domainType), "Cannot get or create persistent entity."); PreparedQuery preparedQuery = PreparedQuery.queryFor(domainType).withCypherQuery(cypherStatement) - .withParameters(parameters).usingMappingFunction(neo4jMappingContext.getRequiredMappingFunctionFor(domainType)) + .withParameters(parameters) + .usingMappingFunction(neo4jMappingContext.getRequiredMappingFunctionFor(domainType)) .build(); return toExecutableQuery(preparedQuery); } @@ -466,7 +475,8 @@ public final class Neo4jTemplate implements Neo4jOperations, BeanFactoryAware { if (targetNodeDescription.isUsingInternalIds()) { PersistentPropertyAccessor targetPropertyAccessor = targetNodeDescription .getPropertyAccessor(relatedNode); - targetPropertyAccessor.setProperty(targetNodeDescription.getRequiredIdProperty(), relatedInternalId); + targetPropertyAccessor + .setProperty(targetNodeDescription.getRequiredIdProperty(), relatedInternalId); } if (processState != ProcessState.PROCESSED_ALL_VALUES) { processNestedRelations(targetNodeDescription, relatedNode, inDatabase, stateMachine); @@ -524,8 +534,14 @@ public final class Neo4jTemplate implements Neo4jOperations, BeanFactoryAware { this.fetchSpec = fetchSpec; } + @SuppressWarnings("unchecked") public List getResults() { - return fetchSpec.all().stream().collect(Collectors.toList()); + + Collection all = fetchSpec.all(); + if (preparedQuery.resultsHaveBeenAggregated()) { + return all.stream().flatMap(nested -> ((Collection) nested).stream()).collect(Collectors.toList()); + } + return all.stream().collect(Collectors.toList()); } public Optional getSingleResult() { diff --git a/src/main/java/org/springframework/data/neo4j/core/PreparedQuery.java b/src/main/java/org/springframework/data/neo4j/core/PreparedQuery.java index b9994fd75..266d86b1d 100644 --- a/src/main/java/org/springframework/data/neo4j/core/PreparedQuery.java +++ b/src/main/java/org/springframework/data/neo4j/core/PreparedQuery.java @@ -19,10 +19,12 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiFunction; import org.apiguardian.api.API; import org.neo4j.driver.Record; +import org.neo4j.driver.types.MapAccessor; import org.neo4j.driver.types.TypeSystem; import org.springframework.lang.Nullable; @@ -53,7 +55,12 @@ public final class PreparedQuery { private PreparedQuery(OptionalBuildSteps optionalBuildSteps) { this.resultType = optionalBuildSteps.resultType; - this.mappingFunction = (BiFunction) optionalBuildSteps.mappingFunction; + if (optionalBuildSteps.mappingFunction == null) { + this.mappingFunction = null; + } else { + this.mappingFunction = (BiFunction) new ListAggregatingMappingFunction( + optionalBuildSteps.mappingFunction); + } this.cypherQuery = optionalBuildSteps.cypherQuery; this.parameters = optionalBuildSteps.parameters; } @@ -66,6 +73,10 @@ public final class PreparedQuery { return Optional.ofNullable(mappingFunction); } + boolean resultsHaveBeenAggregated() { + return this.mappingFunction != null && ((ListAggregatingMappingFunction) this.mappingFunction).hasAggregated(); + } + public String getCypherQuery() { return this.cypherQuery; } @@ -99,7 +110,7 @@ public final class PreparedQuery { final Class resultType; final String cypherQuery; Map parameters = Collections.emptyMap(); - @Nullable BiFunction mappingFunction; + @Nullable BiFunction mappingFunction; OptionalBuildSteps(Class resultType, String cypherQuery) { this.resultType = resultType; @@ -117,7 +128,8 @@ public final class PreparedQuery { return this; } - public OptionalBuildSteps usingMappingFunction(@Nullable BiFunction newMappingFunction) { + public OptionalBuildSteps usingMappingFunction( + @Nullable BiFunction newMappingFunction) { this.mappingFunction = newMappingFunction; return this; } @@ -126,4 +138,27 @@ public final class PreparedQuery { return new PreparedQuery<>(this); } } + + private static class ListAggregatingMappingFunction implements BiFunction { + + private final BiFunction target; + private final AtomicBoolean aggregated = new AtomicBoolean(false); + + ListAggregatingMappingFunction(BiFunction target) { + this.target = target; + } + + @Override + public Object apply(TypeSystem t, Record r) { + if (r.size() == 1 && r.get(0).hasType(t.LIST())) { + aggregated.compareAndSet(false, true); + return r.get(0).asList(v -> target.apply(t, v)); + } + return target.apply(t, new RecordMapAccessor(r)); + } + + boolean hasAggregated() { + return aggregated.get(); + } + } } diff --git a/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java b/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java index 39b73329b..f227a80a5 100644 --- a/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java +++ b/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java @@ -49,18 +49,18 @@ import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.data.mapping.AssociationHandler; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.callback.ReactiveEntityCallbacks; +import org.springframework.data.neo4j.core.mapping.Constants; +import org.springframework.data.neo4j.core.mapping.CreateRelationshipStatementHolder; +import org.springframework.data.neo4j.core.mapping.CypherGenerator; import org.springframework.data.neo4j.core.mapping.MappingSupport; -import org.springframework.data.neo4j.core.mapping.NestedRelationshipContext; -import org.springframework.data.neo4j.core.mapping.NestedRelationshipProcessingStateMachine; -import org.springframework.data.neo4j.core.mapping.NestedRelationshipProcessingStateMachine.ProcessState; 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.Constants; -import org.springframework.data.neo4j.core.mapping.CypherGenerator; +import org.springframework.data.neo4j.core.mapping.NestedRelationshipContext; +import org.springframework.data.neo4j.core.mapping.NestedRelationshipProcessingStateMachine; +import org.springframework.data.neo4j.core.mapping.NestedRelationshipProcessingStateMachine.ProcessState; import org.springframework.data.neo4j.core.mapping.NodeDescription; import org.springframework.data.neo4j.core.mapping.RelationshipDescription; -import org.springframework.data.neo4j.core.mapping.CreateRelationshipStatementHolder; import org.springframework.data.neo4j.repository.event.ReactiveBeforeBindCallback; import org.springframework.data.util.ClassTypeInformation; import org.springframework.lang.Nullable; @@ -301,16 +301,8 @@ public final class ReactiveNeo4jTemplate implements ReactiveNeo4jOperations, Bea Function> binderFunction = neo4jMappingContext.getRequiredBinderFunctionFor(domainClass); return getDatabaseName().flatMapMany(databaseName -> Flux.fromIterable(entities) - .flatMap(eventSupport::maybeCallBeforeBind).collectList().flatMapMany(entitiesToBeSaved -> Mono.defer(() -> { // Defer - // the - // actual - // save - // statement - // until - // the - // previous - // flux - // completes + .flatMap(eventSupport::maybeCallBeforeBind).collectList().flatMapMany(entitiesToBeSaved -> Mono.defer(() -> { + // Defer the actual save statement until the previous flux completes List> boundedEntityList = entitiesToBeSaved.stream().map(binderFunction) .collect(Collectors.toList()); @@ -525,7 +517,7 @@ public final class ReactiveNeo4jTemplate implements ReactiveNeo4jOperations, Bea ReactiveNeo4jClient.RecordFetchSpec fetchSpec = preparedQuery.getOptionalMappingFunction() .map(mappingFunction -> mappingSpec.mappedBy(mappingFunction)).orElse(mappingSpec); - return new DefaultReactiveExecutableQuery<>(fetchSpec); + return new DefaultReactiveExecutableQuery<>(preparedQuery, fetchSpec); }); } @@ -537,17 +529,26 @@ public final class ReactiveNeo4jTemplate implements ReactiveNeo4jOperations, Bea final class DefaultReactiveExecutableQuery implements ExecutableQuery { + private final PreparedQuery preparedQuery; private final ReactiveNeo4jClient.RecordFetchSpec fetchSpec; - DefaultReactiveExecutableQuery(ReactiveNeo4jClient.RecordFetchSpec fetchSpec) { + DefaultReactiveExecutableQuery(PreparedQuery preparedQuery, ReactiveNeo4jClient.RecordFetchSpec fetchSpec) { + this.preparedQuery = preparedQuery; this.fetchSpec = fetchSpec; } /** * @return All results returned by this query. */ + @SuppressWarnings("unchecked") public Flux getResults() { - return fetchSpec.all(); + + return fetchSpec.all().switchOnFirst((signal, f) -> { + if (preparedQuery.resultsHaveBeenAggregated()) { + return f.flatMap(nested -> Flux.fromIterable((Collection) nested)); + } + return f; + }); } /** diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/RecordMapAccessor.java b/src/main/java/org/springframework/data/neo4j/core/RecordMapAccessor.java similarity index 97% rename from src/main/java/org/springframework/data/neo4j/core/mapping/RecordMapAccessor.java rename to src/main/java/org/springframework/data/neo4j/core/RecordMapAccessor.java index 92402098b..fa68343be 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/RecordMapAccessor.java +++ b/src/main/java/org/springframework/data/neo4j/core/RecordMapAccessor.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.neo4j.core.mapping; +package org.springframework.data.neo4j.core; import java.util.Map; import java.util.function.Function; diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/Schema.java b/src/main/java/org/springframework/data/neo4j/core/mapping/Schema.java index 060e7850b..3c3e7def0 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/Schema.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/Schema.java @@ -22,7 +22,7 @@ import java.util.function.BiFunction; import java.util.function.Function; import org.apiguardian.api.API; -import org.neo4j.driver.Record; +import org.neo4j.driver.types.MapAccessor; import org.neo4j.driver.types.TypeSystem; import org.springframework.data.mapping.MappingException; import org.springframework.data.neo4j.core.schema.IdGenerator; @@ -87,12 +87,12 @@ public interface Schema { * @return The default, stateless and reusable mapping function for the given target class * @throws UnknownEntityException When {@code targetClass} is not a managed class */ - default BiFunction getRequiredMappingFunctionFor(Class targetClass) { + default BiFunction getRequiredMappingFunctionFor(Class targetClass) { NodeDescription nodeDescription = getNodeDescription(targetClass); if (nodeDescription == null) { throw new UnknownEntityException(targetClass); } - return (typeSystem, record) -> getEntityConverter().read(targetClass, new RecordMapAccessor(record)); + return (typeSystem, record) -> getEntityConverter().read(targetClass, record); } /** diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/AbstractNeo4jQuery.java b/src/main/java/org/springframework/data/neo4j/repository/query/AbstractNeo4jQuery.java index 874176370..860a55edc 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/AbstractNeo4jQuery.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/AbstractNeo4jQuery.java @@ -21,7 +21,7 @@ import java.util.Optional; import java.util.function.BiFunction; import java.util.function.LongSupplier; -import org.neo4j.driver.Record; +import org.neo4j.driver.types.MapAccessor; import org.neo4j.driver.types.TypeSystem; import org.springframework.core.convert.converter.Converter; import org.springframework.data.domain.Pageable; @@ -33,7 +33,7 @@ import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ReturnedType; -import org.springframework.data.repository.support.PageableExecutionUtils; +import org.springframework.data.support.PageableExecutionUtils; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -109,7 +109,7 @@ abstract class AbstractNeo4jQuery extends Neo4jQuerySupport implements Repositor protected abstract PreparedQuery prepareQuery(Class returnedType, List includedProperties, Neo4jParameterAccessor parameterAccessor, @Nullable Neo4jQueryType queryType, - @Nullable BiFunction mappingFunction); + @Nullable BiFunction mappingFunction); protected Optional> getCountQuery(Neo4jParameterAccessor parameterAccessor) { return Optional.empty(); diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/AbstractReactiveNeo4jQuery.java b/src/main/java/org/springframework/data/neo4j/repository/query/AbstractReactiveNeo4jQuery.java index d1fe0abcc..9982a8e2f 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/AbstractReactiveNeo4jQuery.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/AbstractReactiveNeo4jQuery.java @@ -18,7 +18,7 @@ package org.springframework.data.neo4j.repository.query; import java.util.List; import java.util.function.BiFunction; -import org.neo4j.driver.Record; +import org.neo4j.driver.types.MapAccessor; import org.neo4j.driver.types.TypeSystem; import org.springframework.core.convert.converter.Converter; import org.springframework.data.neo4j.core.PreparedQuery; @@ -83,5 +83,5 @@ abstract class AbstractReactiveNeo4jQuery extends Neo4jQuerySupport implements R protected abstract PreparedQuery prepareQuery(Class returnedType, List includedProperties, Neo4jParameterAccessor parameterAccessor, @Nullable Neo4jQueryType queryType, - @Nullable BiFunction mappingFunction); + @Nullable BiFunction mappingFunction); } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/DtoInstantiatingConverter.java b/src/main/java/org/springframework/data/neo4j/repository/query/DtoInstantiatingConverter.java index 4667ee044..b213f8512 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/DtoInstantiatingConverter.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/DtoInstantiatingConverter.java @@ -19,8 +19,8 @@ import java.util.Collection; import java.util.List; import java.util.function.Function; -import org.neo4j.driver.Record; import org.neo4j.driver.Value; +import org.neo4j.driver.types.MapAccessor; import org.neo4j.driver.types.TypeSystem; import org.springframework.core.CollectionFactory; import org.springframework.core.convert.converter.Converter; @@ -112,7 +112,7 @@ class DtoInstantiatingConverter implements Converter sourceProperty = sourceEntity.getPersistentProperty(targetPropertyName); diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/EntityInstanceWithSource.java b/src/main/java/org/springframework/data/neo4j/repository/query/EntityInstanceWithSource.java index 15e4c9027..70e66ec5b 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/EntityInstanceWithSource.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/EntityInstanceWithSource.java @@ -15,7 +15,7 @@ */ package org.springframework.data.neo4j.repository.query; -import org.neo4j.driver.Record; +import org.neo4j.driver.types.MapAccessor; import org.neo4j.driver.types.TypeSystem; /** @@ -39,9 +39,9 @@ final class EntityInstanceWithSource { /** * The record from which the source above was hydrated and which might contain top level properties that are elligable to mapping. */ - private final Record sourceRecord; + private final MapAccessor sourceRecord; - EntityInstanceWithSource(Object entityInstance, TypeSystem typeSystem, Record sourceRecord) { + EntityInstanceWithSource(Object entityInstance, TypeSystem typeSystem, MapAccessor sourceRecord) { this.entityInstance = entityInstance; this.typeSystem = typeSystem; @@ -56,7 +56,7 @@ final class EntityInstanceWithSource { return typeSystem; } - public Record getSourceRecord() { + public MapAccessor getSourceRecord() { return sourceRecord; } } diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQuerySupport.java b/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQuerySupport.java index b96de67e3..e52c94d87 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQuerySupport.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/Neo4jQuerySupport.java @@ -25,9 +25,9 @@ import java.util.function.BiFunction; import java.util.function.Function; import org.apache.commons.logging.LogFactory; -import org.neo4j.driver.Record; import org.neo4j.driver.Value; import org.neo4j.driver.Values; +import org.neo4j.driver.types.MapAccessor; import org.neo4j.driver.types.TypeSystem; import org.springframework.core.log.LogAccessor; import org.springframework.data.domain.Range; @@ -91,20 +91,20 @@ abstract class Neo4jQuerySupport { actualParameters); } - protected final BiFunction getMappingFunction(final ResultProcessor resultProcessor) { + protected final BiFunction getMappingFunction(final ResultProcessor resultProcessor) { final ReturnedType returnedTypeMetadata = resultProcessor.getReturnedType(); final Class returnedType = returnedTypeMetadata.getReturnedType(); final Class domainType = returnedTypeMetadata.getDomainType(); - final BiFunction mappingFunction; + final BiFunction mappingFunction; if (Neo4jSimpleTypes.HOLDER.isSimpleType(returnedType)) { // Clients automatically selects a single value mapping function. // It will thrown an error if the query contains more than one column. mappingFunction = null; } else if (returnedTypeMetadata.isProjecting()) { - BiFunction target = this.mappingContext.getRequiredMappingFunctionFor(domainType); + BiFunction target = this.mappingContext.getRequiredMappingFunctionFor(domainType); mappingFunction = (t, r) -> new EntityInstanceWithSource(target.apply(t, r), t, r); } else { mappingFunction = this.mappingContext.getRequiredMappingFunctionFor(domainType); diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java b/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java index 7802e076b..fde8e294b 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/PartTreeNeo4jQuery.java @@ -19,7 +19,7 @@ import java.util.List; import java.util.Optional; import java.util.function.BiFunction; -import org.neo4j.driver.Record; +import org.neo4j.driver.types.MapAccessor; import org.neo4j.driver.types.TypeSystem; import org.springframework.data.neo4j.core.Neo4jOperations; import org.springframework.data.neo4j.core.PreparedQuery; @@ -59,7 +59,7 @@ final class PartTreeNeo4jQuery extends AbstractNeo4jQuery { @Override protected PreparedQuery prepareQuery(Class returnedType, List includedProperties, Neo4jParameterAccessor parameterAccessor, @Nullable Neo4jQueryType queryType, - @Nullable BiFunction mappingFunction) { + @Nullable BiFunction mappingFunction) { CypherQueryCreator queryCreator = new CypherQueryCreator(mappingContext, getDomainType(queryMethod), Optional.ofNullable(queryType).orElseGet(() -> Neo4jQueryType.fromPartTree(tree)), tree, parameterAccessor, diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/ReactivePartTreeNeo4jQuery.java b/src/main/java/org/springframework/data/neo4j/repository/query/ReactivePartTreeNeo4jQuery.java index f9a71df6d..22008e968 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/ReactivePartTreeNeo4jQuery.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/ReactivePartTreeNeo4jQuery.java @@ -19,7 +19,7 @@ import java.util.List; import java.util.Optional; import java.util.function.BiFunction; -import org.neo4j.driver.Record; +import org.neo4j.driver.types.MapAccessor; import org.neo4j.driver.types.TypeSystem; import org.springframework.data.neo4j.core.PreparedQuery; import org.springframework.data.neo4j.core.ReactiveNeo4jOperations; @@ -59,7 +59,7 @@ final class ReactivePartTreeNeo4jQuery extends AbstractReactiveNeo4jQuery { @Override protected PreparedQuery prepareQuery(Class returnedType, List includedProperties, Neo4jParameterAccessor parameterAccessor, @Nullable Neo4jQueryType queryType, - @Nullable BiFunction mappingFunction) { + @Nullable BiFunction mappingFunction) { CypherQueryCreator queryCreator = new CypherQueryCreator(mappingContext, getDomainType(queryMethod), Optional.ofNullable(queryType).orElseGet(() -> Neo4jQueryType.fromPartTree(tree)), tree, parameterAccessor, diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveStringBasedNeo4jQuery.java b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveStringBasedNeo4jQuery.java index b71b6efe4..128e448a8 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveStringBasedNeo4jQuery.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/ReactiveStringBasedNeo4jQuery.java @@ -21,7 +21,7 @@ import java.util.Map; import java.util.Optional; import java.util.function.BiFunction; -import org.neo4j.driver.Record; +import org.neo4j.driver.types.MapAccessor; import org.neo4j.driver.types.TypeSystem; import org.springframework.data.mapping.MappingException; import org.springframework.data.neo4j.core.PreparedQuery; @@ -138,7 +138,7 @@ final class ReactiveStringBasedNeo4jQuery extends AbstractReactiveNeo4jQuery { @Override protected PreparedQuery prepareQuery(Class returnedType, List includedProperties, Neo4jParameterAccessor parameterAccessor, @Nullable Neo4jQueryType queryType, - @Nullable BiFunction mappingFunction) { + @Nullable BiFunction mappingFunction) { return PreparedQuery.queryFor(returnedType).withCypherQuery(cypherQuery) .withParameters(bindParameters(parameterAccessor)).usingMappingFunction(mappingFunction).build(); diff --git a/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java b/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java index ccb5b333d..a17552b45 100644 --- a/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java +++ b/src/main/java/org/springframework/data/neo4j/repository/query/StringBasedNeo4jQuery.java @@ -21,7 +21,7 @@ import java.util.Map; import java.util.Optional; import java.util.function.BiFunction; -import org.neo4j.driver.Record; +import org.neo4j.driver.types.MapAccessor; import org.neo4j.driver.types.TypeSystem; import org.springframework.data.domain.Pageable; import org.springframework.data.mapping.MappingException; @@ -135,7 +135,7 @@ final class StringBasedNeo4jQuery extends AbstractNeo4jQuery { @Override protected PreparedQuery prepareQuery(Class returnedType, List includedProperties, Neo4jParameterAccessor parameterAccessor, @Nullable Neo4jQueryType queryType, - @Nullable BiFunction mappingFunction) { + @Nullable BiFunction mappingFunction) { return PreparedQuery.queryFor(returnedType).withCypherQuery(cypherQuery) .withParameters(bindParameters(parameterAccessor)).usingMappingFunction(mappingFunction).build(); diff --git a/src/test/java/org/springframework/data/neo4j/core/Neo4jClientTest.java b/src/test/java/org/springframework/data/neo4j/core/Neo4jClientTest.java index a5833e346..2da488ee9 100644 --- a/src/test/java/org/springframework/data/neo4j/core/Neo4jClientTest.java +++ b/src/test/java/org/springframework/data/neo4j/core/Neo4jClientTest.java @@ -107,12 +107,10 @@ class Neo4jClientTest { parameters.put("bikeName", "M.*"); parameters.put("location", "Sweden"); - String cypher = "MATCH (o:User {name: $name}) - [:OWNS] -> (b:Bike) - [:USED_ON] -> (t:Trip) " - + "WHERE t.takenOn > $aDate " + " AND b.name =~ $bikeName " + " AND t.location = $location " + // TODO Nice - // place to - // add - // coordinates - "RETURN b"; + String cypher = "" + + "MATCH (o:User {name: $name}) - [:OWNS] -> (b:Bike) - [:USED_ON] -> (t:Trip) " + + "WHERE t.takenOn > $aDate " + " AND b.name =~ $bikeName " + " AND t.location = $location " + + "RETURN b"; Collection> usedBikes = client.query(cypher).bind("michael").to("name").bindAll(parameters) .bind(LocalDate.of(2019, 1, 1)).to("aDate").fetch().all(); diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java index 6480c6a8b..05a8687b3 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/RepositoryIT.java @@ -79,7 +79,6 @@ import org.springframework.data.neo4j.core.convert.Neo4jConversions; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.integration.imperative.repositories.PersonRepository; import org.springframework.data.neo4j.integration.imperative.repositories.ThingRepository; -import org.springframework.data.neo4j.integration.shared.common.KotlinPerson; import org.springframework.data.neo4j.integration.shared.common.AltHobby; import org.springframework.data.neo4j.integration.shared.common.AltLikedByPersonRelationship; import org.springframework.data.neo4j.integration.shared.common.AltPerson; @@ -98,9 +97,11 @@ import org.springframework.data.neo4j.integration.shared.common.FriendshipRelati import org.springframework.data.neo4j.integration.shared.common.Hobby; import org.springframework.data.neo4j.integration.shared.common.ImmutablePerson; import org.springframework.data.neo4j.integration.shared.common.Inheritance; +import org.springframework.data.neo4j.integration.shared.common.KotlinPerson; import org.springframework.data.neo4j.integration.shared.common.LikesHobbyRelationship; import org.springframework.data.neo4j.integration.shared.common.MultipleLabels; import org.springframework.data.neo4j.integration.shared.common.ParentNode; +import org.springframework.data.neo4j.integration.shared.common.Person; import org.springframework.data.neo4j.integration.shared.common.PersonWithAllConstructor; import org.springframework.data.neo4j.integration.shared.common.PersonWithNoConstructor; import org.springframework.data.neo4j.integration.shared.common.PersonWithRelationship; @@ -347,6 +348,51 @@ class RepositoryIT { assertThat(persons).anyMatch(person -> person.getName().equals(TEST_PERSON1_NAME)); } + @Test // DATAGRAPH-1429 + void aggregateThroughQueryIntoListShouldWork(@Autowired PersonRepository repository) { + + List people = repository.aggregateAllPeople(); + assertThat(people) + .hasSize(2) + .extracting(PersonWithAllConstructor::getName) + .containsExactlyInAnyOrder(TEST_PERSON1_NAME, TEST_PERSON2_NAME); + } + + @Test // DATAGRAPH-1429 + void aggregateThroughQueryIntoCustomObjectShouldWork(@Autowired PersonRepository repository) { + + PersonRepository.CustomAggregation customAggregation = repository.aggregateAllPeopleCustom(); + assertThat(customAggregation) + .hasSize(2) + .extracting(PersonWithAllConstructor::getName) + .containsExactlyInAnyOrder(TEST_PERSON1_NAME, TEST_PERSON2_NAME); + } + + @Test // DATAGRAPH-1429 + void aggregateThroughQueryIntoCustomObjectDTOShouldWork(@Autowired PersonRepository repository) { + + PersonRepository.CustomAggregationOfDto customAggregation = repository + .findAllDtoProjectionsWithAdditionalPropertiesAsCustomAggregation(TEST_PERSON1_NAME); + assertThat(customAggregation) + .isNotEmpty(); + assertThat(customAggregation.getBySomeLongValue(4711L)) + .satisfies(dto -> { + assertThat(dto.getFirstName()).isEqualTo(TEST_PERSON1_FIRST_NAME); + assertThat(dto.getSomeDoubles()).containsExactly(21.42, 42.21); + assertThat(dto.getOtherPeople()).hasSize(1) + .first() + .extracting(PersonWithAllConstructor::getFirstName) + .isEqualTo(TEST_PERSON2_FIRST_NAME); + }); + } + + @Test // DATAGRAPH-1429 + void queryAggregatesShouldWorkWithTheTemplate(@Autowired Neo4jTemplate template) { + + List people = template.findAll("unwind range(1,5) as i with i create (p:Person {firstName: toString(i)}) return p", Person.class); + assertThat(people).extracting(Person::getFirstName).containsExactly("1", "2", "3", "4", "5"); + } + @Test void loadOnePersonWithAllConstructor(@Autowired PersonRepository repository) { diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/repositories/PersonRepository.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/repositories/PersonRepository.java index 5d6dc9835..392dbfdbf 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/imperative/repositories/PersonRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/repositories/PersonRepository.java @@ -17,8 +17,11 @@ package org.springframework.data.neo4j.integration.imperative.repositories; import java.time.Instant; import java.time.LocalDate; +import java.util.Iterator; import java.util.List; import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; import java.util.stream.Stream; import org.neo4j.driver.types.Point; @@ -42,6 +45,7 @@ import org.springframework.data.neo4j.repository.query.BoundingBox; import org.springframework.data.neo4j.repository.query.Query; import org.springframework.data.neo4j.types.GeographicPoint2d; import org.springframework.data.repository.query.Param; +import org.springframework.data.util.Streamable; import org.springframework.transaction.annotation.Transactional; /** @@ -54,6 +58,28 @@ public interface PersonRepository extends Neo4jRepository aggregateAllPeople(); + + /** + * A custom aggregate that allows for something like getFriend1, 2 or other stuff... + */ + class CustomAggregation implements Streamable { + + private final Streamable delegate; + + public CustomAggregation(Streamable delegate) { + this.delegate = delegate; + } + + @Override public Iterator iterator() { + return delegate.iterator(); + } + } + + @Query("MATCH (n:PersonWithAllConstructor) return collect(n)") + CustomAggregation aggregateAllPeopleCustom(); + @Query("MATCH (n:PersonWithAllConstructor) return n") List getAllPersonsViaQuery(); @@ -239,6 +265,35 @@ public interface PersonRepository extends Neo4jRepository findAllDtoProjectionsWithAdditionalProperties(@Param("name") String name); + /** + * A custom aggregate that allows for something like getFriend1, 2 or other stuff... + */ + class CustomAggregationOfDto implements Streamable { + + private final Streamable delegate; + + public CustomAggregationOfDto(Streamable delegate) { + this.delegate = delegate; + } + + @Override public Iterator iterator() { + return delegate.iterator(); + } + + public DtoPersonProjectionContainingAdditionalFields getBySomeLongValue(long value) { + + return delegate.stream() + .collect(Collectors.toMap(DtoPersonProjectionContainingAdditionalFields::getSomeLongValue, Function.identity())) + .get(value); + } + } + + @Query("" + + "MATCH (n:PersonWithAllConstructor) where n.name = $name " + + "WITH n MATCH(m:PersonWithAllConstructor) WHERE id(n) <> id(m) " + + "RETURN [{n: n, otherPeople: collect(m), someLongValue: 4711, someDoubles: [21.42, 42.21]}]") + CustomAggregationOfDto findAllDtoProjectionsWithAdditionalPropertiesAsCustomAggregation(@Param("name") String name); + @Query("MATCH (n:PersonWithAllConstructor) where n.name = $name return n{.name}") PersonProjection findByNameWithCustomQueryAndMapProjection(@Param("name") String name); diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryIT.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryIT.java index dbe21b6cd..d4556c8e6 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryIT.java +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/ReactiveRepositoryIT.java @@ -63,6 +63,7 @@ import org.springframework.data.domain.Sort; import org.springframework.data.neo4j.config.AbstractReactiveNeo4jConfig; import org.springframework.data.neo4j.core.DatabaseSelection; import org.springframework.data.neo4j.core.ReactiveDatabaseSelectionProvider; +import org.springframework.data.neo4j.core.ReactiveNeo4jTemplate; import org.springframework.data.neo4j.integration.reactive.repositories.ReactivePersonRepository; import org.springframework.data.neo4j.integration.reactive.repositories.ReactiveThingRepository; import org.springframework.data.neo4j.integration.shared.common.AltHobby; @@ -78,6 +79,7 @@ import org.springframework.data.neo4j.integration.shared.common.Hobby; import org.springframework.data.neo4j.integration.shared.common.ImmutablePerson; import org.springframework.data.neo4j.integration.shared.common.LikesHobbyRelationship; import org.springframework.data.neo4j.integration.shared.common.MultipleLabels; +import org.springframework.data.neo4j.integration.shared.common.Person; import org.springframework.data.neo4j.integration.shared.common.PersonWithAllConstructor; import org.springframework.data.neo4j.integration.shared.common.PersonWithRelationship; import org.springframework.data.neo4j.integration.shared.common.PersonWithRelationshipWithProperties; @@ -144,8 +146,6 @@ class ReactiveRepositoryIT { @Override void setupData(Transaction transaction) { - transaction.run("MATCH (n) detach delete n"); - id1 = transaction.run("" + "CREATE (n:PersonWithAllConstructor) " + " SET n.name = $name, n.sameValue = $sameValue, n.first_name = $firstName, n.cool = $cool, n.personNumber = $personNumber, n.bornOn = $bornOn, n.nullable = 'something', n.things = ['a', 'b'], n.place = $place " + "RETURN id(n)", @@ -503,6 +503,24 @@ class ReactiveRepositoryIT { .expectNextMatches(personList::contains).verifyComplete(); } + @Test // DATAGRAPH-1429 + void aggregateThroughQueryIntoListShouldWork(@Autowired ReactivePersonRepository repository) { + List personList = Arrays.asList(person1, person2); + + StepVerifier.create(repository.aggregateAllPeople()).expectNextMatches(personList::contains) + .expectNextMatches(personList::contains).verifyComplete(); + } + + @Test // DATAGRAPH-1429 + void queryAggregatesShouldWorkWithTheTemplate(@Autowired ReactiveNeo4jTemplate template) { + + Flux people = template.findAll("unwind range(1,5) as i with i create (p:Person {firstName: toString(i)}) return p", Person.class); + + StepVerifier.create(people.map(Person::getFirstName)) + .expectNext("1", "2", "3", "4", "5") + .verifyComplete(); + } + @Test void loadOnePersonWithAllConstructor(@Autowired ReactivePersonRepository repository) { StepVerifier.create(repository.getOnePersonViaQuery()).expectNext(person1).verifyComplete(); diff --git a/src/test/java/org/springframework/data/neo4j/integration/reactive/repositories/ReactivePersonRepository.java b/src/test/java/org/springframework/data/neo4j/integration/reactive/repositories/ReactivePersonRepository.java index fb85d73a3..b72462b09 100644 --- a/src/test/java/org/springframework/data/neo4j/integration/reactive/repositories/ReactivePersonRepository.java +++ b/src/test/java/org/springframework/data/neo4j/integration/reactive/repositories/ReactivePersonRepository.java @@ -41,6 +41,9 @@ public interface ReactivePersonRepository extends ReactiveNeo4jRepository getAllPersonsViaQuery(); + @Query("MATCH (n:PersonWithAllConstructor) return collect(n)") + Flux aggregateAllPeople(); + @Query("MATCH (n:PersonWithAllConstructor{name:'Test'}) return n") Mono getOnePersonViaQuery();