DATAGRAPH-1437 - Support mapping of named paths.

This adds support for mapping named paths. To support this, a couple of changes are necessary:

* The mapping process must be more strict:
  Only nodes with having at least the exact primary label are used to hydrate entites.
  One of our tests needed a fix. The test should have tested for various constructor
  operations on _entities_ but did test DTO projections. These dto projects had
  been mapped from custom queries, returning nodes without the label of the original
  entity.
  Of couse we did check for the primary label before, but the driver type indicator of MAP
  is not mutual exclusive. That means a node is map, too.
* The mapping process should not pick the first matching thing to map:
  This means that the mapping will fail now if there is more than one structure that would
  theoritically fit

The second biggest change here is that the `Neo4jEntityConverter` has become now
stateful. It keeps a cache of seen objects for as long as one query keeps on running.
That means for each interaction, the converter is created fresh (it hadn't been a
bean beforehand).
This allows to handle nodes on a path that are usually seen twice.

The path mapping itself relies on the same mechanism that we introduced for
DATAGRAPH-1429: Working with one aggregate coming back from the database on the top
level. A named path is such an aggregate much like a list.

The path mapping will only populate one main entity along a path, using the possible
other nodes only for filling up relationships.

The imperative and reactive template will return now the _distinct_ list of entities.
This commit is contained in:
Michael Simons
2020-11-27 11:40:12 +01:00
parent ce31512c81
commit 9ac3b99cb6
23 changed files with 1433 additions and 143 deletions

39
etc/adr/adr-008.adoc Normal file
View File

@@ -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.

View File

