From 9ac3b99cb620a7b7b57e4bc2e67f252555eb197e Mon Sep 17 00:00:00 2001 From: Michael Simons Date: Fri, 27 Nov 2020 11:40:12 +0100 Subject: [PATCH] DATAGRAPH-1437 - Support mapping of named paths. This adds support for mapping named paths. To support this, a couple of changes are necessary: * The mapping process must be more strict: Only nodes with having at least the exact primary label are used to hydrate entites. One of our tests needed a fix. The test should have tested for various constructor operations on _entities_ but did test DTO projections. These dto projects had been mapped from custom queries, returning nodes without the label of the original entity. Of couse we did check for the primary label before, but the driver type indicator of MAP is not mutual exclusive. That means a node is map, too. * The mapping process should not pick the first matching thing to map: This means that the mapping will fail now if there is more than one structure that would theoritically fit The second biggest change here is that the `Neo4jEntityConverter` has become now stateful. It keeps a cache of seen objects for as long as one query keeps on running. That means for each interaction, the converter is created fresh (it hadn't been a bean beforehand). This allows to handle nodes on a path that are usually seen twice. The path mapping itself relies on the same mechanism that we introduced for DATAGRAPH-1429: Working with one aggregate coming back from the database on the top level. A named path is such an aggregate much like a list. The path mapping will only populate one main entity along a path, using the possible other nodes only for filling up relationships. The imperative and reactive template will return now the _distinct_ list of entities. --- etc/adr/adr-008.adoc | 39 ++ src/main/asciidoc/faq/faq.adoc | 114 ++++ src/main/asciidoc/img/bacon-distance.png | Bin 0 -> 20639 bytes .../{images => img}/springdatagraph.png | Bin .../data/neo4j/core/DefaultNeo4jClient.java | 3 +- .../data/neo4j/core/Neo4jTemplate.java | 4 +- .../data/neo4j/core/PreparedQuery.java | 90 +++- .../neo4j/core/ReactiveNeo4jTemplate.java | 4 +- .../data/neo4j/core/mapping/Constants.java | 11 +- .../mapping/DefaultNeo4jEntityConverter.java | 203 ++++--- .../neo4j/core/mapping/MappingSupport.java | 27 +- .../core/mapping/Neo4jMappingContext.java | 14 +- .../mapping/NoRootNodeMappingException.java | 37 ++ .../data/neo4j/core/mapping/Schema.java | 6 +- .../integration/imperative/RepositoryIT.java | 14 +- .../repositories/PersonRepository.java | 27 +- .../PersonWithNoConstructorRepository.java | 38 ++ .../PersonWithWitherRepository.java | 39 ++ .../data/neo4j/integration/movies/Actor.java | 59 ++ .../integration/movies/AdvancedMappingIT.java | 167 ++++++ .../data/neo4j/integration/movies/Movie.java | 88 +++ .../data/neo4j/integration/movies/Person.java | 84 +++ src/test/resources/data/movies.cypher | 508 ++++++++++++++++++ 23 files changed, 1433 insertions(+), 143 deletions(-) create mode 100644 etc/adr/adr-008.adoc create mode 100644 src/main/asciidoc/img/bacon-distance.png rename src/main/asciidoc/{images => img}/springdatagraph.png (100%) create mode 100644 src/main/java/org/springframework/data/neo4j/core/mapping/NoRootNodeMappingException.java create mode 100644 src/test/java/org/springframework/data/neo4j/integration/imperative/repositories/PersonWithNoConstructorRepository.java create mode 100644 src/test/java/org/springframework/data/neo4j/integration/imperative/repositories/PersonWithWitherRepository.java create mode 100644 src/test/java/org/springframework/data/neo4j/integration/movies/Actor.java create mode 100644 src/test/java/org/springframework/data/neo4j/integration/movies/AdvancedMappingIT.java create mode 100644 src/test/java/org/springframework/data/neo4j/integration/movies/Movie.java create mode 100644 src/test/java/org/springframework/data/neo4j/integration/movies/Person.java create mode 100644 src/test/resources/data/movies.cypher diff --git a/etc/adr/adr-008.adoc b/etc/adr/adr-008.adoc new file mode 100644 index 000000000..9dbb7b5d6 --- /dev/null +++ b/etc/adr/adr-008.adoc @@ -0,0 +1,39 @@ +== ADR 8: Strictness of node mapping + +=== Status + +accepted + +=== Context + +Up until 6.0.1 included SDN might map arbitrary nodes onto domain classes if there's no exact fit. +We could keep it that way or be more strict about what to map. +This became apparent while working on hydrating domain objects on paths. + +=== Decision + +SDN prior to 6.0.2 did the following when there was not exactly one node with the primary target label in the result set: + +* Check if there are structures having a `MAP` type in the result record +* Take the first one + +As it happens, driver type `MAP` is not mutual exclusive from `NODE` or `RELATIONSHIP`. +That means a mapping request for an entity labeled `A` and a record containing only nodes labeled `B` and `C` +would either map `B` or `C` to the entity. + +These results are not desirable and would lead in the worst case to data loss (attributes not being populated +with a write afterwards) or to a non-deterministic mapping in case SDN was dealing was custom query that did return +`*` without enumerating columns explicitly. + +The decision was made to be strict, both in what types are mapped as fallbacks (only "pure" maps) and that +only nodes with a matching label are mapped. + +In the context of this, the `Neo4jEntityConverter` has been made stateful as well so that it is able to +distinguish between objects it has already seen and objects that are new. + +=== Consequences + +Some users might see an exception of type `NoRootNodeMappingException` or a general `MappingException`. +This happens usually on either custom queries that don't contain all the necessary content or on mappings +that like we had in the `RepositoryIT`. There we needed to fix a test that accidentally created DTO projections +based on data that wouldn't have fit the original entity. diff --git a/src/main/asciidoc/faq/faq.adoc b/src/main/asciidoc/faq/faq.adoc index e9e6f687f..81a882284 100644 --- a/src/main/asciidoc/faq/faq.adoc +++ b/src/main/asciidoc/faq/faq.adoc @@ -229,6 +229,120 @@ movieExample = Example.of( movies = this.movieRepository.findAll(movieExample); ---- +[[faq.path-mapping]] +== Can I map named paths? + +A series of connected nodes and relationships is called a "path" in Neo4j. +Cypher allows paths to be named using an identifer, as exemplified by: + +[source,cypher] +---- +p = (a)-[*3..5]->(b) +---- + +or as in the infamous Movie graph, that includes the following path (in that case, one of the shortest path between two actors): + +[[bacon-distance]] +[source,cypher] +.The "Bacon" distance +---- +MATCH p=shortestPath((bacon:Person {name:"Kevin Bacon"})-[*]-(meg:Person {name:"Meg Ryan"})) +RETURN p +---- + +Which looks like this: + +image::bacon-distance.png[] + +We find 3 nodes labeled `Person` and 2 nodes labeled `Movie`. Both can be mapped with a custom queury. +Assume there's a node entity for both `Person` and `Movie` as well as `Actor` taking care of the relationship: + + +[source,java] +."Standard" movie graph domain model +---- +@Node +public final class Person { + + @Id @GeneratedValue + private final Long id; + + private final String name; + + private Integer born; + + @Relationship("REVIEWED") + private List reviewed = new ArrayList<>(); +} + +@RelationshipProperties +public final class Actor { + + @TargetNode + private final Person person; + + private final List roles; +} + +@Node +public final class Movie { + + @Id + private final String title; + + @Property("tagline") + private final String description; + + @Relationship(value = "ACTED_IN", direction = Direction.INCOMING) + private final List actors; +} +---- + +When using a query as shown in <> for a domain class of type `Person` like this + +[source,java] +---- +interface PeopleRepository extends Neo4jRepository { + @Query("" + + "MATCH p=shortestPath((bacon:Person {name: $person1})-[*]-(meg:Person {name: $person2}))\n" + + "RETURN p" + ) + List findAllOnShortestPathBetween(@Param("person1") String person1, @Param("person2") String person2); +} +---- + +it will retrieve all people from the path and map them. +If there are relationship types on the path like `REVIEWED` that are also present on the domain, these +will be filled accordingly from the path. + +WARNING: Take special care when you use nodes hydrated from a path based query to save data. + If not all relationships are hydrated, data will be lost. + +The other way round works as well. The same query can be used with the `Movie` entity. +It then will only populate movies. +The following listing shows how todo this as well as how the query can be enriched with additional data +not found on the path. That data is used to correctly populate the missing relationships (in that case, all the actors) + +[source,java] +---- +interface MovieRepository extends Neo4jRepository { + + @Query("" + + "MATCH p=shortestPath(\n" + + "(bacon:Person {name: $person1})-[*]-(meg:Person {name: $person2}))\n" + + "WITH p, [n IN nodes(p) WHERE n:Movie] AS x\n" + + "UNWIND x AS m\n" + + "MATCH (m) <-[r:DIRECTED]-(d:Person)\n" + + "RETURN p, collect(r), collect(d)" + ) + List findAllOnShortestPathBetween(@Param("person1") String person1, @Param("person2") String person2); +} +---- + +The query returns the path plus all relationships and related nodes collected so that the movie entities are fully hydrated. + +The path mapping works for single paths as well for multiple records of paths (which are returned by the `allShortestPath` function.) + [[faq.spring-boot.sdn]] == Do I need Spring Boot to use Spring Data Neo4j? diff --git a/src/main/asciidoc/img/bacon-distance.png b/src/main/asciidoc/img/bacon-distance.png new file mode 100644 index 0000000000000000000000000000000000000000..6bf0b9d5c03c3c317a811ec0e30448401426f8e0 GIT binary patch literal 20639 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 values = result.stream().map(partialMappingFunction(typeSystem)).collect(Collectors.toList()); + Collection values = result.stream().map(partialMappingFunction(typeSystem)).collect(Collectors.toList()); ResultSummaries.process(result.consume()); return values; } catch (RuntimeException e) { 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 886249728..d97452024 100644 --- a/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java +++ b/src/main/java/org/springframework/data/neo4j/core/Neo4jTemplate.java @@ -577,7 +577,7 @@ public final class Neo4jTemplate implements Neo4jOperations, BeanFactoryAware { Collection all = fetchSpec.all(); if (preparedQuery.resultsHaveBeenAggregated()) { - return all.stream().flatMap(nested -> ((Collection) nested).stream()).collect(Collectors.toList()); + return all.stream().flatMap(nested -> ((Collection) nested).stream()).distinct().collect(Collectors.toList()); } return all.stream().collect(Collectors.toList()); } @@ -588,7 +588,7 @@ public final class Neo4jTemplate implements Neo4jOperations, BeanFactoryAware { } catch (NoSuchRecordException e) { // This exception is thrown by the driver in both cases when there are 0 or 1+n records // So there has been an incorrect result size, but not to few results but to many. - throw new IncorrectResultSizeDataAccessException(1); + throw new IncorrectResultSizeDataAccessException(e.getMessage(), 1); } } 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 266d86b1d..ccda334bf 100644 --- a/src/main/java/org/springframework/data/neo4j/core/PreparedQuery.java +++ b/src/main/java/org/springframework/data/neo4j/core/PreparedQuery.java @@ -15,17 +15,29 @@ */ package org.springframework.data.neo4j.core; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Collectors; import org.apiguardian.api.API; 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.Path; import org.neo4j.driver.types.TypeSystem; +import org.springframework.data.neo4j.core.mapping.Constants; +import org.springframework.data.neo4j.core.mapping.MappingSupport; +import org.springframework.data.neo4j.core.mapping.NoRootNodeMappingException; import org.springframework.lang.Nullable; /** @@ -58,7 +70,7 @@ public final class PreparedQuery { if (optionalBuildSteps.mappingFunction == null) { this.mappingFunction = null; } else { - this.mappingFunction = (BiFunction) new ListAggregatingMappingFunction( + this.mappingFunction = (BiFunction) new AggregatingMappingFunction( optionalBuildSteps.mappingFunction); } this.cypherQuery = optionalBuildSteps.cypherQuery; @@ -74,7 +86,7 @@ public final class PreparedQuery { } boolean resultsHaveBeenAggregated() { - return this.mappingFunction != null && ((ListAggregatingMappingFunction) this.mappingFunction).hasAggregated(); + return this.mappingFunction != null && ((AggregatingMappingFunction) this.mappingFunction).hasAggregated(); } public String getCypherQuery() { @@ -139,22 +151,84 @@ public final class PreparedQuery { } } - private static class ListAggregatingMappingFunction implements BiFunction { + private static class AggregatingMappingFunction implements BiFunction { private final BiFunction target; private final AtomicBoolean aggregated = new AtomicBoolean(false); - ListAggregatingMappingFunction(BiFunction target) { + AggregatingMappingFunction(BiFunction target) { this.target = target; } + private Collection aggregateList(TypeSystem t, Value value) { + + if (MappingSupport.isListContainingOnly(t.LIST(), t.PATH()).test(value)) { + Set result = new LinkedHashSet<>(); + for (Value path : value.values()) { + result.addAll(aggregatePath(t, path, Collections.emptyList())); + } + return result; + } + return value.asList(v -> target.apply(t, v)); + } + + private Collection aggregatePath(TypeSystem t, Value value, + List> additionalValues) { + Path path = value.asPath(); + + // We are using a linked hash set here so that the order of nodes will be stable and + // match the one the path + Set result = new LinkedHashSet<>(); + path.iterator().forEachRemaining(segment -> { + + Map mapValue = new HashMap<>(); + mapValue.put(Constants.NAME_OF_IS_PATH_SEGMENT, Values.value(true)); + mapValue.put(Constants.PATH_START, Values.value(segment.start())); + mapValue.put(Constants.PATH_RELATIONSHIP, Values.value(segment.relationship())); + mapValue.put(Constants.PATH_END, Values.value(segment.end())); + additionalValues.forEach(e -> mapValue.put(e.getKey(), e.getValue())); + + Value v = Values.value(mapValue); + try { + result.add(target.apply(t, v)); + } catch (NoRootNodeMappingException e) { + // This is the case for nodes on the path that are not of the target type + // We can safely ignore those. + } + }); + + return result; + } + @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)); + + if (r.size() == 1) { + Value value = r.get(0); + if (value.hasType(t.LIST())) { + aggregated.compareAndSet(false, true); + return aggregateList(t, value); + } else if (value.hasType(t.PATH())) { + aggregated.compareAndSet(false, true); + return aggregatePath(t, value, Collections.emptyList()); + } + } + + try { + return target.apply(t, new RecordMapAccessor(r)); + } catch (NoRootNodeMappingException e) { + + // We didn't find anything on the top level. It still can be a path plus some additional information + // to enrich the nodes on the path with. + Map>> pathValues = r.asMap(Function.identity()).entrySet() + .stream() + .collect(Collectors.partitioningBy(entry -> entry.getValue().hasType(t.PATH()))); + if (pathValues.get(true).size() == 1) { + aggregated.compareAndSet(false, true); + return aggregatePath(t, pathValues.get(true).get(0).getValue(), pathValues.get(false)); + } + throw e; } - return target.apply(t, new RecordMapAccessor(r)); } boolean hasAggregated() { 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 8065e96da..14f2917dd 100644 --- a/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java +++ b/src/main/java/org/springframework/data/neo4j/core/ReactiveNeo4jTemplate.java @@ -598,7 +598,7 @@ public final class ReactiveNeo4jTemplate implements ReactiveNeo4jOperations, Bea return fetchSpec.all().switchOnFirst((signal, f) -> { if (preparedQuery.resultsHaveBeenAggregated()) { - return f.flatMap(nested -> Flux.fromIterable((Collection) nested)); + return f.flatMap(nested -> Flux.fromIterable((Collection) nested).distinct()); } return f; }); @@ -614,7 +614,7 @@ public final class ReactiveNeo4jTemplate implements ReactiveNeo4jOperations, Bea } catch (NoSuchRecordException e) { // This exception is thrown by the driver in both cases when there are 0 or 1+n records // So there has been an incorrect result size, but not to few results but to many. - throw new IncorrectResultSizeDataAccessException(1); + throw new IncorrectResultSizeDataAccessException(e.getMessage(), 1); } } } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/Constants.java b/src/main/java/org/springframework/data/neo4j/core/mapping/Constants.java index fbe0f638c..05ed6b0f9 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/Constants.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/Constants.java @@ -21,7 +21,8 @@ import org.neo4j.cypherdsl.core.Cypher; import org.neo4j.cypherdsl.core.SymbolicName; /** - * A pool of constants used in our Cypher generation. These constants may change without further notice. + * A pool of constants used in our Cypher generation. These constants may change without further notice and are meant + * for internal use only. * * @author Michael J. Simons * @soundtrack Milky Chance - Sadnecessary @@ -42,8 +43,14 @@ public final class Constants { public static final String NAME_OF_ENTITY_LIST_PARAM = "__entities__"; public static final String NAME_OF_PATHS = "__paths__"; public static final String NAME_OF_ALL_PROPERTIES = "__allProperties__"; + public static final String NAME_OF_IS_PATH_SEGMENT = "__is_path_segment__"; + + public static final String PATH_START = "__start__"; + public static final String PATH_RELATIONSHIP = "__relationship__"; + public static final String PATH_END = "__end__"; public static final String FROM_ID_PARAMETER_NAME = "fromId"; - private Constants() {} + private Constants() { + } } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jEntityConverter.java b/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jEntityConverter.java index 631c86db6..ed5db74b7 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jEntityConverter.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/DefaultNeo4jEntityConverter.java @@ -31,18 +31,18 @@ import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Collectors; +import java.util.stream.Stream; import java.util.stream.StreamSupport; -import org.apache.commons.logging.LogFactory; import org.neo4j.driver.Value; import org.neo4j.driver.Values; import org.neo4j.driver.types.MapAccessor; import org.neo4j.driver.types.Node; import org.neo4j.driver.types.Path; import org.neo4j.driver.types.Relationship; +import org.neo4j.driver.types.Type; import org.neo4j.driver.types.TypeSystem; import org.springframework.core.CollectionFactory; -import org.springframework.core.log.LogAccessor; import org.springframework.data.mapping.AssociationHandler; import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.PersistentPropertyAccessor; @@ -66,65 +66,102 @@ import org.springframework.util.Assert; */ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { - private static final LogAccessor log = new LogAccessor(LogFactory.getLog(DefaultNeo4jEntityConverter.class)); - private final EntityInstantiators entityInstantiators; private final NodeDescriptionStore nodeDescriptionStore; private final Neo4jConversionService conversionService; - private TypeSystem typeSystem; + private final KnownObjects knownObjects = new KnownObjects(); - DefaultNeo4jEntityConverter(EntityInstantiators entityInstantiators, Neo4jConversionService conversionService, NodeDescriptionStore nodeDescriptionStore) { + private final Type nodeType; + private final Type relationshipType; + private final Type mapType; + private final Type listType; + private final Type pathType; + + DefaultNeo4jEntityConverter(EntityInstantiators entityInstantiators, Neo4jConversionService conversionService, + NodeDescriptionStore nodeDescriptionStore, TypeSystem typeSystem) { Assert.notNull(entityInstantiators, "EntityInstantiators must not be null!"); Assert.notNull(conversionService, "Neo4jConversionService must not be null!"); Assert.notNull(nodeDescriptionStore, "NodeDescriptionStore must not be null!"); + Assert.notNull(typeSystem, "TypeSystem must not be null!"); this.entityInstantiators = entityInstantiators; this.conversionService = conversionService; this.nodeDescriptionStore = nodeDescriptionStore; + + this.nodeType = typeSystem.NODE(); + this.relationshipType = typeSystem.RELATIONSHIP(); + this.mapType = typeSystem.MAP(); + this.listType = typeSystem.LIST(); + this.pathType = typeSystem.PATH(); } @Override public R read(Class targetType, MapAccessor mapAccessor) { - Neo4jPersistentEntity rootNodeDescription = (Neo4jPersistentEntity) nodeDescriptionStore - .getNodeDescription(targetType); + Neo4jPersistentEntity rootNodeDescription = (Neo4jPersistentEntity) nodeDescriptionStore.getNodeDescription(targetType); + MapAccessor queryRoot = determineQueryRoot(mapAccessor, rootNodeDescription); + if (queryRoot == null) { + throw new NoRootNodeMappingException(String.format("Could not find mappable nodes or relationships inside %s for %s", mapAccessor, rootNodeDescription)); + } try { - Iterable recordValues = mapAccessor instanceof Value && ((Value) mapAccessor).hasType(typeSystem.NODE()) ? - Collections.singletonList((Value) mapAccessor) : mapAccessor.values(); - String nodeLabel = rootNodeDescription.getPrimaryLabel(); - MapAccessor queryRoot = null; - for (Value value : recordValues) { - if (value.hasType(typeSystem.NODE()) && value.asNode().hasLabel(nodeLabel)) { - if (mapAccessor.size() > 1) { - queryRoot = mergeRootNodeWithRecord(value.asNode(), mapAccessor); - } else { - queryRoot = value.asNode(); - } - break; - } - } - if (queryRoot == null) { - for (Value value : recordValues) { - if (value.hasType(typeSystem.MAP())) { - queryRoot = value; - break; - } - } - } - - if (queryRoot == null) { - throw new MappingException(String.format("Could not find mappable nodes or relationships inside %s for %s", mapAccessor, rootNodeDescription)); - } else { - return map(queryRoot, queryRoot, rootNodeDescription, new KnownObjects(), new HashSet<>()); - } + return map(queryRoot, queryRoot, rootNodeDescription, new HashSet<>()); } catch (Exception e) { throw new MappingException("Error mapping " + mapAccessor.toString(), e); } } + private MapAccessor determineQueryRoot(MapAccessor mapAccessor, Neo4jPersistentEntity rootNodeDescription) { + + String primaryLabel = rootNodeDescription.getPrimaryLabel(); + + // Massage the initial mapAccessor into something we can deal with + Iterable recordValues = mapAccessor instanceof Value && ((Value) mapAccessor).hasType(nodeType) ? + Collections.singletonList((Value) mapAccessor) : mapAccessor.values(); + + List matchingNodes = new ArrayList<>(); // The node that eventually becomes the query root. The list should only contain one node. + List seenMatchingNodes = new ArrayList<>(); // A list of candidates: All things that are nodes and have a matching label + + for (Value value : recordValues) { + if (value.hasType(nodeType)) { // It is a node + Node node = value.asNode(); + if (node.hasLabel(primaryLabel)) { // it has a matching label + // We haven't seen this node yet, so we take it + if (knownObjects.getObject(node.id()) == null) { + matchingNodes.add(node); + } else { + seenMatchingNodes.add(node); + } + } + } + } + + // Prefer the candidates over candidates previously seen + List finalCandidates = matchingNodes.isEmpty() ? seenMatchingNodes : matchingNodes; + MapAccessor queryRoot = null; + + if (finalCandidates.size() > 1 && !mapAccessor.containsKey(Constants.NAME_OF_IS_PATH_SEGMENT)) { + throw new MappingException("More than one matching node in the record."); + } else if (!finalCandidates.isEmpty()) { + if (mapAccessor.size() > 1) { + queryRoot = mergeRootNodeWithRecord(finalCandidates.get(0), mapAccessor); + } else { + queryRoot = finalCandidates.get(0); + } + } else { + for (Value value : recordValues) { + if (value.hasType(mapType) && !(value.hasType(nodeType) || value.hasType( + relationshipType))) { + queryRoot = value; + break; + } + } + } + return queryRoot; + } + private Collection createDynamicLabelsProperty(TypeInformation type, Collection dynamicLabels) { Collection target = CollectionFactory.createCollection(type.getType(), String.class, dynamicLabels.size()); @@ -172,10 +209,6 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { } } - void setTypeSystem(TypeSystem typeSystem) { - this.typeSystem = typeSystem; - } - /** * Merges the root node of a query and the remaining record into one map, adding the internal ID of the node, too. * Merge happens only when the record contains additional values. @@ -198,17 +231,16 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { * @param queryResult The original query result or a reduced form like a node or similar * @param allValues The original query result * @param nodeDescription The node description of the current entity to be mapped from the result - * @param knownObjects The current list of known objects * @param processedSegments Path segments already processed in the mapping process. Only applies to path-based queries * @param As in entity type * @return The mapped entity */ - private ET map(MapAccessor queryResult, MapAccessor allValues, Neo4jPersistentEntity nodeDescription, KnownObjects knownObjects, Set processedSegments) { - return map(queryResult, allValues, nodeDescription, knownObjects, null, processedSegments); + private ET map(MapAccessor queryResult, MapAccessor allValues, Neo4jPersistentEntity nodeDescription, Set processedSegments) { + return map(queryResult, allValues, nodeDescription, null, processedSegments); } - private ET map(MapAccessor queryResult, MapAccessor allValues, Neo4jPersistentEntity nodeDescription, KnownObjects knownObjects, - @Nullable Object lastMappedEntity, Set processedSegments) { + private ET map(MapAccessor queryResult, MapAccessor allValues, Neo4jPersistentEntity nodeDescription, + @Nullable Object lastMappedEntity, Set processedSegments) { // if the given result does not contain an identifier to the mapped object cannot get temporarily saved Long internalId = getInternalId(queryResult); @@ -223,7 +255,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { Collection relationships = concreteNodeDescription.getRelationships(); - ET instance = instantiate(concreteNodeDescription, queryResult, allValues, knownObjects, relationships, + ET instance = instantiate(concreteNodeDescription, queryResult, allValues, relationships, nodeDescriptionAndLabels.getDynamicLabels(), lastMappedEntity, processedSegments); PersistentPropertyAccessor propertyAccessor = concreteNodeDescription.getPropertyAccessor(instance); @@ -243,7 +275,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { knownObjects.storeObject(internalId, instance); // Fill associations concreteNodeDescription.doWithAssociations( - populateFrom(queryResult, allValues, propertyAccessor, isConstructorParameter, relationships, knownObjects, processedSegments)); + populateFrom(queryResult, allValues, propertyAccessor, isConstructorParameter, relationships, processedSegments)); } ET bean = propertyAccessor.getBean(); @@ -288,9 +320,10 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { return labels; } - private ET instantiate(Neo4jPersistentEntity nodeDescription, MapAccessor values, MapAccessor allValues, KnownObjects knownObjects, - Collection relationships, Collection surplusLabels, Object lastMappedEntity, - Set processedSegments) { + private ET instantiate(Neo4jPersistentEntity nodeDescription, MapAccessor values, MapAccessor allValues, + Collection relationships, Collection surplusLabels, + Object lastMappedEntity, + Set processedSegments) { ParameterValueProvider parameterValueProvider = new ParameterValueProvider() { @Override @@ -299,7 +332,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { Neo4jPersistentProperty matchingProperty = nodeDescription.getRequiredPersistentProperty(parameter.getName()); if (matchingProperty.isRelationship()) { - return createInstanceOfRelationships(matchingProperty, values, allValues, knownObjects, relationships, processedSegments).orElse(null); + return createInstanceOfRelationships(matchingProperty, values, allValues, relationships, processedSegments).orElse(null); } else if (matchingProperty.isDynamicLabels()) { return createDynamicLabelsProperty(matchingProperty.getTypeInformation(), surplusLabels); } else if (matchingProperty.isEntityInRelationshipWithProperties()) { @@ -336,7 +369,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { private AssociationHandler populateFrom(MapAccessor queryResult, MapAccessor allValues, PersistentPropertyAccessor propertyAccessor, Predicate isConstructorParameter, - Collection relationshipDescriptions, KnownObjects knownObjects, Set processedSegments) { + Collection relationshipDescriptions, Set processedSegments) { return association -> { Neo4jPersistentProperty persistentProperty = association.getInverse(); @@ -344,18 +377,18 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { return; } - createInstanceOfRelationships(persistentProperty, queryResult, allValues, knownObjects, relationshipDescriptions, processedSegments) + createInstanceOfRelationships(persistentProperty, queryResult, allValues, relationshipDescriptions, processedSegments) .ifPresent(value -> propertyAccessor.setProperty(persistentProperty, value)); }; } private Optional createInstanceOfRelationships(Neo4jPersistentProperty persistentProperty, MapAccessor values, - MapAccessor allValues, KnownObjects knownObjects, Collection relationshipDescriptions, Set processedSegments) { + MapAccessor allValues, Collection relationshipDescriptions, Set processedSegments) { RelationshipDescription relationshipDescription = relationshipDescriptions.stream() .filter(r -> r.getFieldName().equals(persistentProperty.getName())).findFirst().get(); - String relationshipType = relationshipDescription.getType(); + String typeOfRelationship = relationshipDescription.getType(); String sourceLabel = relationshipDescription.getSource().getPrimaryLabel(); String targetLabel = relationshipDescription.getTarget().getPrimaryLabel(); @@ -398,26 +431,21 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { boolean isGeneratedPathBased = allValues.containsKey(Constants.NAME_OF_PATHS); - Predicate isList = entry -> typeSystem.LIST().isTypeOf(entry); - if (isGeneratedPathBased) { Value internalStartNodeIdValue = values.get(Constants.NAME_OF_INTERNAL_ID); long startNodeId = internalStartNodeIdValue.asLong(); - Predicate containsOnlyPaths = entry -> entry.asList(Function.identity()).stream() - .allMatch(listEntry -> typeSystem.PATH().isTypeOf(listEntry)); - List allPaths = StreamSupport.stream(values.values().spliterator(), false) - .filter(isList.and(containsOnlyPaths)).flatMap(entry -> entry.asList(Value::asPath).stream()) + .filter(MappingSupport.isListContainingOnly(listType, pathType)).flatMap(entry -> entry.asList(Value::asPath).stream()) .collect(Collectors.toList()); List segments = allPaths.stream() .flatMap(p -> StreamSupport.stream(p.spliterator(), false)) .filter(s -> s.start().id() == startNodeId - && (relationshipDescription.isIncoming() ? s.relationship().endNodeId() : s.relationship().startNodeId()) == startNodeId - && (s.relationship().hasType(relationshipType) || relationshipDescription.isDynamic()) - && s.end().hasLabel(targetLabel)) + && (relationshipDescription.isIncoming() ? s.relationship().endNodeId() : s.relationship().startNodeId()) == startNodeId + && (s.relationship().hasType(typeOfRelationship) || relationshipDescription.isDynamic()) + && s.end().hasLabel(targetLabel)) .distinct() .collect(Collectors.toList()); @@ -432,7 +460,7 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { Object relationshipProperties = map(segment.relationship(), allValues, (Neo4jPersistentEntity) relationshipDescription.getRelationshipPropertiesEntity(), - knownObjects, mappedObject, processedSegments); + mappedObject, processedSegments); relationshipsAndProperties.add(relationshipProperties); mappedObjectHandler.accept(segment.relationship().type(), relationshipProperties); } else { @@ -441,22 +469,33 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { } } else if (Values.NULL.equals(list)) { - Predicate containsOnlyRelationships = entry -> entry.asList(Function.identity()).stream() - .allMatch(listEntry -> typeSystem.RELATIONSHIP().isTypeOf(listEntry)); + Collection allMatchingTypeRelationshipsInResult = new ArrayList<>(); + Collection allNodesWithMatchingLabelInResult = new ArrayList<>(); - Predicate containsOnlyNodes = entry -> entry.asList(Function.identity()).stream() - .allMatch(listEntry -> typeSystem.NODE().isTypeOf(listEntry)); + // find relationships and related nodes in the result + // Take special care of the components of a path segment + if (allValues.containsKey(Constants.NAME_OF_IS_PATH_SEGMENT) && allValues.get(Constants.NAME_OF_IS_PATH_SEGMENT).asBoolean()) { + Stream.of(allValues.get(Constants.PATH_RELATIONSHIP).asRelationship()) + .filter(r -> r.type().equals(typeOfRelationship) || relationshipDescription.isDynamic()) + .forEach(allMatchingTypeRelationshipsInResult::add); - // find relationships in the result - List allMatchingTypeRelationshipsInResult = StreamSupport - .stream(allValues.values().spliterator(), false).filter(isList.and(containsOnlyRelationships)) + Stream.of(allValues.get(Constants.PATH_START).asNode(), allValues.get(Constants.PATH_START).asNode()) + .filter(n -> n.hasLabel(targetLabel)).collect(Collectors.toList()) + .forEach(allNodesWithMatchingLabelInResult::add); + } + + // Grab everything else + StreamSupport.stream(allValues.values().spliterator(), false) + .filter(MappingSupport.isListContainingOnly(listType, this.relationshipType)) .flatMap(entry -> entry.asList(Value::asRelationship).stream()) - .filter(r -> r.type().equals(relationshipType) || relationshipDescription.isDynamic()) - .collect(Collectors.toList()); + .filter(r -> r.type().equals(typeOfRelationship) || relationshipDescription.isDynamic()) + .forEach(allMatchingTypeRelationshipsInResult::add); - List allNodesWithMatchingLabelInResult = StreamSupport.stream(allValues.values().spliterator(), false) - .filter(isList.and(containsOnlyNodes)).flatMap(entry -> entry.asList(Value::asNode).stream()) - .filter(n -> n.hasLabel(targetLabel)).collect(Collectors.toList()); + StreamSupport.stream(allValues.values().spliterator(), false) + .filter(MappingSupport.isListContainingOnly(listType, this.nodeType)) + .flatMap(entry -> entry.asList(Value::asNode).stream()) + .filter(n -> n.hasLabel(targetLabel)).collect(Collectors.toList()) + .forEach(allNodesWithMatchingLabelInResult::add); if (allNodesWithMatchingLabelInResult.isEmpty() && allMatchingTypeRelationshipsInResult.isEmpty()) { return Optional.empty(); @@ -470,12 +509,12 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { for (Relationship possibleRelationship : allMatchingTypeRelationshipsInResult) { if (targetIdSelector.apply(possibleRelationship) == targetNodeId && sourceIdSelector.apply(possibleRelationship).equals(sourceNodeId)) { - Object mappedObject = map(possibleValueNode, values, concreteTargetNodeDescription, knownObjects, processedSegments); + Object mappedObject = map(possibleValueNode, values, concreteTargetNodeDescription, processedSegments); if (relationshipDescription.hasRelationshipProperties()) { Object relationshipProperties = map(possibleRelationship, allValues, (Neo4jPersistentEntity) relationshipDescription.getRelationshipPropertiesEntity(), - knownObjects, mappedObject, processedSegments); + mappedObject, processedSegments); relationshipsAndProperties.add(relationshipProperties); mappedObjectHandler.accept(possibleRelationship.type(), relationshipProperties); } else { @@ -489,17 +528,17 @@ final class DefaultNeo4jEntityConverter implements Neo4jEntityConverter { } else { for (Value relatedEntity : list.asList(Function.identity())) { - Object valueEntry = map(relatedEntity, allValues, concreteTargetNodeDescription, knownObjects, processedSegments); + Object valueEntry = map(relatedEntity, allValues, concreteTargetNodeDescription, processedSegments); if (relationshipDescription.hasRelationshipProperties()) { String relationshipSymbolicName = sourceLabel - + RelationshipDescription.NAME_OF_RELATIONSHIP + targetLabel; + + RelationshipDescription.NAME_OF_RELATIONSHIP + targetLabel; Relationship relatedEntityRelationship = relatedEntity.get(relationshipSymbolicName) .asRelationship(); Object relationshipProperties = map(relatedEntityRelationship, allValues, (Neo4jPersistentEntity) relationshipDescription.getRelationshipPropertiesEntity(), - knownObjects, valueEntry, processedSegments); + valueEntry, processedSegments); relationshipsAndProperties.add(relationshipProperties); mappedObjectHandler.accept(relatedEntity.get(RelationshipDescription.NAME_OF_RELATIONSHIP_TYPE).asString(), relationshipProperties); } else { diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/MappingSupport.java b/src/main/java/org/springframework/data/neo4j/core/mapping/MappingSupport.java index a89b282ec..bed05ae27 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/MappingSupport.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/MappingSupport.java @@ -19,9 +19,12 @@ import java.util.AbstractMap.SimpleEntry; import java.util.Collection; import java.util.Collections; import java.util.Map; +import java.util.function.Predicate; import java.util.stream.Collectors; import org.apiguardian.api.API; +import org.neo4j.driver.Value; +import org.neo4j.driver.types.Type; /** * @author Michael J. Simons @@ -60,6 +63,29 @@ public final class MappingSupport { return unifiedValue; } + /** + * A helper that produces a predicate to check whether a {@link Value} is a list value and contains only other + * values with a given type. + * + * @param collectionType The required collection type system + * @param requiredType The required type + * @return A predicate + */ + public static Predicate isListContainingOnly(Type collectionType, Type requiredType) { + + Predicate containsOnlyRequiredType = entry -> { + for (Value listEntry : entry.values()) { + if (!listEntry.hasType(requiredType)) { + return false; + } + } + return true; + }; + + Predicate isList = entry -> entry.hasType(collectionType); + return isList.and(containsOnlyRequiredType); + } + private MappingSupport() {} /** @@ -83,5 +109,4 @@ public final class MappingSupport { return relatedEntity; } } - } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java b/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java index 854df78f9..ea471ccba 100644 --- a/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/Neo4jMappingContext.java @@ -85,10 +85,7 @@ public final class Neo4jMappingContext extends AbstractMappingContext propMap = new HashMap<>(); // write relationship properties - entityConverter.write(relatedValue.getRelationshipProperties(), propMap); + getEntityConverter().write(relatedValue.getRelationshipProperties(), propMap); return new CreateRelationshipStatementHolder(relationshipCreationQuery, propMap); } diff --git a/src/main/java/org/springframework/data/neo4j/core/mapping/NoRootNodeMappingException.java b/src/main/java/org/springframework/data/neo4j/core/mapping/NoRootNodeMappingException.java new file mode 100644 index 000000000..10ecd2e2a --- /dev/null +++ b/src/main/java/org/springframework/data/neo4j/core/mapping/NoRootNodeMappingException.java @@ -0,0 +1,37 @@ +/* + * Copyright 2011-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.neo4j.core.mapping; + +import org.apiguardian.api.API; +import org.springframework.data.mapping.MappingException; + +/** + * A {@link NoRootNodeMappingException} is thrown when the entity converter cannot find a node or map like structure + * that can be mapped. + * Nodes eligible for mapping are actual nodes with at least the primarly label attached or exactly one map structure + * that is neither a node or relationship itself. + * + * @author Michael J. Simons + * @soundtrack Helge Schneider - Sammlung Schneider! Musik und Lifeshows! + * @since 6.0.2 + */ +@API(status = API.Status.INTERNAL, since = "6.0.2") +public final class NoRootNodeMappingException extends MappingException { + + public NoRootNodeMappingException(String s) { + super(s); + } +} 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 3c3e7def0..aa058c3bb 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 @@ -92,7 +92,8 @@ public interface Schema { if (nodeDescription == null) { throw new UnknownEntityException(targetClass); } - return (typeSystem, record) -> getEntityConverter().read(targetClass, record); + Neo4jEntityConverter entityConverter = getEntityConverter(); + return (typeSystem, record) -> entityConverter.read(targetClass, record); } /** @@ -106,9 +107,10 @@ public interface Schema { throw new UnknownEntityException(sourceClass); } + Neo4jEntityConverter entityConverter = getEntityConverter(); return t -> { Map parameters = new HashMap<>(); - getEntityConverter().write(t, parameters); + entityConverter.write(t, parameters); return parameters; }; } 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 429935a52..4d40de3d4 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 @@ -81,6 +81,8 @@ import org.springframework.data.neo4j.core.Neo4jTemplate; 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.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.AltHobby; import org.springframework.data.neo4j.integration.shared.common.AltLikedByPersonRelationship; @@ -449,7 +451,7 @@ class RepositoryIT { } @Test - void loadAllPersonsWithNoConstructor(@Autowired PersonRepository repository) { + void loadAllPersonsWithNoConstructor(@Autowired PersonWithNoConstructorRepository repository) { List persons = repository.getAllPersonsWithNoConstructorViaQuery(); @@ -458,7 +460,7 @@ class RepositoryIT { } @Test - void loadOnePersonWithNoConstructor(@Autowired PersonRepository repository) { + void loadOnePersonWithNoConstructor(@Autowired PersonWithNoConstructorRepository repository) { PersonWithNoConstructor person = repository.getOnePersonWithNoConstructorViaQuery(); assertThat(person.getName()).isEqualTo(TEST_PERSON1_NAME); @@ -466,7 +468,7 @@ class RepositoryIT { } @Test - void loadOptionalPersonWithNoConstructor(@Autowired PersonRepository repository) { + void loadOptionalPersonWithNoConstructor(@Autowired PersonWithNoConstructorRepository repository) { Optional person = repository.getOptionalPersonWithNoConstructorViaQuery(); assertThat(person).isPresent(); @@ -475,7 +477,7 @@ class RepositoryIT { } @Test - void loadAllPersonsWithWither(@Autowired PersonRepository repository) { + void loadAllPersonsWithWither(@Autowired PersonWithWitherRepository repository) { List persons = repository.getAllPersonsWithWitherViaQuery(); @@ -483,14 +485,14 @@ class RepositoryIT { } @Test - void loadOnePersonWithWither(@Autowired PersonRepository repository) { + void loadOnePersonWithWither(@Autowired PersonWithWitherRepository repository) { PersonWithWither person = repository.getOnePersonWithWitherViaQuery(); assertThat(person.getName()).isEqualTo(TEST_PERSON1_NAME); } @Test - void loadOptionalPersonWithWither(@Autowired PersonRepository repository) { + void loadOptionalPersonWithWither(@Autowired PersonWithWitherRepository repository) { Optional person = repository.getOptionalPersonWithWitherViaQuery(); assertThat(person).isPresent(); 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 b27190460..20b9ab2b4 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 @@ -37,8 +37,6 @@ import org.springframework.data.neo4j.integration.shared.common.DtoPersonProject import org.springframework.data.neo4j.integration.shared.common.DtoPersonProjectionContainingAdditionalFields; import org.springframework.data.neo4j.integration.shared.common.PersonProjection; 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.PersonWithWither; import org.springframework.data.neo4j.integration.shared.common.ThingWithGeneratedId; import org.springframework.data.neo4j.repository.Neo4jRepository; import org.springframework.data.neo4j.repository.query.BoundingBox; @@ -83,7 +81,7 @@ public interface PersonRepository extends Neo4jRepository getAllPersonsViaQuery(); - @Query("MATCH (n:UnknownLabel) return n") + @Query("MATCH (n) WHERE 1 = 2 return n") List getNobodyViaQuery(); @Query("MATCH (n:PersonWithAllConstructor{name:'Test'}) return n") @@ -102,24 +100,6 @@ public interface PersonRepository extends Neo4jRepository getOptionalPersonViaNamedQuery(@Param("part1") String part1, @Param("part2") String part2); - @Query("MATCH (n:PersonWithNoConstructor) return n") - List getAllPersonsWithNoConstructorViaQuery(); - - @Query("MATCH (n:PersonWithNoConstructor{name:'Test'}) return n") - PersonWithNoConstructor getOnePersonWithNoConstructorViaQuery(); - - @Query("MATCH (n:PersonWithNoConstructor{name:'Test'}) return n") - Optional getOptionalPersonWithNoConstructorViaQuery(); - - @Query("MATCH (n:PersonWithWither) return n") - List getAllPersonsWithWitherViaQuery(); - - @Query("MATCH (n:PersonWithWither{name:'Test'}) return n") - PersonWithWither getOnePersonWithWitherViaQuery(); - - @Query("MATCH (n:PersonWithWither{name:'Test'}) return n") - Optional getOptionalPersonWithWitherViaQuery(); - // Derived finders, should be extracted into another repo. Optional findOneByNameAndFirstName(String name, String firstName); @@ -139,11 +119,6 @@ public interface PersonRepository extends Neo4jRepository findAllByNameLike(String aName); - // TODO - // due to a needed bug fix in Spring Data commons commented because this will turn - // the repository in a reactive one - // CompletableFuture findOneByFirstName(String aName); - List findAllBySameValue(String sameValue); List findAllBySameValueIgnoreCase(String sameValue); diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/repositories/PersonWithNoConstructorRepository.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/repositories/PersonWithNoConstructorRepository.java new file mode 100644 index 000000000..a9617ec62 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/repositories/PersonWithNoConstructorRepository.java @@ -0,0 +1,38 @@ +/* + * Copyright 2011-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.neo4j.integration.imperative.repositories; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.neo4j.integration.shared.common.PersonWithNoConstructor; +import org.springframework.data.neo4j.repository.Neo4jRepository; +import org.springframework.data.neo4j.repository.query.Query; + +/** + * @author Michael J. Simons + */ +public interface PersonWithNoConstructorRepository extends Neo4jRepository { + + @Query("MATCH (n:PersonWithNoConstructor) return n") + List getAllPersonsWithNoConstructorViaQuery(); + + @Query("MATCH (n:PersonWithNoConstructor{name:'Test'}) return n") + PersonWithNoConstructor getOnePersonWithNoConstructorViaQuery(); + + @Query("MATCH (n:PersonWithNoConstructor{name:'Test'}) return n") + Optional getOptionalPersonWithNoConstructorViaQuery(); +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/imperative/repositories/PersonWithWitherRepository.java b/src/test/java/org/springframework/data/neo4j/integration/imperative/repositories/PersonWithWitherRepository.java new file mode 100644 index 000000000..7d3db90cd --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/imperative/repositories/PersonWithWitherRepository.java @@ -0,0 +1,39 @@ +/* + * Copyright 2011-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.neo4j.integration.imperative.repositories; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.neo4j.integration.shared.common.PersonWithWither; +import org.springframework.data.neo4j.repository.Neo4jRepository; +import org.springframework.data.neo4j.repository.query.Query; + +/** + * @author Michael J. Simons + */ +public interface PersonWithWitherRepository extends Neo4jRepository { + + @Query("MATCH (n:PersonWithWither) return n") + List getAllPersonsWithWitherViaQuery(); + + @Query("MATCH (n:PersonWithWither{name:'Test'}) return n") + PersonWithWither getOnePersonWithWitherViaQuery(); + + @Query("MATCH (n:PersonWithWither{name:'Test'}) return n") + Optional getOptionalPersonWithWitherViaQuery(); + +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/movies/Actor.java b/src/test/java/org/springframework/data/neo4j/integration/movies/Actor.java new file mode 100644 index 000000000..bf5358fb1 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/movies/Actor.java @@ -0,0 +1,59 @@ +/* + * Copyright 2011-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.neo4j.integration.movies; + +import java.util.Collections; +import java.util.List; + +import org.springframework.data.neo4j.core.schema.RelationshipProperties; +import org.springframework.data.neo4j.core.schema.TargetNode; + +/** + * @author Michael J. Simons + * @soundtrack Body Count - Manslaughter + */ +@RelationshipProperties +public final class Actor { + + @TargetNode + private final Person person; + + private final List roles; + + public Actor(Person person, List roles) { + this.person = person; + this.roles = roles; + } + + public Person getPerson() { + return person; + } + + public String getName() { + return person.getName(); + } + + public List getRoles() { + return Collections.unmodifiableList(roles); + } + + @Override public String toString() { + return "Actor{" + + "person=" + person + + ", roles=" + roles + + '}'; + } +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/movies/AdvancedMappingIT.java b/src/test/java/org/springframework/data/neo4j/integration/movies/AdvancedMappingIT.java new file mode 100644 index 000000000..883ce051a --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/movies/AdvancedMappingIT.java @@ -0,0 +1,167 @@ +/* + * Copyright 2011-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.neo4j.integration.movies; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.neo4j.driver.Driver; +import org.neo4j.driver.Session; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.neo4j.config.AbstractNeo4jConfig; +import org.springframework.data.neo4j.core.Neo4jTemplate; +import org.springframework.data.neo4j.test.Neo4jExtension; +import org.springframework.data.neo4j.test.Neo4jIntegrationTest; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +/** + * @author Michael J. Simons + * @soundtrack Body Count - Manslaughter + */ +@Neo4jIntegrationTest +class AdvancedMappingIT { + + protected static Neo4jExtension.Neo4jConnectionSupport neo4jConnectionSupport; + + @BeforeAll + static void setupData(@Autowired Driver driver) throws IOException { + + try (BufferedReader moviesReader = new BufferedReader( + new InputStreamReader(AdvancedMappingIT.class.getClass().getResourceAsStream("/data/movies.cypher"))); + Session session = driver.session()) { + session.run("MATCH (n) DETACH DELETE n"); + String moviesCypher = moviesReader.lines().collect(Collectors.joining(" ")); + session.run(moviesCypher); + } + } + + /** + * Here all paths are going into multiple records. Each path will be one record. The elements in the path will be + * seen as aggregated on the server side and each of the aggregates will also be aggregated. + * + * @param template Used for querying + */ + @Test // DATAGRAPH-1437 + void multiplePathsShouldWork(@Autowired Neo4jTemplate template) { + + Map parameters = new HashMap<>(); + parameters.put("person1", "Kevin Bacon"); + parameters.put("person2", "Angela Scope"); + String cypherQuery = + "MATCH allPaths=allShortestPathS((p1:Person {name: $person1})-[*]-(p2:Person {name: $person2}))\n" + + "RETURN allPaths"; + + List people = template.findAll(cypherQuery, parameters, Person.class); + assertThat(people).hasSize(7); + } + + /** + * Here all paths are going into one single record. + * + * @param template Used for querying + */ + @Test // DATAGRAPH-1437 + void multiplePreAggregatedPathsShouldWork(@Autowired Neo4jTemplate template) { + + Map parameters = new HashMap<>(); + parameters.put("person1", "Kevin Bacon"); + parameters.put("person2", "Angela Scope"); + String cypherQuery = + "MATCH allPaths=allShortestPathS((p1:Person {name: $person1})-[*]-(p2:Person {name: $person2}))\n" + + "RETURN collect(allPaths)"; + + List people = template.findAll(cypherQuery, parameters, Person.class); + assertThat(people).hasSize(7); + } + + /** + * This tests checks whether all nodes that fit a certain class along a path are mapped correctly. + * + * @param template Used for querying + */ + @Test // DATAGRAPH-1437 + void pathMappingWithoutAdditionalInformationShouldWork(@Autowired Neo4jTemplate template) { + + Map parameters = new HashMap<>(); + parameters.put("person1", "Kevin Bacon"); + parameters.put("person2", "Angela Scope"); + parameters.put("requiredMovie", "The Da Vinci Code"); + String cypherQuery = + "MATCH p=shortestPath((p1:Person {name: $person1})-[*]-(p2:Person {name: $person2}))\n" + + "WHERE size([n IN nodes(p) WHERE n.title = $requiredMovie]) > 0\n" + + "RETURN p"; + List people = template.findAll(cypherQuery, parameters, Person.class); + + assertThat(people) + .hasSize(4) + .extracting(Person::getName) + .contains("Kevin Bacon", "Jessica Thompson", + "Angela Scope"); // Two paths lead there, one with Ron Howard, one with Tom Hanks. + assertThat(people).element(2).extracting(Person::getReviewed) + .satisfies( + movies -> assertThat(movies).extracting(Movie::getTitle).containsExactly("The Da Vinci Code")); + } + + /** + * This tests checks whether all nodes that fit a certain class along a path are mapped correctly and if the + * additional joined information is applied as well. + * + * @param template Used for querying + */ + @Test // DATAGRAPH-1437 + void pathMappingWithAdditionalInformationShouldWork(@Autowired Neo4jTemplate template) { + Map parameters = new HashMap<>(); + parameters.put("person1", "Kevin Bacon"); + parameters.put("person2", "Meg Ryan"); + parameters.put("requiredMovie", "The Da Vinci Code"); + String cypherQuery = + "MATCH p=shortestPath(\n" + + "(p1:Person {name: $person1})-[*]-(p2:Person {name: $person2}))\n" + + "WITH p, [n in nodes(p) WHERE n:Movie] as mn\n" + + "UNWIND mn as m\n" + + "MATCH (m) <-[r:DIRECTED]- (d:Person)\n" + + "RETURN p, collect(r), collect(d)"; + List movies = template.findAll(cypherQuery, parameters, Movie.class); + + assertThat(movies) + .hasSize(2) + .allSatisfy(m -> assertThat(m.getDirectors()).isNotEmpty()) + .first() + .satisfies(m -> assertThat(m.getDirectors()).extracting(Person::getName) + .containsAnyOf("Ron Howard", "Rob Reiner")); + } + + @Configuration + @EnableTransactionManagement + static class Config extends AbstractNeo4jConfig { + + @Bean + public Driver driver() { + return neo4jConnectionSupport.getDriver(); + } + } +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/movies/Movie.java b/src/test/java/org/springframework/data/neo4j/integration/movies/Movie.java new file mode 100644 index 000000000..426364edf --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/movies/Movie.java @@ -0,0 +1,88 @@ +/* + * Copyright 2011-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.neo4j.integration.movies; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.neo4j.core.schema.Id; +import org.springframework.data.neo4j.core.schema.Node; +import org.springframework.data.neo4j.core.schema.Property; +import org.springframework.data.neo4j.core.schema.Relationship; +import org.springframework.data.neo4j.core.schema.Relationship.Direction; + +/** + * @author Michael J. Simons + * @soundtrack Body Count - Manslaughter + */ +@Node +public final class Movie { + + @Id + private final String title; + + @Property("tagline") + private final String description; + + @Relationship(value = "ACTED_IN", direction = Direction.INCOMING) + private final List actors; + + @Relationship(value = "DIRECTED", direction = Direction.INCOMING) + private final List directors; + + private Integer released; + + public Movie(String title, String description) { + this.title = title; + this.description = description; + this.actors = new ArrayList<>(); + this.directors = new ArrayList<>(); + } + + @PersistenceConstructor + public Movie(String title, String description, List actors, List directors) { + this.title = title; + this.description = description; + this.actors = actors == null ? Collections.emptyList() : new ArrayList<>(actors); + this.directors = directors == null ? Collections.emptyList() : new ArrayList<>(directors); + } + + public String getTitle() { + return title; + } + + public String getDescription() { + return description; + } + + public List getActors() { + return Collections.unmodifiableList(this.actors); + } + + public List getDirectors() { + return Collections.unmodifiableList(this.directors); + } + + public Integer getReleased() { + return released; + } + + public void setReleased(Integer released) { + this.released = released; + } +} diff --git a/src/test/java/org/springframework/data/neo4j/integration/movies/Person.java b/src/test/java/org/springframework/data/neo4j/integration/movies/Person.java new file mode 100644 index 000000000..66d04e181 --- /dev/null +++ b/src/test/java/org/springframework/data/neo4j/integration/movies/Person.java @@ -0,0 +1,84 @@ +/* + * Copyright 2011-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.neo4j.integration.movies; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.neo4j.core.schema.GeneratedValue; +import org.springframework.data.neo4j.core.schema.Id; +import org.springframework.data.neo4j.core.schema.Node; +import org.springframework.data.neo4j.core.schema.Relationship; + +/** + * @author Michael J. Simons + * @soundtrack Body Count - Manslaughter + */ +@Node +public final class Person { + + @Id @GeneratedValue + private final Long id; + + private final String name; + + private Integer born; + + @Relationship("REVIEWED") + private List reviewed = new ArrayList<>(); + + @PersistenceConstructor + private Person(Long id, String name, Integer born) { + this.id = id; + this.born = born; + this.name = name; + } + + public Person(String name, Integer born) { + this(null, name, born); + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public Integer getBorn() { + return born; + } + + public void setBorn(Integer born) { + this.born = born; + } + + public List getReviewed() { + return reviewed; + } + + + + @Override public String toString() { + return "Person{" + + "id=" + id + + ", name='" + name + '\'' + + ", born=" + born + + '}'; + } +} diff --git a/src/test/resources/data/movies.cypher b/src/test/resources/data/movies.cypher new file mode 100644 index 000000000..b45c37679 --- /dev/null +++ b/src/test/resources/data/movies.cypher @@ -0,0 +1,508 @@ +CREATE (TheMatrix:Movie {title:'The Matrix', released:1999, tagline:'Welcome to the Real World'}) +CREATE (Keanu:Person {name:'Keanu Reeves', born:1964}) +CREATE (Carrie:Person {name:'Carrie-Anne Moss', born:1967}) +CREATE (Laurence:Person {name:'Laurence Fishburne', born:1961}) +CREATE (Hugo:Person {name:'Hugo Weaving', born:1960}) +CREATE (LillyW:Person {name:'Lilly Wachowski', born:1967}) +CREATE (LanaW:Person {name:'Lana Wachowski', born:1965}) +CREATE (JoelS:Person {name:'Joel Silver', born:1952}) +CREATE +(Keanu)-[:ACTED_IN {roles:['Neo']}]->(TheMatrix), +(Carrie)-[:ACTED_IN {roles:['Trinity']}]->(TheMatrix), +(Laurence)-[:ACTED_IN {roles:['Morpheus']}]->(TheMatrix), +(Hugo)-[:ACTED_IN {roles:['Agent Smith']}]->(TheMatrix), +(LillyW)-[:DIRECTED]->(TheMatrix), +(LanaW)-[:DIRECTED]->(TheMatrix), +(JoelS)-[:PRODUCED]->(TheMatrix) + +CREATE (Emil:Person {name:"Emil Eifrem", born:1978}) +CREATE (Emil)-[:ACTED_IN {roles:["Emil"]}]->(TheMatrix) + +CREATE (TheMatrixReloaded:Movie {title:'The Matrix Reloaded', released:2003, tagline:'Free your mind'}) +CREATE +(Keanu)-[:ACTED_IN {roles:['Neo']}]->(TheMatrixReloaded), +(Carrie)-[:ACTED_IN {roles:['Trinity']}]->(TheMatrixReloaded), +(Laurence)-[:ACTED_IN {roles:['Morpheus']}]->(TheMatrixReloaded), +(Hugo)-[:ACTED_IN {roles:['Agent Smith']}]->(TheMatrixReloaded), +(LillyW)-[:DIRECTED]->(TheMatrixReloaded), +(LanaW)-[:DIRECTED]->(TheMatrixReloaded), +(JoelS)-[:PRODUCED]->(TheMatrixReloaded) + +CREATE (TheMatrixRevolutions:Movie {title:'The Matrix Revolutions', released:2003, tagline:'Everything that has a beginning has an end'}) +CREATE +(Keanu)-[:ACTED_IN {roles:['Neo']}]->(TheMatrixRevolutions), +(Carrie)-[:ACTED_IN {roles:['Trinity']}]->(TheMatrixRevolutions), +(Laurence)-[:ACTED_IN {roles:['Morpheus']}]->(TheMatrixRevolutions), +(Hugo)-[:ACTED_IN {roles:['Agent Smith']}]->(TheMatrixRevolutions), +(LillyW)-[:DIRECTED]->(TheMatrixRevolutions), +(LanaW)-[:DIRECTED]->(TheMatrixRevolutions), +(JoelS)-[:PRODUCED]->(TheMatrixRevolutions) + +CREATE (TheDevilsAdvocate:Movie {title:"The Devil's Advocate", released:1997, tagline:'Evil has its winning ways'}) +CREATE (Charlize:Person {name:'Charlize Theron', born:1975}) +CREATE (Al:Person {name:'Al Pacino', born:1940}) +CREATE (Taylor:Person {name:'Taylor Hackford', born:1944}) +CREATE +(Keanu)-[:ACTED_IN {roles:['Kevin Lomax']}]->(TheDevilsAdvocate), +(Charlize)-[:ACTED_IN {roles:['Mary Ann Lomax']}]->(TheDevilsAdvocate), +(Al)-[:ACTED_IN {roles:['John Milton']}]->(TheDevilsAdvocate), +(Taylor)-[:DIRECTED]->(TheDevilsAdvocate) + +CREATE (AFewGoodMen:Movie {title:"A Few Good Men", released:1992, tagline:"In the heart of the nation's capital, in a courthouse of the U.S. government, one man will stop at nothing to keep his honor, and one will stop at nothing to find the truth."}) +CREATE (TomC:Person {name:'Tom Cruise', born:1962}) +CREATE (JackN:Person {name:'Jack Nicholson', born:1937}) +CREATE (DemiM:Person {name:'Demi Moore', born:1962}) +CREATE (KevinB:Person {name:'Kevin Bacon', born:1958}) +CREATE (KieferS:Person {name:'Kiefer Sutherland', born:1966}) +CREATE (NoahW:Person {name:'Noah Wyle', born:1971}) +CREATE (CubaG:Person {name:'Cuba Gooding Jr.', born:1968}) +CREATE (KevinP:Person {name:'Kevin Pollak', born:1957}) +CREATE (JTW:Person {name:'J.T. Walsh', born:1943}) +CREATE (JamesM:Person {name:'James Marshall', born:1967}) +CREATE (ChristopherG:Person {name:'Christopher Guest', born:1948}) +CREATE (RobR:Person {name:'Rob Reiner', born:1947}) +CREATE (AaronS:Person {name:'Aaron Sorkin', born:1961}) +CREATE +(TomC)-[:ACTED_IN {roles:['Lt. Daniel Kaffee']}]->(AFewGoodMen), +(JackN)-[:ACTED_IN {roles:['Col. Nathan R. Jessup']}]->(AFewGoodMen), +(DemiM)-[:ACTED_IN {roles:['Lt. Cdr. JoAnne Galloway']}]->(AFewGoodMen), +(KevinB)-[:ACTED_IN {roles:['Capt. Jack Ross']}]->(AFewGoodMen), +(KieferS)-[:ACTED_IN {roles:['Lt. Jonathan Kendrick']}]->(AFewGoodMen), +(NoahW)-[:ACTED_IN {roles:['Cpl. Jeffrey Barnes']}]->(AFewGoodMen), +(CubaG)-[:ACTED_IN {roles:['Cpl. Carl Hammaker']}]->(AFewGoodMen), +(KevinP)-[:ACTED_IN {roles:['Lt. Sam Weinberg']}]->(AFewGoodMen), +(JTW)-[:ACTED_IN {roles:['Lt. Col. Matthew Andrew Markinson']}]->(AFewGoodMen), +(JamesM)-[:ACTED_IN {roles:['Pfc. Louden Downey']}]->(AFewGoodMen), +(ChristopherG)-[:ACTED_IN {roles:['Dr. Stone']}]->(AFewGoodMen), +(AaronS)-[:ACTED_IN {roles:['Man in Bar']}]->(AFewGoodMen), +(RobR)-[:DIRECTED]->(AFewGoodMen), +(AaronS)-[:WROTE]->(AFewGoodMen) + +CREATE (TopGun:Movie {title:"Top Gun", released:1986, tagline:'I feel the need, the need for speed.'}) +CREATE (KellyM:Person {name:'Kelly McGillis', born:1957}) +CREATE (ValK:Person {name:'Val Kilmer', born:1959}) +CREATE (AnthonyE:Person {name:'Anthony Edwards', born:1962}) +CREATE (TomS:Person {name:'Tom Skerritt', born:1933}) +CREATE (MegR:Person {name:'Meg Ryan', born:1961}) +CREATE (TonyS:Person {name:'Tony Scott', born:1944}) +CREATE (JimC:Person {name:'Jim Cash', born:1941}) +CREATE +(TomC)-[:ACTED_IN {roles:['Maverick']}]->(TopGun), +(KellyM)-[:ACTED_IN {roles:['Charlie']}]->(TopGun), +(ValK)-[:ACTED_IN {roles:['Iceman']}]->(TopGun), +(AnthonyE)-[:ACTED_IN {roles:['Goose']}]->(TopGun), +(TomS)-[:ACTED_IN {roles:['Viper']}]->(TopGun), +(MegR)-[:ACTED_IN {roles:['Carole']}]->(TopGun), +(TonyS)-[:DIRECTED]->(TopGun), +(JimC)-[:WROTE]->(TopGun) + +CREATE (JerryMaguire:Movie {title:'Jerry Maguire', released:2000, tagline:'The rest of his life begins now.'}) +CREATE (ReneeZ:Person {name:'Renee Zellweger', born:1969}) +CREATE (KellyP:Person {name:'Kelly Preston', born:1962}) +CREATE (JerryO:Person {name:"Jerry O'Connell", born:1974}) +CREATE (JayM:Person {name:'Jay Mohr', born:1970}) +CREATE (BonnieH:Person {name:'Bonnie Hunt', born:1961}) +CREATE (ReginaK:Person {name:'Regina King', born:1971}) +CREATE (JonathanL:Person {name:'Jonathan Lipnicki', born:1996}) +CREATE (CameronC:Person {name:'Cameron Crowe', born:1957}) +CREATE +(TomC)-[:ACTED_IN {roles:['Jerry Maguire']}]->(JerryMaguire), +(CubaG)-[:ACTED_IN {roles:['Rod Tidwell']}]->(JerryMaguire), +(ReneeZ)-[:ACTED_IN {roles:['Dorothy Boyd']}]->(JerryMaguire), +(KellyP)-[:ACTED_IN {roles:['Avery Bishop']}]->(JerryMaguire), +(JerryO)-[:ACTED_IN {roles:['Frank Cushman']}]->(JerryMaguire), +(JayM)-[:ACTED_IN {roles:['Bob Sugar']}]->(JerryMaguire), +(BonnieH)-[:ACTED_IN {roles:['Laurel Boyd']}]->(JerryMaguire), +(ReginaK)-[:ACTED_IN {roles:['Marcee Tidwell']}]->(JerryMaguire), +(JonathanL)-[:ACTED_IN {roles:['Ray Boyd']}]->(JerryMaguire), +(CameronC)-[:DIRECTED]->(JerryMaguire), +(CameronC)-[:PRODUCED]->(JerryMaguire), +(CameronC)-[:WROTE]->(JerryMaguire) + +CREATE (StandByMe:Movie {title:"Stand By Me", released:1986, tagline:"For some, it's the last real taste of innocence, and the first real taste of life. But for everyone, it's the time that memories are made of."}) +CREATE (RiverP:Person {name:'River Phoenix', born:1970}) +CREATE (CoreyF:Person {name:'Corey Feldman', born:1971}) +CREATE (WilW:Person {name:'Wil Wheaton', born:1972}) +CREATE (JohnC:Person {name:'John Cusack', born:1966}) +CREATE (MarshallB:Person {name:'Marshall Bell', born:1942}) +CREATE +(WilW)-[:ACTED_IN {roles:['Gordie Lachance']}]->(StandByMe), +(RiverP)-[:ACTED_IN {roles:['Chris Chambers']}]->(StandByMe), +(JerryO)-[:ACTED_IN {roles:['Vern Tessio']}]->(StandByMe), +(CoreyF)-[:ACTED_IN {roles:['Teddy Duchamp']}]->(StandByMe), +(JohnC)-[:ACTED_IN {roles:['Denny Lachance']}]->(StandByMe), +(KieferS)-[:ACTED_IN {roles:['Ace Merrill']}]->(StandByMe), +(MarshallB)-[:ACTED_IN {roles:['Mr. Lachance']}]->(StandByMe), +(RobR)-[:DIRECTED]->(StandByMe) + +CREATE (AsGoodAsItGets:Movie {title:'As Good as It Gets', released:1997, tagline:'A comedy from the heart that goes for the throat.'}) +CREATE (HelenH:Person {name:'Helen Hunt', born:1963}) +CREATE (GregK:Person {name:'Greg Kinnear', born:1963}) +CREATE (JamesB:Person {name:'James L. Brooks', born:1940}) +CREATE +(JackN)-[:ACTED_IN {roles:['Melvin Udall']}]->(AsGoodAsItGets), +(HelenH)-[:ACTED_IN {roles:['Carol Connelly']}]->(AsGoodAsItGets), +(GregK)-[:ACTED_IN {roles:['Simon Bishop']}]->(AsGoodAsItGets), +(CubaG)-[:ACTED_IN {roles:['Frank Sachs']}]->(AsGoodAsItGets), +(JamesB)-[:DIRECTED]->(AsGoodAsItGets) + +CREATE (WhatDreamsMayCome:Movie {title:'What Dreams May Come', released:1998, tagline:'After life there is more. The end is just the beginning.'}) +CREATE (AnnabellaS:Person {name:'Annabella Sciorra', born:1960}) +CREATE (MaxS:Person {name:'Max von Sydow', born:1929}) +CREATE (WernerH:Person {name:'Werner Herzog', born:1942}) +CREATE (Robin:Person {name:'Robin Williams', born:1951}) +CREATE (VincentW:Person {name:'Vincent Ward', born:1956}) +CREATE +(Robin)-[:ACTED_IN {roles:['Chris Nielsen']}]->(WhatDreamsMayCome), +(CubaG)-[:ACTED_IN {roles:['Albert Lewis']}]->(WhatDreamsMayCome), +(AnnabellaS)-[:ACTED_IN {roles:['Annie Collins-Nielsen']}]->(WhatDreamsMayCome), +(MaxS)-[:ACTED_IN {roles:['The Tracker']}]->(WhatDreamsMayCome), +(WernerH)-[:ACTED_IN {roles:['The Face']}]->(WhatDreamsMayCome), +(VincentW)-[:DIRECTED]->(WhatDreamsMayCome) + +CREATE (SnowFallingonCedars:Movie {title:'Snow Falling on Cedars', released:1999, tagline:'First loves last. Forever.'}) +CREATE (EthanH:Person {name:'Ethan Hawke', born:1970}) +CREATE (RickY:Person {name:'Rick Yune', born:1971}) +CREATE (JamesC:Person {name:'James Cromwell', born:1940}) +CREATE (ScottH:Person {name:'Scott Hicks', born:1953}) +CREATE +(EthanH)-[:ACTED_IN {roles:['Ishmael Chambers']}]->(SnowFallingonCedars), +(RickY)-[:ACTED_IN {roles:['Kazuo Miyamoto']}]->(SnowFallingonCedars), +(MaxS)-[:ACTED_IN {roles:['Nels Gudmundsson']}]->(SnowFallingonCedars), +(JamesC)-[:ACTED_IN {roles:['Judge Fielding']}]->(SnowFallingonCedars), +(ScottH)-[:DIRECTED]->(SnowFallingonCedars) + +CREATE (YouveGotMail:Movie {title:"You've Got Mail", released:1998, tagline:'At odds in life... in love on-line.'}) +CREATE (ParkerP:Person {name:'Parker Posey', born:1968}) +CREATE (DaveC:Person {name:'Dave Chappelle', born:1973}) +CREATE (SteveZ:Person {name:'Steve Zahn', born:1967}) +CREATE (TomH:Person {name:'Tom Hanks', born:1956}) +CREATE (NoraE:Person {name:'Nora Ephron', born:1941}) +CREATE +(TomH)-[:ACTED_IN {roles:['Joe Fox']}]->(YouveGotMail), +(MegR)-[:ACTED_IN {roles:['Kathleen Kelly']}]->(YouveGotMail), +(GregK)-[:ACTED_IN {roles:['Frank Navasky']}]->(YouveGotMail), +(ParkerP)-[:ACTED_IN {roles:['Patricia Eden']}]->(YouveGotMail), +(DaveC)-[:ACTED_IN {roles:['Kevin Jackson']}]->(YouveGotMail), +(SteveZ)-[:ACTED_IN {roles:['George Pappas']}]->(YouveGotMail), +(NoraE)-[:DIRECTED]->(YouveGotMail) + +CREATE (SleeplessInSeattle:Movie {title:'Sleepless in Seattle', released:1993, tagline:'What if someone you never met, someone you never saw, someone you never knew was the only someone for you?'}) +CREATE (RitaW:Person {name:'Rita Wilson', born:1956}) +CREATE (BillPull:Person {name:'Bill Pullman', born:1953}) +CREATE (VictorG:Person {name:'Victor Garber', born:1949}) +CREATE (RosieO:Person {name:"Rosie O'Donnell", born:1962}) +CREATE +(TomH)-[:ACTED_IN {roles:['Sam Baldwin']}]->(SleeplessInSeattle), +(MegR)-[:ACTED_IN {roles:['Annie Reed']}]->(SleeplessInSeattle), +(RitaW)-[:ACTED_IN {roles:['Suzy']}]->(SleeplessInSeattle), +(BillPull)-[:ACTED_IN {roles:['Walter']}]->(SleeplessInSeattle), +(VictorG)-[:ACTED_IN {roles:['Greg']}]->(SleeplessInSeattle), +(RosieO)-[:ACTED_IN {roles:['Becky']}]->(SleeplessInSeattle), +(NoraE)-[:DIRECTED]->(SleeplessInSeattle) + +CREATE (JoeVersustheVolcano:Movie {title:'Joe Versus the Volcano', released:1990, tagline:'A story of love, lava and burning desire.'}) +CREATE (JohnS:Person {name:'John Patrick Stanley', born:1950}) +CREATE (Nathan:Person {name:'Nathan Lane', born:1956}) +CREATE +(TomH)-[:ACTED_IN {roles:['Joe Banks']}]->(JoeVersustheVolcano), +(MegR)-[:ACTED_IN {roles:['DeDe', 'Angelica Graynamore', 'Patricia Graynamore']}]->(JoeVersustheVolcano), +(Nathan)-[:ACTED_IN {roles:['Baw']}]->(JoeVersustheVolcano), +(JohnS)-[:DIRECTED]->(JoeVersustheVolcano) + +CREATE (WhenHarryMetSally:Movie {title:'When Harry Met Sally', released:1998, tagline:'Can two friends sleep together and still love each other in the morning?'}) +CREATE (BillyC:Person {name:'Billy Crystal', born:1948}) +CREATE (CarrieF:Person {name:'Carrie Fisher', born:1956}) +CREATE (BrunoK:Person {name:'Bruno Kirby', born:1949}) +CREATE +(BillyC)-[:ACTED_IN {roles:['Harry Burns']}]->(WhenHarryMetSally), +(MegR)-[:ACTED_IN {roles:['Sally Albright']}]->(WhenHarryMetSally), +(CarrieF)-[:ACTED_IN {roles:['Marie']}]->(WhenHarryMetSally), +(BrunoK)-[:ACTED_IN {roles:['Jess']}]->(WhenHarryMetSally), +(RobR)-[:DIRECTED]->(WhenHarryMetSally), +(RobR)-[:PRODUCED]->(WhenHarryMetSally), +(NoraE)-[:PRODUCED]->(WhenHarryMetSally), +(NoraE)-[:WROTE]->(WhenHarryMetSally) + +CREATE (ThatThingYouDo:Movie {title:'That Thing You Do', released:1996, tagline:'In every life there comes a time when that thing you dream becomes that thing you do'}) +CREATE (LivT:Person {name:'Liv Tyler', born:1977}) +CREATE +(TomH)-[:ACTED_IN {roles:['Mr. White']}]->(ThatThingYouDo), +(LivT)-[:ACTED_IN {roles:['Faye Dolan']}]->(ThatThingYouDo), +(Charlize)-[:ACTED_IN {roles:['Tina']}]->(ThatThingYouDo), +(TomH)-[:DIRECTED]->(ThatThingYouDo) + +CREATE (TheReplacements:Movie {title:'The Replacements', released:2000, tagline:'Pain heals, Chicks dig scars... Glory lasts forever'}) +CREATE (Brooke:Person {name:'Brooke Langton', born:1970}) +CREATE (Gene:Person {name:'Gene Hackman', born:1930}) +CREATE (Orlando:Person {name:'Orlando Jones', born:1968}) +CREATE (Howard:Person {name:'Howard Deutch', born:1950}) +CREATE +(Keanu)-[:ACTED_IN {roles:['Shane Falco']}]->(TheReplacements), +(Brooke)-[:ACTED_IN {roles:['Annabelle Farrell']}]->(TheReplacements), +(Gene)-[:ACTED_IN {roles:['Jimmy McGinty']}]->(TheReplacements), +(Orlando)-[:ACTED_IN {roles:['Clifford Franklin']}]->(TheReplacements), +(Howard)-[:DIRECTED]->(TheReplacements) + +CREATE (RescueDawn:Movie {title:'RescueDawn', released:2006, tagline:"Based on the extraordinary true story of one man's fight for freedom"}) +CREATE (ChristianB:Person {name:'Christian Bale', born:1974}) +CREATE (ZachG:Person {name:'Zach Grenier', born:1954}) +CREATE +(MarshallB)-[:ACTED_IN {roles:['Admiral']}]->(RescueDawn), +(ChristianB)-[:ACTED_IN {roles:['Dieter Dengler']}]->(RescueDawn), +(ZachG)-[:ACTED_IN {roles:['Squad Leader']}]->(RescueDawn), +(SteveZ)-[:ACTED_IN {roles:['Duane']}]->(RescueDawn), +(WernerH)-[:DIRECTED]->(RescueDawn) + +CREATE (TheBirdcage:Movie {title:'The Birdcage', released:1996, tagline:'Come as you are'}) +CREATE (MikeN:Person {name:'Mike Nichols', born:1931}) +CREATE +(Robin)-[:ACTED_IN {roles:['Armand Goldman']}]->(TheBirdcage), +(Nathan)-[:ACTED_IN {roles:['Albert Goldman']}]->(TheBirdcage), +(Gene)-[:ACTED_IN {roles:['Sen. Kevin Keeley']}]->(TheBirdcage), +(MikeN)-[:DIRECTED]->(TheBirdcage) + +CREATE (Unforgiven:Movie {title:'Unforgiven', released:1992, tagline:"It's a hell of a thing, killing a man"}) +CREATE (RichardH:Person {name:'Richard Harris', born:1930}) +CREATE (ClintE:Person {name:'Clint Eastwood', born:1930}) +CREATE +(RichardH)-[:ACTED_IN {roles:['English Bob']}]->(Unforgiven), +(ClintE)-[:ACTED_IN {roles:['Bill Munny']}]->(Unforgiven), +(Gene)-[:ACTED_IN {roles:['Little Bill Daggett']}]->(Unforgiven), +(ClintE)-[:DIRECTED]->(Unforgiven) + +CREATE (JohnnyMnemonic:Movie {title:'Johnny Mnemonic', released:1995, tagline:'The hottest data on earth. In the coolest head in town'}) +CREATE (Takeshi:Person {name:'Takeshi Kitano', born:1947}) +CREATE (Dina:Person {name:'Dina Meyer', born:1968}) +CREATE (IceT:Person {name:'Ice-T', born:1958}) +CREATE (RobertL:Person {name:'Robert Longo', born:1953}) +CREATE +(Keanu)-[:ACTED_IN {roles:['Johnny Mnemonic']}]->(JohnnyMnemonic), +(Takeshi)-[:ACTED_IN {roles:['Takahashi']}]->(JohnnyMnemonic), +(Dina)-[:ACTED_IN {roles:['Jane']}]->(JohnnyMnemonic), +(IceT)-[:ACTED_IN {roles:['J-Bone']}]->(JohnnyMnemonic), +(RobertL)-[:DIRECTED]->(JohnnyMnemonic) + +CREATE (CloudAtlas:Movie {title:'Cloud Atlas', released:2012, tagline:'Everything is connected'}) +CREATE (HalleB:Person {name:'Halle Berry', born:1966}) +CREATE (JimB:Person {name:'Jim Broadbent', born:1949}) +CREATE (TomT:Person {name:'Tom Tykwer', born:1965}) +CREATE (DavidMitchell:Person {name:'David Mitchell', born:1969}) +CREATE (StefanArndt:Person {name:'Stefan Arndt', born:1961}) +CREATE +(TomH)-[:ACTED_IN {roles:['Zachry', 'Dr. Henry Goose', 'Isaac Sachs', 'Dermot Hoggins']}]->(CloudAtlas), +(Hugo)-[:ACTED_IN {roles:['Bill Smoke', 'Haskell Moore', 'Tadeusz Kesselring', 'Nurse Noakes', 'Boardman Mephi', 'Old Georgie']}]->(CloudAtlas), +(HalleB)-[:ACTED_IN {roles:['Luisa Rey', 'Jocasta Ayrs', 'Ovid', 'Meronym']}]->(CloudAtlas), +(JimB)-[:ACTED_IN {roles:['Vyvyan Ayrs', 'Captain Molyneux', 'Timothy Cavendish']}]->(CloudAtlas), +(TomT)-[:DIRECTED]->(CloudAtlas), +(LillyW)-[:DIRECTED]->(CloudAtlas), +(LanaW)-[:DIRECTED]->(CloudAtlas), +(DavidMitchell)-[:WROTE]->(CloudAtlas), +(StefanArndt)-[:PRODUCED]->(CloudAtlas) + +CREATE (TheDaVinciCode:Movie {title:'The Da Vinci Code', released:2006, tagline:'Break The Codes'}) +CREATE (IanM:Person {name:'Ian McKellen', born:1939}) +CREATE (AudreyT:Person {name:'Audrey Tautou', born:1976}) +CREATE (PaulB:Person {name:'Paul Bettany', born:1971}) +CREATE (RonH:Person {name:'Ron Howard', born:1954}) +CREATE +(TomH)-[:ACTED_IN {roles:['Dr. Robert Langdon']}]->(TheDaVinciCode), +(IanM)-[:ACTED_IN {roles:['Sir Leight Teabing']}]->(TheDaVinciCode), +(AudreyT)-[:ACTED_IN {roles:['Sophie Neveu']}]->(TheDaVinciCode), +(PaulB)-[:ACTED_IN {roles:['Silas']}]->(TheDaVinciCode), +(RonH)-[:DIRECTED]->(TheDaVinciCode) + +CREATE (VforVendetta:Movie {title:'V for Vendetta', released:2006, tagline:'Freedom! Forever!'}) +CREATE (NatalieP:Person {name:'Natalie Portman', born:1981}) +CREATE (StephenR:Person {name:'Stephen Rea', born:1946}) +CREATE (JohnH:Person {name:'John Hurt', born:1940}) +CREATE (BenM:Person {name: 'Ben Miles', born:1967}) +CREATE +(Hugo)-[:ACTED_IN {roles:['V']}]->(VforVendetta), +(NatalieP)-[:ACTED_IN {roles:['Evey Hammond']}]->(VforVendetta), +(StephenR)-[:ACTED_IN {roles:['Eric Finch']}]->(VforVendetta), +(JohnH)-[:ACTED_IN {roles:['High Chancellor Adam Sutler']}]->(VforVendetta), +(BenM)-[:ACTED_IN {roles:['Dascomb']}]->(VforVendetta), +(JamesM)-[:DIRECTED]->(VforVendetta), +(LillyW)-[:PRODUCED]->(VforVendetta), +(LanaW)-[:PRODUCED]->(VforVendetta), +(JoelS)-[:PRODUCED]->(VforVendetta), +(LillyW)-[:WROTE]->(VforVendetta), +(LanaW)-[:WROTE]->(VforVendetta) + +CREATE (SpeedRacer:Movie {title:'Speed Racer', released:2008, tagline:'Speed has no limits'}) +CREATE (EmileH:Person {name:'Emile Hirsch', born:1985}) +CREATE (JohnG:Person {name:'John Goodman', born:1960}) +CREATE (SusanS:Person {name:'Susan Sarandon', born:1946}) +CREATE (MatthewF:Person {name:'Matthew Fox', born:1966}) +CREATE (ChristinaR:Person {name:'Christina Ricci', born:1980}) +CREATE (Rain:Person {name:'Rain', born:1982}) +CREATE +(EmileH)-[:ACTED_IN {roles:['Speed Racer']}]->(SpeedRacer), +(JohnG)-[:ACTED_IN {roles:['Pops']}]->(SpeedRacer), +(SusanS)-[:ACTED_IN {roles:['Mom']}]->(SpeedRacer), +(MatthewF)-[:ACTED_IN {roles:['Racer X']}]->(SpeedRacer), +(ChristinaR)-[:ACTED_IN {roles:['Trixie']}]->(SpeedRacer), +(Rain)-[:ACTED_IN {roles:['Taejo Togokahn']}]->(SpeedRacer), +(BenM)-[:ACTED_IN {roles:['Cass Jones']}]->(SpeedRacer), +(LillyW)-[:DIRECTED]->(SpeedRacer), +(LanaW)-[:DIRECTED]->(SpeedRacer), +(LillyW)-[:WROTE]->(SpeedRacer), +(LanaW)-[:WROTE]->(SpeedRacer), +(JoelS)-[:PRODUCED]->(SpeedRacer) + +CREATE (NinjaAssassin:Movie {title:'Ninja Assassin', released:2009, tagline:'Prepare to enter a secret world of assassins'}) +CREATE (NaomieH:Person {name:'Naomie Harris'}) +CREATE +(Rain)-[:ACTED_IN {roles:['Raizo']}]->(NinjaAssassin), +(NaomieH)-[:ACTED_IN {roles:['Mika Coretti']}]->(NinjaAssassin), +(RickY)-[:ACTED_IN {roles:['Takeshi']}]->(NinjaAssassin), +(BenM)-[:ACTED_IN {roles:['Ryan Maslow']}]->(NinjaAssassin), +(JamesM)-[:DIRECTED]->(NinjaAssassin), +(LillyW)-[:PRODUCED]->(NinjaAssassin), +(LanaW)-[:PRODUCED]->(NinjaAssassin), +(JoelS)-[:PRODUCED]->(NinjaAssassin) + +CREATE (TheGreenMile:Movie {title:'The Green Mile', released:1999, tagline:"Walk a mile you'll never forget."}) +CREATE (MichaelD:Person {name:'Michael Clarke Duncan', born:1957}) +CREATE (DavidM:Person {name:'David Morse', born:1953}) +CREATE (SamR:Person {name:'Sam Rockwell', born:1968}) +CREATE (GaryS:Person {name:'Gary Sinise', born:1955}) +CREATE (PatriciaC:Person {name:'Patricia Clarkson', born:1959}) +CREATE (FrankD:Person {name:'Frank Darabont', born:1959}) +CREATE +(TomH)-[:ACTED_IN {roles:['Paul Edgecomb']}]->(TheGreenMile), +(MichaelD)-[:ACTED_IN {roles:['John Coffey']}]->(TheGreenMile), +(DavidM)-[:ACTED_IN {roles:['Brutus "Brutal" Howell']}]->(TheGreenMile), +(BonnieH)-[:ACTED_IN {roles:['Jan Edgecomb']}]->(TheGreenMile), +(JamesC)-[:ACTED_IN {roles:['Warden Hal Moores']}]->(TheGreenMile), +(SamR)-[:ACTED_IN {roles:['"Wild Bill" Wharton']}]->(TheGreenMile), +(GaryS)-[:ACTED_IN {roles:['Burt Hammersmith']}]->(TheGreenMile), +(PatriciaC)-[:ACTED_IN {roles:['Melinda Moores']}]->(TheGreenMile), +(FrankD)-[:DIRECTED]->(TheGreenMile) + +CREATE (FrostNixon:Movie {title:'Frost/Nixon', released:2008, tagline:'400 million people were waiting for the truth.'}) +CREATE (FrankL:Person {name:'Frank Langella', born:1938}) +CREATE (MichaelS:Person {name:'Michael Sheen', born:1969}) +CREATE (OliverP:Person {name:'Oliver Platt', born:1960}) +CREATE +(FrankL)-[:ACTED_IN {roles:['Richard Nixon']}]->(FrostNixon), +(MichaelS)-[:ACTED_IN {roles:['David Frost']}]->(FrostNixon), +(KevinB)-[:ACTED_IN {roles:['Jack Brennan']}]->(FrostNixon), +(OliverP)-[:ACTED_IN {roles:['Bob Zelnick']}]->(FrostNixon), +(SamR)-[:ACTED_IN {roles:['James Reston, Jr.']}]->(FrostNixon), +(RonH)-[:DIRECTED]->(FrostNixon) + +CREATE (Hoffa:Movie {title:'Hoffa', released:1992, tagline:"He didn't want law. He wanted justice."}) +CREATE (DannyD:Person {name:'Danny DeVito', born:1944}) +CREATE (JohnR:Person {name:'John C. Reilly', born:1965}) +CREATE +(JackN)-[:ACTED_IN {roles:['Hoffa']}]->(Hoffa), +(DannyD)-[:ACTED_IN {roles:['Robert "Bobby" Ciaro']}]->(Hoffa), +(JTW)-[:ACTED_IN {roles:['Frank Fitzsimmons']}]->(Hoffa), +(JohnR)-[:ACTED_IN {roles:['Peter "Pete" Connelly']}]->(Hoffa), +(DannyD)-[:DIRECTED]->(Hoffa) + +CREATE (Apollo13:Movie {title:'Apollo 13', released:1995, tagline:'Houston, we have a problem.'}) +CREATE (EdH:Person {name:'Ed Harris', born:1950}) +CREATE (BillPax:Person {name:'Bill Paxton', born:1955}) +CREATE +(TomH)-[:ACTED_IN {roles:['Jim Lovell']}]->(Apollo13), +(KevinB)-[:ACTED_IN {roles:['Jack Swigert']}]->(Apollo13), +(EdH)-[:ACTED_IN {roles:['Gene Kranz']}]->(Apollo13), +(BillPax)-[:ACTED_IN {roles:['Fred Haise']}]->(Apollo13), +(GaryS)-[:ACTED_IN {roles:['Ken Mattingly']}]->(Apollo13), +(RonH)-[:DIRECTED]->(Apollo13) + +CREATE (Twister:Movie {title:'Twister', released:1996, tagline:"Don't Breathe. Don't Look Back."}) +CREATE (PhilipH:Person {name:'Philip Seymour Hoffman', born:1967}) +CREATE (JanB:Person {name:'Jan de Bont', born:1943}) +CREATE +(BillPax)-[:ACTED_IN {roles:['Bill Harding']}]->(Twister), +(HelenH)-[:ACTED_IN {roles:['Dr. Jo Harding']}]->(Twister), +(ZachG)-[:ACTED_IN {roles:['Eddie']}]->(Twister), +(PhilipH)-[:ACTED_IN {roles:['Dustin "Dusty" Davis']}]->(Twister), +(JanB)-[:DIRECTED]->(Twister) + +CREATE (CastAway:Movie {title:'Cast Away', released:2000, tagline:'At the edge of the world, his journey begins.'}) +CREATE (RobertZ:Person {name:'Robert Zemeckis', born:1951}) +CREATE +(TomH)-[:ACTED_IN {roles:['Chuck Noland']}]->(CastAway), +(HelenH)-[:ACTED_IN {roles:['Kelly Frears']}]->(CastAway), +(RobertZ)-[:DIRECTED]->(CastAway) + +CREATE (OneFlewOvertheCuckoosNest:Movie {title:"One Flew Over the Cuckoo's Nest", released:1975, tagline:"If he's crazy, what does that make you?"}) +CREATE (MilosF:Person {name:'Milos Forman', born:1932}) +CREATE +(JackN)-[:ACTED_IN {roles:['Randle McMurphy']}]->(OneFlewOvertheCuckoosNest), +(DannyD)-[:ACTED_IN {roles:['Martini']}]->(OneFlewOvertheCuckoosNest), +(MilosF)-[:DIRECTED]->(OneFlewOvertheCuckoosNest) + +CREATE (SomethingsGottaGive:Movie {title:"Something's Gotta Give", released:2003}) +CREATE (DianeK:Person {name:'Diane Keaton', born:1946}) +CREATE (NancyM:Person {name:'Nancy Meyers', born:1949}) +CREATE +(JackN)-[:ACTED_IN {roles:['Harry Sanborn']}]->(SomethingsGottaGive), +(DianeK)-[:ACTED_IN {roles:['Erica Barry']}]->(SomethingsGottaGive), +(Keanu)-[:ACTED_IN {roles:['Julian Mercer']}]->(SomethingsGottaGive), +(NancyM)-[:DIRECTED]->(SomethingsGottaGive), +(NancyM)-[:PRODUCED]->(SomethingsGottaGive), +(NancyM)-[:WROTE]->(SomethingsGottaGive) + +CREATE (BicentennialMan:Movie {title:'Bicentennial Man', released:1999, tagline:"One robot's 200 year journey to become an ordinary man."}) +CREATE (ChrisC:Person {name:'Chris Columbus', born:1958}) +CREATE +(Robin)-[:ACTED_IN {roles:['Andrew Marin']}]->(BicentennialMan), +(OliverP)-[:ACTED_IN {roles:['Rupert Burns']}]->(BicentennialMan), +(ChrisC)-[:DIRECTED]->(BicentennialMan) + +CREATE (CharlieWilsonsWar:Movie {title:"Charlie Wilson's War", released:2007, tagline:"A stiff drink. A little mascara. A lot of nerve. Who said they couldn't bring down the Soviet empire."}) +CREATE (JuliaR:Person {name:'Julia Roberts', born:1967}) +CREATE +(TomH)-[:ACTED_IN {roles:['Rep. Charlie Wilson']}]->(CharlieWilsonsWar), +(JuliaR)-[:ACTED_IN {roles:['Joanne Herring']}]->(CharlieWilsonsWar), +(PhilipH)-[:ACTED_IN {roles:['Gust Avrakotos']}]->(CharlieWilsonsWar), +(MikeN)-[:DIRECTED]->(CharlieWilsonsWar) + +CREATE (ThePolarExpress:Movie {title:'The Polar Express', released:2004, tagline:'This Holiday Season... Believe'}) +CREATE +(TomH)-[:ACTED_IN {roles:['Hero Boy', 'Father', 'Conductor', 'Hobo', 'Scrooge', 'Santa Claus']}]->(ThePolarExpress), +(RobertZ)-[:DIRECTED]->(ThePolarExpress) + +CREATE (ALeagueofTheirOwn:Movie {title:'A League of Their Own', released:1992, tagline:'Once in a lifetime you get a chance to do something different.'}) +CREATE (Madonna:Person {name:'Madonna', born:1954}) +CREATE (GeenaD:Person {name:'Geena Davis', born:1956}) +CREATE (LoriP:Person {name:'Lori Petty', born:1963}) +CREATE (PennyM:Person {name:'Penny Marshall', born:1943}) +CREATE +(TomH)-[:ACTED_IN {roles:['Jimmy Dugan']}]->(ALeagueofTheirOwn), +(GeenaD)-[:ACTED_IN {roles:['Dottie Hinson']}]->(ALeagueofTheirOwn), +(LoriP)-[:ACTED_IN {roles:['Kit Keller']}]->(ALeagueofTheirOwn), +(RosieO)-[:ACTED_IN {roles:['Doris Murphy']}]->(ALeagueofTheirOwn), +(Madonna)-[:ACTED_IN {roles:['"All the Way" Mae Mordabito']}]->(ALeagueofTheirOwn), +(BillPax)-[:ACTED_IN {roles:['Bob Hinson']}]->(ALeagueofTheirOwn), +(PennyM)-[:DIRECTED]->(ALeagueofTheirOwn) + +CREATE (PaulBlythe:Person {name:'Paul Blythe'}) +CREATE (AngelaScope:Person {name:'Angela Scope'}) +CREATE (JessicaThompson:Person {name:'Jessica Thompson'}) +CREATE (JamesThompson:Person {name:'James Thompson'}) + +CREATE +(JamesThompson)-[:FOLLOWS]->(JessicaThompson), +(AngelaScope)-[:FOLLOWS]->(JessicaThompson), +(PaulBlythe)-[:FOLLOWS]->(AngelaScope) + +CREATE +(JessicaThompson)-[:REVIEWED {summary:'An amazing journey', rating:95}]->(CloudAtlas), +(JessicaThompson)-[:REVIEWED {summary:'Silly, but fun', rating:65}]->(TheReplacements), +(JamesThompson)-[:REVIEWED {summary:'The coolest football movie ever', rating:100}]->(TheReplacements), +(AngelaScope)-[:REVIEWED {summary:'Pretty funny at times', rating:62}]->(TheReplacements), +(JessicaThompson)-[:REVIEWED {summary:'Dark, but compelling', rating:85}]->(Unforgiven), +(JessicaThompson)-[:REVIEWED {summary:"Slapstick redeemed only by the Robin Williams and Gene Hackman's stellar performances", rating:45}]->(TheBirdcage), +(JessicaThompson)-[:REVIEWED {summary:'A solid romp', rating:68}]->(TheDaVinciCode), +(JamesThompson)-[:REVIEWED {summary:'Fun, but a little far fetched', rating:65}]->(TheDaVinciCode), +(JessicaThompson)-[:REVIEWED {summary:'You had me at Jerry', rating:92}]->(JerryMaguire) + +WITH TomH as a +MATCH (a)-[:ACTED_IN]->(m)<-[:DIRECTED]-(d) RETURN a,m,d LIMIT 10; \ No newline at end of file