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 000000000..6bf0b9d5c Binary files /dev/null and b/src/main/asciidoc/img/bacon-distance.png differ diff --git a/src/main/asciidoc/images/springdatagraph.png b/src/main/asciidoc/img/springdatagraph.png similarity index 100% rename from src/main/asciidoc/images/springdatagraph.png rename to src/main/asciidoc/img/springdatagraph.png diff --git a/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java b/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java index 6fd61f282..a37228416 100644 --- a/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java +++ b/src/main/java/org/springframework/data/neo4j/core/DefaultNeo4jClient.java @@ -19,7 +19,6 @@ import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Collection; -import java.util.List; import java.util.Map; import java.util.Optional; import java.util.function.BiFunction; @@ -310,7 +309,7 @@ class DefaultNeo4jClient implements Neo4jClient { try (AutoCloseableQueryRunner statementRunner = getQueryRunner(this.targetDatabase)) { Result result = runnableStatement.runWith(statementRunner); - List 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