A custom Cypher query that returns the same root on several records can produce a partial or surprising mapped graph.
Spring Data Neo4j custom query: return one coherent root record
A row is a mapping unit
A shipment has three checkpoints and two assigned drivers. A query matching both collections can yield six records before aggregation. Returning the shipment root on each record forces Spring Data Neo4j to reconcile multiple fragments of one mapped entity. The outcome may differ from the graph the caller thought it loaded. For a graph-shaped repository return, collect related values into one record per root or use a focused projection. Graph ownership also matters when that result is saved.
Avoid accidental cartesian work
Match each relationship pattern with a bounded tenant and shipment predicate. Use a query plan to confirm the root lookup uses the intended index before expanding paths. A full graph traversal for a detail page increases both database work and Java object allocation. If the UI only needs counts, return scalar counts instead of materializing every related node.
Verify shape and cost
Create a shipment with three checkpoints and two drivers, then assert one root, three unique checkpoints and two unique drivers. Add 47 checkpoints and inspect records produced, row expansion and query latency. A simple two-node fixture can hide the cardinality error.
Implementation sketch
MATCH (shipment:Shipment {tenantId: $tenantId, shipmentId: $shipmentId})
OPTIONAL MATCH (shipment)-[:HAS_CHECKPOINT]->(checkpoint:Checkpoint)
WITH shipment, collect(DISTINCT checkpoint) AS checkpoints
OPTIONAL MATCH (shipment)-[:ASSIGNED_TO]->(driver:Driver)
RETURN shipment, checkpoints, collect(DISTINCT driver) AS driversCost and verification
Collecting one bounded subgraph avoids duplicate root records but consumes memory proportional to returned relationships. Cap expansion or project a summary when the fan-out is large.
Common Mistakes
- Do not return one root entity on many unrelated result records and assume mapping is free.
- Do not match two high-cardinality relationship sets before narrowing the root.
- Do not load the entire graph when the caller needs only a count.
Read next
Spring Data Neo4j relationships: save only the graph you own, Spring Data Neo4j tenant lookup: constrain before traversing, Spring GraphQL batch loader tenant scope: never key a shared cache by row ID alone.
