Mapping query results to domain entities using SDN 4.1

April 6, 2016 · 5 min read

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.

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.

Graph Model

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 @QueryResults 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!


Meet the authors