From 263ae3976a61d32475374148c473ea248b177ecd Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Wed, 16 Jun 2021 13:40:11 +0200 Subject: [PATCH] GH-2292 - Support collections of entities as parameters to custom repository queries. This adds support for collections of entities as parameters. As of know, we try to figure out the common element of that collection and see if we have fitting entity type. If so, all elements are converted into `Map` and put into a list. Another approach would be trying to figure out the declared, resolvable type and than the generic type of a collection, but that would require much bigger changes in the infrastructure. An additional benefit: Heterogenous collections are supported as well to some extend. If this solution posses to be too slow, we can still investigate the generics approach. This closes #2292. --- .../data/neo4j/core/TemplateSupport.java | 8 +- .../repository/query/Neo4jQuerySupport.java | 18 ++++ .../integration/imperative/RepositoryIT.java | 90 +++++++++++++++---- 3 files changed, 97 insertions(+), 19 deletions(-) diff --git a/src/main/java/org/springframework/data/neo4j/core/TemplateSupport.java b/src/main/java/org/springframework/data/neo4j/core/TemplateSupport.java index b2fc1ddee..01ea82b6b 100644 --- a/src/main/java/org/springframework/data/neo4j/core/TemplateSupport.java +++ b/src/main/java/org/springframework/data/neo4j/core/TemplateSupport.java @@ -55,7 +55,7 @@ import org.springframework.util.Assert; * @since 6.0.9 */ @API(status = API.Status.INTERNAL, since = "6.0.9") -final class TemplateSupport { +public final class TemplateSupport { enum FetchType { @@ -64,11 +64,11 @@ final class TemplateSupport { } @Nullable - static Class findCommonElementType(Iterable collection) { + public static Class findCommonElementType(Iterable collection) { - List> allClasses = StreamSupport.stream(collection.spliterator(), true) + Collection> allClasses = StreamSupport.stream(collection.spliterator(), true) .filter(o -> o != null) - .map(Object::getClass).collect(Collectors.toList()); + .map(Object::getClass).collect(Collectors.toSet()); Class candidate = null; for (Class type : allClasses) { 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 1b8fdeeb9..a32015c4e 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 @@ -18,6 +18,7 @@ package org.springframework.data.neo4j.repository.query; import java.time.Instant; import java.time.ZoneOffset; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -28,6 +29,7 @@ import java.util.Set; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Supplier; +import java.util.stream.Collectors; import org.apache.commons.logging.LogFactory; import org.neo4j.driver.Value; @@ -35,11 +37,13 @@ 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.convert.EntityWriter; import org.springframework.data.domain.Range; import org.springframework.data.geo.Box; import org.springframework.data.geo.Circle; import org.springframework.data.geo.Distance; import org.springframework.data.geo.Metrics; +import org.springframework.data.neo4j.core.TemplateSupport; import org.springframework.data.neo4j.core.convert.Neo4jSimpleTypes; import org.springframework.data.neo4j.core.mapping.CypherGenerator; import org.springframework.data.neo4j.core.mapping.EntityInstanceWithSource; @@ -181,6 +185,20 @@ abstract class Neo4jQuerySupport { return convertBoundingBox((BoundingBox) parameter); } + Class type; + if (parameter instanceof Collection && mappingContext + .hasPersistentEntityFor(TemplateSupport.findCommonElementType((Collection) parameter))) { + + EntityWriter> objectMapEntityWriter = Neo4jNestedMapEntityWriter + .forContext(mappingContext); + + return ((Collection) parameter).stream().map(v -> { + Map result = new HashMap<>(); + objectMapEntityWriter.write(v, result); + return result; + }).collect(Collectors.toList()); + } + if (mappingContext.hasPersistentEntityFor(parameter.getClass())) { Map result = new HashMap<>(); 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 c61548cb8..262bba443 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 @@ -86,12 +86,11 @@ import org.springframework.data.neo4j.core.convert.Neo4jConversions; import org.springframework.data.neo4j.core.mapping.Neo4jMappingContext; import org.springframework.data.neo4j.core.transaction.Neo4jBookmarkManager; import org.springframework.data.neo4j.core.transaction.Neo4jTransactionManager; +import org.springframework.data.neo4j.integration.imperative.repositories.FlightRepository; import org.springframework.data.neo4j.integration.imperative.repositories.PersonRepository; import org.springframework.data.neo4j.integration.imperative.repositories.PersonWithNoConstructorRepository; import org.springframework.data.neo4j.integration.imperative.repositories.PersonWithWitherRepository; import org.springframework.data.neo4j.integration.imperative.repositories.ThingRepository; -import org.springframework.data.neo4j.integration.shared.common.Flight; -import org.springframework.data.neo4j.integration.imperative.repositories.FlightRepository; 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; @@ -110,6 +109,7 @@ import org.springframework.data.neo4j.integration.shared.common.EntitiesWithDyna import org.springframework.data.neo4j.integration.shared.common.EntityWithConvertedId; import org.springframework.data.neo4j.integration.shared.common.EntityWithRelationshipPropertiesPath; import org.springframework.data.neo4j.integration.shared.common.ExtendedParentNode; +import org.springframework.data.neo4j.integration.shared.common.Flight; import org.springframework.data.neo4j.integration.shared.common.Friend; import org.springframework.data.neo4j.integration.shared.common.FriendshipRelationship; import org.springframework.data.neo4j.integration.shared.common.Hobby; @@ -1909,17 +1909,7 @@ class RepositoryIT { Assumptions.assumeTrue(ServerVersion.version(driver).greaterThanOrEqual(ServerVersion.v4_1_0)); - PersonWithRelationship p = new PersonWithRelationship(); - p.setName("A Person"); - p.setId(4711L); - Hobby h = new Hobby(); - h.setName("A Hobby"); - p.setHobbies(h); - p.setPets(Arrays.asList(new Pet("A"), new Pet("B"))); - - Club club = new Club(); - club.setName("C27"); - p.setClub(club); + PersonWithRelationship p = createNewPerson("A Person", createNewClub("C27")); PersonWithRelationship newPerson = repository.createWithCustomQuery(p); newPerson = repository.findById(newPerson.getId()).get(); @@ -1929,6 +1919,46 @@ class RepositoryIT { assertThat(newPerson.getClub().getName()).isEqualTo("C27"); } + private PersonWithRelationship createNewPerson(String name, Club club) { + PersonWithRelationship p = new PersonWithRelationship(); + p.setName(name); + p.setId(4711L); + Hobby h = new Hobby(); + h.setName("A Hobby"); + p.setHobbies(h); + p.setPets(Arrays.asList(new Pet("A"), new Pet("B"))); + + p.setClub(club); + return p; + } + + private Club createNewClub(String name) { + Club club = new Club(); + club.setName(name); + return club; + } + + @Test // DATAGRAPH-2292 + void createWithCustomQueryShouldWorkWithCollectionsOfNestedObjects(@Autowired Driver driver, @Autowired RelationshipRepository repository) { + + Assumptions.assumeTrue(ServerVersion.version(driver).greaterThanOrEqual(ServerVersion.v4_1_0)); + + Club c27 = createNewClub("C27"); + Set people = new HashSet<>(); + people.add(createNewPerson("A person", c27)); + people.add(createNewPerson("Another person", c27)); + + List newPeople = repository.createManyWithCustomQuery(people); + assertThat(newPeople).hasSize(2) + .allSatisfy(p -> { + PersonWithRelationship newPerson = repository.findById(p.getId()).get(); + assertThat(newPerson.getName()).isEqualTo(p.getName()); + assertThat(newPerson.getHobbies().getName()).isEqualTo("A Hobby"); + assertThat(newPerson.getPets()).extracting(Pet::getName).containsExactlyInAnyOrder("A", "B"); + assertThat(newPerson.getClub().getName()).isEqualTo("C27"); + }); + } + @Test void saveSingleEntityWithRelationships(@Autowired RelationshipRepository repository) { @@ -1937,8 +1967,7 @@ class RepositoryIT { Hobby hobby = new Hobby(); hobby.setName("Music"); person.setHobbies(hobby); - Club club = new Club(); - club.setName("ClownsClub"); + Club club = createNewClub("ClownsClub"); person.setClub(club); Pet pet1 = new Pet("Jerry"); Pet pet2 = new Pet("Tom"); @@ -4142,6 +4171,37 @@ class RepositoryIT { + "RETURN n, collect(r), collect(p)") PersonWithRelationship createWithCustomQuery(PersonWithRelationship p); + @Transactional + @Query("UNWIND $0 AS pwr WITH pwr CREATE (n:PersonWithRelationship) \n" + + "SET n.name = pwr.__properties__.name \n" + + "WITH pwr, n, id(n) as parentId\n" + + "UNWIND pwr.__properties__.Has as x\n" + + "CALL { WITH x, parentId\n" + + " \n" + + " WITH x, parentId\n" + + " MATCH (_) \n" + + " WHERE id(_) = parentId AND x.__labels__[0] = 'Pet'\n" + + " CREATE (p:Pet {name: x.__properties__.name}) <- [r:Has] - (_)\n" + + " RETURN p, r\n" + + " \n" + + " UNION\n" + + " WITH x, parentId\n" + + " MATCH (_) \n" + + " WHERE id(_) = parentId AND x.__labels__[0] = 'Hobby'\n" + + " CREATE (p:Hobby {name: x.__properties__.name}) <- [r:Has] - (_)\n" + + " RETURN p, r\n" + + "\n" + + " UNION\n" + + " WITH x, parentId\n" + + " MATCH (_) \n" + + " WHERE id(_) = parentId AND x.__labels__[0] = 'Club'\n" + + " CREATE (p:Club {name: x.__properties__.name}) - [r:Has] -> (_)\n" + + " RETURN p, r\n" + + "\n" + + "}\n" + + "RETURN n, collect(r), collect(p)") + List createManyWithCustomQuery(Collection p); + PersonWithRelationship.PersonWithHobby findDistinctByHobbiesName(String hobbyName); }