Collectives™ on Stack Overflow

Find centralized, trusted content and collaborate around the technologies you use most.

Learn more about Collectives

Teams

Q&A for work

Connect and share knowledge within a single location that is structured and easy to search.

Learn more about Teams

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:

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:

  • GitHub Repo
  • Spring Data Neo4j Reference Documentation
  • Neo4j-OGM Docs
  • 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!

    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}
                    That is a fine catch! Thanks! is there a way I can have a relationship from Metric to Calculus or Value interchangeably? At least other than deleting the interface and just combining Calculus and Value properties in the same entity and setting null values in each case. Thank you! You've been so helpful, you deserve the bounty.
    – Ariel Mirra
                    Aug 14, 2020 at 19:34
                    You could just get rid of Value, and always use Calculus by adding var value: Double? = null to the Calculus definition and modifying its calculate() to return value if it is non-null.
    – cybersam
                    Aug 14, 2020 at 19:51
            

    Thanks for contributing an answer to Stack Overflow!

    • Please be sure to answer the question. Provide details and share your research!

    But avoid

    • Asking for help, clarification, or responding to other answers.
    • Making statements based on opinion; back them up with references or personal experience.

    To learn more, see our tips on writing great answers.