Spring Data Neo4j 4.1 introduces the ability to map nodes and relationships returned by custom Cypher queries to domain entities. This blog post will explain how different types of query results map to entities.
But first
There are two things to keep in mind-
- This functionality is only available in SDN 4.1 and Neo4j-OGM 2.0
- SDN/Neo4j-OGM does not modify your Cypher queries at runtime and it can only map what the query returns
The Basics
This simple example shows a custom Cypher query that returns a single node entity and a calculated value:
@Query("MATCH (user:User) WHERE <complex conditions> RETURN user, calculatedValue")UserResultfindUserByComplexCondition(Stringparam
and the @QueryResult
looks like
@QueryResultpublicclassUserResult{UseruserlongcalculatedValue}
The Cypher query returns a User node containing its own properties, and a calculated value, both of which are mapped to their respective fields in the UserResult
class. None of the relationships of the User
are available because none were returned by the Cypher query, hence, the equivalent is a depth 0 load of the User
entity.
Map all related entities
What if we want the relationships of the User
to be mapped as well? Then, we must return the relationships and nodes on either end so that SDN can map them all.
The Cypher query will change to
MATCH(user:User)WHERE<complexconditions>WITHuser,calculatedValueMATCHp=(user)-[r*0..1]-()RETURNuser,calculatedValue,nodes(p),rels(p)
Note that the UserResult
doesn’t have to change: all we’re doing is extending the depth to which the User
object it contains gets hydrated. Changing the path length will result in the entity being mapped to a different depth.
Relationship Entities
Apart from nodes, relationship entities can also be mapped in a @QueryResult
. Consider users that rate movies. We want to query for a particular User
based on some complex condition and retrieve all his ratings and his average rating.
The RATED
relationship is modelled as a @RelationshipEntity
.
@RelationshipEntity(type="RATED")publicclassRating{privateLongid@StartNodeprivateUseruser@EndNodeprivateMoviemovieprivateintstars...}
The @QueryResult
is now expected to contain the User
, a collection of Rating
relationship entities, and the average rating-
@QueryResultpublicclassUserQueryResult{UseruserSet<Rating>ratingsfloatavgRating}
To map a relationship entity, SDN must have the start node (user) and end node (movie) as well as the relationship entity (rating) itself. The following Cypher query supplies all the data required:
MATCH(user:User)-[r:RATED]->(m)WHEREuser.name={0}AND<complexcondition>RETURNuser,collect(r)asratings,collect(m)asmovies,avg(r.stars)asavgRating
This query can be used with these two @QueryResult
s as well-
@QueryResultpublicclassUserQueryResult{UseruserfloatavgRating}
and
@QueryResultpublicclassUserQueryResult{UseruserSet<Rating>ratingsSet<Movie>moviesfloatavgRating}
Note that in all cases above, the User
and Movie
entities are partially hydrated. Only the RATED
relations are mapped.
Caveats
Using Session or Neo4jTemplate queryForObjects but not specifying which objects you want
Both the Session
(Neo4j-OGM) and Neo4jTemplate
(SDN) allow you to query for objects of a type via a custom Cypher query.
Be careful when your query returns related objects of the same type. SDN/Neo4j-OGM cannot guess which one(s) you want- all it can do is map everything returned by the Cypher query as best it can, and return all entities that match the type you’ve requested for.
For example,
template.queryForObjects(User.class,"MATCH p=(user:User)-[r:FRIEND_OF]-(friend:User) return nodes(p),rels(p),user",Collections.EMPTY_MAP
will result in the OGM mapping users and their friends- but which ones are you expecting? The user or the friends? The OGM cannot guess, and so it will return all mapped entities for User.class
.
So what can you do if you want a User
with his friends hydrated?
Use template.query
instead and retrieve the column you’re interested in from the Result
that is returned.
Returning relationships but not the start or end nodes
If you have a Cypher query such as
MATCH (user:User)-[r:RATED]->(m:Movie) RETURN u,r
then only the User
entity can be mapped to depth 0. The Movie
is not returned and hence it is not mapped, and it is also the end node of the RATED
relationship so the relationship cannot be mapped either.
Remember to include all nodes and relationships that you want the OGM to hydrate.
Partially hydrating entities
Entities may be partially hydrated in terms of what relationships are mapped depending on what the Cypher query returns. In the example we saw earlier,
MATCH(user:User)-[r:RATED]->(m)WHEREuser.name={0}AND<complexcondition>RETURNuser,collect(r)asratings,collect(m)asmovies,avg(r.stars)asavgRating
would partially hydrate both the User
and Movie
entities. While all their properties are mapped, any relationships they may have to other entities apart from RATED
will not be mapped because SDN knows only what is returned from the Cypher query.
This mechanism can be employed when you want to map some relationships but not all for a densely related node.
Returning paths
A Cypher query that returns a path is not supported by Neo4j-OGM or SDN 4.1. There is no notion of a path entity in SDN, rather, it is able to hydrate object references.
So instead of returning a path, return the objects that comprise it instead- nodes and relationships.
Conclusion
Query result mapping can be very useful when domain entities as well as calculated values or aggregations are to be returned, or when a partially hydrated entity is required.
It is worth reiterating that what your query returns is what is mapped and Neo4j-OGM/SDN currently do not modify any user queries. If this is kept in mind, you should have no problems mapping any/all of the result data back to domain entities.
We’re presenting “Build Spring Data Neo4j 4.1 Applications like a Superhero” at GraphConnect Europe 2016 – we hope to see you there!