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();