@@ -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<Movie> reviewed = new ArrayList<>();
}
@RelationshipProperties
public final class Actor {
@TargetNode
private final Person person;
private final List<String> 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<Actor> actors;
}
----
When using a query as shown in <<bacon-distance>> for a domain class of type `Person` like this
[source,java]
----
interface PeopleRepository extends Neo4jRepository<Person, Long> {
@Query(""
+ "MATCH p=shortestPath((bacon:Person {name: $person1})-[*]-(meg:Person {name: $person2}))\n"
+ "RETURN p"
)
List<Person> 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<Movie, String> {
@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<Movie> 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?

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -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<T> values = result.stream().map(partialMappingFunction(typeSystem)).collect(Collectors.toList());
Collection<T> values = result.stream().map(partialMappingFunction(typeSystem)).collect(Collectors.toList());
ResultSummaries.process(result.consume());
return values;
} catch (RuntimeException e) {

View File

@@ -577,7 +577,7 @@ public final class Neo4jTemplate implements Neo4jOperations, BeanFactoryAware {
Collection<T> all = fetchSpec.all();
if (preparedQuery.resultsHaveBeenAggregated()) {
return all.stream().flatMap(nested -> ((Collection<T>) nested).stream()).collect(Collectors.toList());
return all.stream().flatMap(nested -> ((Collection<T>) 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);
}
}

View File

@@ -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<T> {
if (optionalBuildSteps.mappingFunction == null) {
this.mappingFunction = null;
} else {
this.mappingFunction = (BiFunction<TypeSystem, Record, T>) new ListAggregatingMappingFunction(
this.mappingFunction = (BiFunction<TypeSystem, Record, T>) new AggregatingMappingFunction(
optionalBuildSteps.mappingFunction);
}
this.cypherQuery = optionalBuildSteps.cypherQuery;
@@ -74,7 +86,7 @@ public final class PreparedQuery<T> {
}
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<T> {
}
}
private static class ListAggregatingMappingFunction implements BiFunction<TypeSystem, Record, Object> {
private static class AggregatingMappingFunction implements BiFunction<TypeSystem, Record, Object> {
private final BiFunction<TypeSystem, MapAccessor, ?> target;
private final AtomicBoolean aggregated = new AtomicBoolean(false);
ListAggregatingMappingFunction(BiFunction<TypeSystem, MapAccessor, ?> target) {
AggregatingMappingFunction(BiFunction<TypeSystem, MapAccessor, ?> target) {
this.target = target;
}
private Collection<?> aggregateList(TypeSystem t, Value value) {
if (MappingSupport.isListContainingOnly(t.LIST(), t.PATH()).test(value)) {
Set<Object> 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<Map.Entry<String, Value>> 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<Object> result = new LinkedHashSet<>();
path.iterator().forEachRemaining(segment -> {
Map<String, Value> 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<Boolean, List<Map.Entry<String, Value>>> 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() {

View File

@@ -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<T>) nested));
return f.flatMap(nested -> Flux.fromIterable((Collection<T>) 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);
}
}
}

View File

@@ -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() {
}
}

View File

@@ -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> R read(Class<R> targetType, MapAccessor mapAccessor) {
Neo4jPersistentEntity<R> rootNodeDescription = (Neo4jPersistentEntity) nodeDescriptionStore
.getNodeDescription(targetType);
Neo4jPersistentEntity<R> 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<Value> 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 <R> MapAccessor determineQueryRoot(MapAccessor mapAccessor, Neo4jPersistentEntity<R> rootNodeDescription) {
String primaryLabel = rootNodeDescription.getPrimaryLabel();
// Massage the initial mapAccessor into something we can deal with
Iterable<Value> recordValues = mapAccessor instanceof Value && ((Value) mapAccessor).hasType(nodeType) ?
Collections.singletonList((Value) mapAccessor) : mapAccessor.values();
List<Node> matchingNodes = new ArrayList<>(); // The node that eventually becomes the query root. The list should only contain one node.
List<Node> 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<Node> 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<String> createDynamicLabelsProperty(TypeInformation<?> type, Collection<String> dynamicLabels) {
Collection<String> 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 <ET> As in entity type
* @return The mapped entity
*/
private <ET> ET map(MapAccessor queryResult, MapAccessor allValues, Neo4jPersistentEntity<ET> nodeDescription, KnownObjects knownObjects, Set<Path.Segment> processedSegments) {
return map(queryResult, allValues, nodeDescription, knownObjects, null, processedSegments);
private <ET> ET map(MapAccessor queryResult, MapAccessor allValues, Neo4jPersistentEntity<ET> nodeDescription, Set<Path.Segment> processedSegments) {
return map(queryResult, allValues, nodeDescription, null, processedSegments);
}
private <ET> ET map(MapAccessor queryResult, MapAccessor allValues, Neo4jPersistentEntity<ET> nodeDescription, KnownObjects knownObjects,
@Nullable Object lastMappedEntity, Set<Path.Segment> processedSegments) {
private <ET> ET map(MapAccessor queryResult, MapAccessor allValues, Neo4jPersistentEntity<ET> nodeDescription,
@Nullable Object lastMappedEntity, Set<Path.Segment> 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<RelationshipDescription> relationships = concreteNodeDescription.getRelationships();
ET instance = instantiate(concreteNodeDescription, queryResult, allValues, knownObjects, relationships,
ET instance = instantiate(concreteNodeDescription, queryResult, allValues, relationships,
nodeDescriptionAndLabels.getDynamicLabels(), lastMappedEntity, processedSegments);
PersistentPropertyAccessor<ET> 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> ET instantiate(Neo4jPersistentEntity<ET> nodeDescription, MapAccessor values, MapAccessor allValues, KnownObjects knownObjects,
Collection<RelationshipDescription> relationships, Collection<String> surplusLabels, Object lastMappedEntity,
Set<Path.Segment> processedSegments) {
private <ET> ET instantiate(Neo4jPersistentEntity<ET> nodeDescription, MapAccessor values, MapAccessor allValues,
Collection<RelationshipDescription> relationships, Collection<String> surplusLabels,
Object lastMappedEntity,
Set<Path.Segment> processedSegments) {
ParameterValueProvider<Neo4jPersistentProperty> parameterValueProvider = new ParameterValueProvider<Neo4jPersistentProperty>() {
@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<Neo4jPersistentProperty> populateFrom(MapAccessor queryResult, MapAccessor allValues,
PersistentPropertyAccessor<?> propertyAccessor, Predicate<Neo4jPersistentProperty> isConstructorParameter,
Collection<RelationshipDescription> relationshipDescriptions, KnownObjects knownObjects, Set<Path.Segment> processedSegments) {
Collection<RelationshipDescription> relationshipDescriptions, Set<Path.Segment> 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<Object> createInstanceOfRelationships(Neo4jPersistentProperty persistentProperty, MapAccessor values,
MapAccessor allValues, KnownObjects knownObjects, Collection<RelationshipDescription> relationshipDescriptions, Set<Path.Segment> processedSegments) {
MapAccessor allValues, Collection<RelationshipDescription> relationshipDescriptions, Set<Path.Segment> 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<Value> isList = entry -> typeSystem.LIST().isTypeOf(entry);
if (isGeneratedPathBased) {
Value internalStartNodeIdValue = values.get(Constants.NAME_OF_INTERNAL_ID);
long startNodeId = internalStartNodeIdValue.asLong();
Predicate<Value> containsOnlyPaths = entry -> entry.asList(Function.identity()).stream()
.allMatch(listEntry -> typeSystem.PATH().isTypeOf(listEntry));
List<Path> 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<Path.Segment> 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<Value> containsOnlyRelationships = entry -> entry.asList(Function.identity()).stream()
.allMatch(listEntry -> typeSystem.RELATIONSHIP().isTypeOf(listEntry));
Collection<Relationship> allMatchingTypeRelationshipsInResult = new ArrayList<>();
Collection<Node> allNodesWithMatchingLabelInResult = new ArrayList<>();
Predicate<Value> 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<Relationship> 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<Node> 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 {

View File

@@ -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<Value> isListContainingOnly(Type collectionType, Type requiredType) {
Predicate<Value> containsOnlyRequiredType = entry -> {
for (Value listEntry : entry.values()) {
if (!listEntry.hasType(requiredType)) {
return false;
}
}
return true;
};
Predicate<Value> isList = entry -> entry.hasType(collectionType);
return isList.and(containsOnlyRequiredType);
}
private MappingSupport() {}
/**
@@ -83,5 +109,4 @@ public final class MappingSupport {
return relatedEntity;
}
}
}

View File

@@ -85,10 +85,7 @@ public final class Neo4jMappingContext extends AbstractMappingContext<Neo4jPersi
*/
private final NodeDescriptionStore nodeDescriptionStore = new NodeDescriptionStore();
/**
* The converter used in this mapping context.
*/
private final Neo4jEntityConverter entityConverter;
private final TypeSystem typeSystem;
private final Neo4jConversionService conversionService;
@@ -117,14 +114,11 @@ public final class Neo4jMappingContext extends AbstractMappingContext<Neo4jPersi
super.setSimpleTypeHolder(Neo4jSimpleTypes.HOLDER);
this.conversionService = new DefaultNeo4jConversionService(neo4jConversions);
DefaultNeo4jEntityConverter defaultNeo4jConverter = new DefaultNeo4jEntityConverter(INSTANTIATORS,
conversionService, nodeDescriptionStore);
defaultNeo4jConverter.setTypeSystem(typeSystem == null ? InternalTypeSystem.TYPE_SYSTEM : typeSystem);
this.entityConverter = defaultNeo4jConverter;
this.typeSystem = typeSystem == null ? InternalTypeSystem.TYPE_SYSTEM : typeSystem;
}
public Neo4jEntityConverter getEntityConverter() {
return entityConverter;
return new DefaultNeo4jEntityConverter(INSTANTIATORS, conversionService, nodeDescriptionStore, typeSystem);
}
public Neo4jConversionService getConversionService() {
@@ -317,7 +311,7 @@ public final class Neo4jMappingContext extends AbstractMappingContext<Neo4jPersi
neo4jPersistentEntity, relationshipContext.getRelationship(), relatedInternalId);
Map<String, Object> propMap = new HashMap<>();
// write relationship properties
entityConverter.write(relatedValue.getRelationshipProperties(), propMap);
getEntityConverter().write(relatedValue.getRelationshipProperties(), propMap);
return new CreateRelationshipStatementHolder(relationshipCreationQuery, propMap);
}

View File

@@ -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);
}
}

View File

@@ -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<String, Object> parameters = new HashMap<>();
getEntityConverter().write(t, parameters);
entityConverter.write(t, parameters);
return parameters;
};
}

View File

@@ -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<PersonWithNoConstructor> 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<PersonWithNoConstructor> person = repository.getOptionalPersonWithNoConstructorViaQuery();
assertThat(person).isPresent();
@@ -475,7 +477,7 @@ class RepositoryIT {
}
@Test
void loadAllPersonsWithWither(@Autowired PersonRepository repository) {
void loadAllPersonsWithWither(@Autowired PersonWithWitherRepository repository) {
List<PersonWithWither> 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<PersonWithWither> person = repository.getOptionalPersonWithWitherViaQuery();
assertThat(person).isPresent();

View File

@@ -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<PersonWithAllConstruct
@Query("MATCH (n:PersonWithAllConstructor) return n")
List<PersonWithAllConstructor> getAllPersonsViaQuery();
@Query("MATCH (n:UnknownLabel) return n")
@Query("MATCH (n) WHERE 1 = 2 return n")
List<PersonWithAllConstructor> getNobodyViaQuery();
@Query("MATCH (n:PersonWithAllConstructor{name:'Test'}) return n")
@@ -102,24 +100,6 @@ public interface PersonRepository extends Neo4jRepository<PersonWithAllConstruct
Optional<PersonWithAllConstructor> getOptionalPersonViaNamedQuery(@Param("part1") String part1,
@Param("part2") String part2);
@Query("MATCH (n:PersonWithNoConstructor) return n")
List<PersonWithNoConstructor> getAllPersonsWithNoConstructorViaQuery();
@Query("MATCH (n:PersonWithNoConstructor{name:'Test'}) return n")
PersonWithNoConstructor getOnePersonWithNoConstructorViaQuery();
@Query("MATCH (n:PersonWithNoConstructor{name:'Test'}) return n")
Optional<PersonWithNoConstructor> getOptionalPersonWithNoConstructorViaQuery();
@Query("MATCH (n:PersonWithWither) return n")
List<PersonWithWither> getAllPersonsWithWitherViaQuery();
@Query("MATCH (n:PersonWithWither{name:'Test'}) return n")
PersonWithWither getOnePersonWithWitherViaQuery();
@Query("MATCH (n:PersonWithWither{name:'Test'}) return n")
Optional<PersonWithWither> getOptionalPersonWithWitherViaQuery();
// Derived finders, should be extracted into another repo.
Optional<PersonWithAllConstructor> findOneByNameAndFirstName(String name, String firstName);
@@ -139,11 +119,6 @@ public interface PersonRepository extends Neo4jRepository<PersonWithAllConstruct
Stream<PersonWithAllConstructor> 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<PersonWithAllConstructor> findOneByFirstName(String aName);
List<PersonWithAllConstructor> findAllBySameValue(String sameValue);
List<PersonWithAllConstructor> findAllBySameValueIgnoreCase(String sameValue);

View File

@@ -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<PersonWithNoConstructor, Long> {
@Query("MATCH (n:PersonWithNoConstructor) return n")
List<PersonWithNoConstructor> getAllPersonsWithNoConstructorViaQuery();
@Query("MATCH (n:PersonWithNoConstructor{name:'Test'}) return n")
PersonWithNoConstructor getOnePersonWithNoConstructorViaQuery();
@Query("MATCH (n:PersonWithNoConstructor{name:'Test'}) return n")
Optional<PersonWithNoConstructor> getOptionalPersonWithNoConstructorViaQuery();
}

View File

@@ -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<PersonWithWither, Long> {
@Query("MATCH (n:PersonWithWither) return n")
List<PersonWithWither> getAllPersonsWithWitherViaQuery();
@Query("MATCH (n:PersonWithWither{name:'Test'}) return n")
PersonWithWither getOnePersonWithWitherViaQuery();
@Query("MATCH (n:PersonWithWither{name:'Test'}) return n")
Optional<PersonWithWither> getOptionalPersonWithWitherViaQuery();
}

View File

@@ -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<String> roles;
public Actor(Person person, List<String> roles) {
this.person = person;
this.roles = roles;
}
public Person getPerson() {
return person;
}
public String getName() {
return person.getName();
}
public List<String> getRoles() {
return Collections.unmodifiableList(roles);
}
@Override public String toString() {
return "Actor{" +
"person=" + person +
", roles=" + roles +
'}';
}
}

View File

@@ -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<String, Object> 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<Person> 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<String, Object> 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<Person> 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<String, Object> 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<Person> 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<String, Object> 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<Movie> 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();
}
}
}

View File

@@ -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<Actor> actors;
@Relationship(value = "DIRECTED", direction = Direction.INCOMING)
private final List<Person> 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<Actor> actors, List<Person> 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<Actor> getActors() {
return Collections.unmodifiableList(this.actors);
}
public List<Person> getDirectors() {
return Collections.unmodifiableList(this.directors);
}
public Integer getReleased() {
return released;
}
public void setReleased(Integer released) {
this.released = released;
}
}

View File

@@ -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<Movie> 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<Movie> getReviewed() {
return reviewed;
}
@Override public String toString() {
return "Person{" +
"id=" + id +
", name='" + name + '\'' +
", born=" + born +
'}';
}
}

View File

@@ -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;