Search code examples
spring-bootkotlinneo4jspring-data-neo4jneo4j-ogm

Spring boot Neo4j - query depth not working correctly


TL;DR: @Depth(value = -1) throws nullpointer and other values above 1 are ignored

In my Spring Boot with Neo4j project I have 3 simple entities with relationships:

@NodeEntity
data class Metric(
        @Id @GeneratedValue val id: Long = -1,
        val name: String = "",
        val description: String = "",
        @Relationship(type = "CALCULATES")
        val calculates: MutableSet<Calculable> = mutableSetOf()
) {
    fun calculates(calculable: Calculus) = calculates.add(calculable)
    fun calculate() = calculates.map { c -> c.calculate() }.sum()
}

interface Calculable {
    fun calculate(): Double
}

@NodeEntity
data class Calculus(
        @Id @GeneratedValue val id: Long = -1,
        val name: String = "",
        @Relationship(type = "LEFT")
        var left: Calculable? = null,
        @Relationship(type = "RIGHT")
        var right: Calculable? = null,
        var operator: Operator? = null
) : Calculable {
    override fun calculate(): Double =
            operator!!.apply(left!!.calculate(), right!!.calculate())
}

@NodeEntity
data class Value(
        @Id @GeneratedValue val id: Long = -1,
        val name: String = "",
        var value: Double = 0.0
) : Calculable {
    override fun calculate(): Double = value
}

enum class Operator : BinaryOperator<Double>, DoubleBinaryOperator {//not relevant}

I create a simple graph like this one:

Neo4j Graph

With the following repositories:

@Repository
interface MetricRepository : Neo4jRepository<Metric, Long>{
    @Depth(value = 2)
    fun findByName(name: String): Metric?
}

@Repository
interface CalculusRepository : Neo4jRepository<Calculus, Long>{
    fun findByName(name: String): Calculus?
}

@Repository
interface ValueRepository : Neo4jRepository<Value, Long>{
    fun findByName(name: String): Value?
}

And the following code:

// calculus
val five = valueRepository.save(Value(
        name = "5",
        value = 5.0
))

val two = valueRepository.save(Value(
        name = "2",
        value = 2.0
))

val fiveTimesTwo = calculusRepository.save(Calculus(
        name = "5 * 2",
        operator = Operator.TIMES,
        left = five,
        right = two
))

println("---")
println(fiveTimesTwo)
val fromRepository = calculusRepository.findByName("5 * 2")!!
println(fromRepository) // sometimes has different id than fiveTimesTwo
println("5 * 2 = ${fromRepository.calculate()}")
println("--- \n")


// metric
val metric = metricRepository.save(Metric(
        name = "Metric 1",
        description = "Measures a calculus",
        calculates = mutableSetOf(fromRepository)
))
metricRepository.save(metric)

println("---")
println(metric)
val metricFromRepository = metricRepository.findByName("Metric 1")!!
println(metricFromRepository) // calculates node is partially empty
println("--- \n")

To retrieve the same graph as shown in the picture above (taken from the actual neo4j dashboard), I do metricRepository.findByName("Metric 1") which has @Depth(value = 2) and then print the saved metric and the retrieved metric:

Metric(id=9, name=Metric 1, description=Measures a calculus, calculates=[Calculus(id=2, name=5 * 2, left=Value(id=18, name=5, value=5.0), right=Value(id=1, name=2, value=2.0), operator=TIMES)])

Metric(id=9, name=Metric 1, description=Measures a calculus, calculates=[Calculus(id=2, name=5 * 2, left=null, right=null, operator=TIMES)])

No matter the value of the depth, I can't get the Metric node with all his children nodes, it retrieves one level deep max and returns null on the leaf nodes.

I've read in the docs that depth=-1 retrieves the fully-resolved node but doing so causes the findByName() method to fail with a null pointer: kotlin.KotlinNullPointerException: null

Here is a list of resources I've consulted and a working GitHub repository with the full code:

Final notes:

  • The entities all have default parameters because Kotlin then makes an empty constructor, I think the OGM needs it
  • I've also tried making custom queries but couldn't specify the depth value because there are different relationships and can be at different levels
  • To use the GitHub repository I linked you must have Neo4j installed, the repo has a stackoverflow-question branch with all the code.

Versions:

  • Spring boot: 2.3.0.BUILD-SNAPSHOT
  • spring-boot-starter-data-neo4j: 2.3.0.BUILD-SNAPSHOT

Thank you for helping and all feedback is welcomed!


Solution

  • The problem is not with the query depth but with the model. The Metric entity has a relation with Calculable, but Calculable itself has no relationships defined. Spring Data cannot scan all possible implementations of the Calculable interface for their relationships. If you changed Metrics.calculates type to MutableSet<Calculus>, it would work as expected.

    To see Cypher requests send to the server you can add logging.level.org.neo4j.ogm.drivers.bolt=DEBUG to the application.properties

    Request before the change:

    MATCH (n:`Metric`) WHERE n.`name` = $`name_0` WITH n RETURN n,[ [ (n)->[r_c1:`CALCULATES`]->(x1) | [ r_c1, x1 ] ] ], ID(n) with params {name_0=Metric 1}
    

    Request after the change:

    MATCH (n:`Metric`) WHERE n.`name` = $`name_0` WITH n RETURN n,[ [ (n)->[r_c1:`CALCULATES`]->(c1:`Calculus`) | [ r_c1, c1, [ [ (c1)-[r_l2:`LEFT`]-(v2:`Value`) | [ r_l2, v2 ] ], [ (c1)-[r_r2:`RIGHT`]-(v2:`Value`) | [ r_r2, v2 ] ] ] ] ] ], ID(n) with params {name_0=Metric 1}