At this point, we should take a look at the Cypher type system. Despite Neo4j being written in Java (the j in Neo4j stands for Java after all), there are some discrepancies between the types available in Cypher and native Java types.
Some values like strings, numbers, booleans, dates, and nulls map directly to Java types but more complex types like nodes, relationship, points, durations need special handling.
Java Types to Neo4j/Cypher Types
Java Type | Neo4j Cypher Type | Notes |
---|---|---|
|
|
|
|
|
Neo4j can only store a flat array containing strings, booleans or numbers. |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
See Temporal Types |
|
|
See Temporal Types |
|
|
See Temporal Types |
|
|
See Temporal Types |
|
|
See Temporal Types |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
You can use the as{Type}()
method on any Value
type or nested structure to cast underlying values to expected types.
See the API docs for Value
for more information.
For example, in the code block below, the year
value is cast as a Number
.
Value nodeValue = row.get("movie");
// key names
Iterable<String> keys = nodeValue.keys();
// number of values / length of list
nodeValue.size();
// get all contained values
Iterable<Value> values = nodeValue.values();
// treat value as type, e.g. Node, Relationship, Path or primitive
Node node = nodeValue.asNode();
// string-key accessors
Value titleValue = node.get("title");
String title = titleValue.asString();
Number year = node.get("year").asNumber();
Integer releaseYear = node.get("year").asInt(0);
// index based accessors for lists and records
nodeValue.get(0);
Let’s take a look at some of these types in more detail.
Numbers
For numeric values the main confusion comes from the naming, while Neo4j itself can store all kinds of Java primitive values for a unified surface, Cypher only exposes one floating point type called Float
(equivalent to 64-bit double) and one integer type called Integer
(equivalent to 64-bit long).
Cypher itself has functions like toFloat()
and toInteger()
respectively and driver parameters of other types are automatically coerced.
The driver’s Value
type can return most Java numeric types via as{Type}()
methods.
Only BigDecimal/BigInteger
for arbitrary precision math are not supported.
Temporal Types
The Temporal types used in the Cypher type system mirror the java.time.*
types, so there are few surprises.
The only difference is IsoDuration
for which the driver provides a custom type.
Type | Description | Example | Access Function |
---|---|---|---|
|
Represents an instant capturing the date, but not the time, nor the timezone. |
|
|
|
Represents an instant capturing the date, the time and the timezone identifier. |
|
|
|
Represents an instant capturing the date and the time, but not the timezone. |
|
|
|
Represents an instant capturing the time of day, but not the date, nor the timezone. |
|
|
|
Represents an instant capturing the time of day, and the timezone offset in seconds, but not the date. |
|
|
|
Represents a duration between two dates or timestamps (different resolutions). Has individual accessors for the parts. |
|
|
// Driver consumes regular java.time.* datatypes
// Temporal Types from Value
var released = nodeValue.get("released");
released.asLocalDate();
released.asLocalDateTime();
released.asLocalTime();
released.asOffsetDateTime();
// custom duration type
IsoDuration duration = released.asIsoDuration();
Nodes & Relationships
Nodes and Relationships are both returned as similar types with a common superclass Entity
.
As an example, let’s take the following code snippet:
// Execute a query within a read transaction
Result res = session.readTransaction(tx -> tx.run("""
MATCH path = (person:Person)-[actedIn:ACTED_IN]->(movie:Movie)
RETURN path, person, actedIn, movie,
size ( (person)-[:ACTED]->() ) as movieCount,
exists { (person)-[:DIRECTED]->() } as isDirector
LIMIT 1
"""));
Nodes
We can retrieve the person
value using the .get()
method on the row and then turn it into a Node
via asNode()
.
// Get a node
Node person = row.get("person").asNode();
The value assigned to the person
variable will be the instance of a Node
.
Node
is a type provided by the Neo4j Java Driver to represent the information held in Neo4j for a node.
An instance of a Node
has three parts:
var nodeId = person.id(); // (1)
var labels = person.labels(); // (2)
var properties = person.asMap(); // (3)
-
id
- representing the internal ID for the node. -
labels
- an Iterable of String values, eg.['Person', 'Actor']
-
properties
- A Java Map containing all the properties for the node.
eg.{"name": "Tom Hanks", "tmdbId": "31" }
Properties can also be retrieved from Entity
instances with the get(name)
method which then returns a Value
that has to be converted further.
Internal IDs
Internal IDs should be treated as opaque values and just sent back to the database as you get them. These ids can be re-used, a best practice is to always look up a node by its business key and label rather than relying on an internal ID.Relationships
Relationship
objects are also Entity
instances, they also include an id
, a type and properties.
var actedIn = row.get("actedIn").asRelationship();
var relId = actedIn.id(); // (1)
String type = actedIn.type(); // (2)
var relProperties = actedIn.asMap(); // (3)
var startId = actedIn.startNodeId(); // (4)
var endId = actedIn.endNodeId(); // (5)
-
identity
- the internal ID for the relationship. -
type
- the type of the relationship, eg -ACTED_IN
-
properties
- A Java Map containing all the properties for the relationship.
eg.{"role": "Woody" }
-
startNodeId
- representing the internal ID for the node at the start of the relationship -
endNodeId
- representing the internal ID for the node at the end of the relationship
Paths
If you return a path of nodes and relationships, they will be returned as an instance of a Path
.
Path path = row.get("path").asPath();
Node start = path.start(); // (1)
Node end = path.end(); // (2)
var length = path.length(); // (3)
Iterable<Path.Segment> segments = path; // (4)
Iterable<Node> nodes = path.nodes(); // (5)
Iterable<Relationship> rels = path.relationships(); // (6)
-
start
- the node starting the path -
end
- the node ending the path -
length
- A count of the number of segments within the path -
segments
- A path is an Iterable ofPath.Segment
-
nodes
- An Iterable of Nodes of the Path -
relationships
- An Iterable of Relationships of the Path
Path Segments
A path is split into segments representing each relationship in the path.
For example, say we have a path of (p:Person)-[:ACTED_IN]->(m:Movie)-[:IN_GENRE]->(g:Genre)
, there would be two segments.
-
(p:Person)-[:ACTED_IN]->(m:Movie)
-
(m:Movie)-[:IN_GENRE]->(g:Genre)
path.forEach(segment -> {
System.out.println(segment.start());
System.out.println(segment.end());
System.out.println(segment.relationship());
});
The PathSegment
object has three properties:
-
relationship
- ARelationship
object representing that part of the path. -
start
- start node for this path segment*
-
end
- end node for this path segment*
*
Start and End nodes within the Path Segment object
PathSegment
may differ from the start and end nodes of the relationship itself if the relationship was traversed in reverse direction.Converting these values en masse
There may be times when you need to convert many Neo4j types back into native Java types. For example, when retrieving a set of properties.
For this the Value
and Record
type has three functions
-
asObject()
recursively converts aValue
into the appropriate Java objects -
asMap()
converts a Value into a Java Map, it can take a callback function to customize conversion of individual keys and values -
list()
converts a Value into a Java List, it can take a callback function to customize conversion of individual values
The function are recursive, and will handle nested objects and arrays.
Additional helper functions
Value
has additional helper functions
-
isTrue
andisFalse
for boolean values -
isNull
for null checks -
isEmpty
for lists and maps
Check Your Understanding
1. Accessing Node Properties
Which property would you access to retrieve the "name" property for each person?
Select the correct option in the code block below.
var res = session.executeRead(tx ->
tx.run("""
MATCH (p:Person)-[:ACTED_IN]->(:Movie {title: $title})
RETURN p
LIMIT 10
""",
Values.parameters("title", "Toy Story"))
)
var names = res.stream().map(row -> {
return row.get('p')./*select:properties.name*/
})
-
❏ name
-
❏ property['name']
-
✓ get("name")
-
❏ properties[0]
Hint
There is a dedicated method for retrieving properties.
Solution
Properties can be accessed on nodes and relationships using the .get()
method - for example node.get("name")
.
2. Temporal Accessors
Which of the following functions does the Neo4j Value not support for temporal types?
-
❏
asOffsetTime()
-
❏
asIsoDuration()
-
✓
asTimestamp()
-
❏
asLocalDateTime()
Hint
Neo4j supports five temporal types.
Solution
The only unsupported method above is asTimestamp()
Lesson Summary
In this lesson you have learned how to handle some of the more complex objects returned by a Cypher statement. As we progress through this module, you will use the knowledge gained so far to read data from, and write data back to the database.
In the next Challenge, you will modify code to read from the database.