cancel
Showing results for 
Search instead for 
Did you mean: 

Join the community at Nodes 2022, our free virtual event on November 16 - 17.

Kotlin Neo4J Graph Saving

wayne
Node Clone

Kotlin 1.4.31
Spring Framework Boot 2.4.4
Desktop 4.2.4
also see GitHub - Wayne-P/graph: Kotlin Neo4j Graph Issue

Possibly related to NullPointerException when persisting Kotlin data class · Issue #2141 · spring-projects/spring-data-n... ,
possibly related to Return related objects with ids set after save · Issue #2148 · spring-projects/spring-data-neo4j · G... ,
probably related to something I am overlooking owing to an Easter hangover.

So we have three classes imaginatively labelled A, B & C

@Node ("NODE_A")
data class A constructor(
    @Id @GeneratedValue val id: Long? = null, val aName: String, @Relationship(type = "A_TO_B_REL") val b: B, @Relationship(type = "A_TO_C_REL") val c: C
)
@Node ("NODE_B")
data class B constructor(
    @Id @GeneratedValue val id: Long? = null, val bName:String, @Relationship(type = "B_TO_C_REL") val cList: List<C>
)
@Node("NODE_C")
data class C constructor(
    @Id @GeneratedValue val id: Long? = null, val cName: String
)

So, roughly A has a B and a C.
B has a list of C .
So far, so good.

The application code ...

@Transactional
@SpringBootApplication
class GraphApplication {
    @Bean
    fun init(aRepository: ARepository, bRepository: BRepository, cRepository: CRepository): CommandLineRunner {
        return CommandLineRunner {
            val c1 = C(cName = "c1")
            val c2 = C(cName = "c2")
            val b1 = B(bName = "b1", cList = listOf(c1, c2))
            val a1 = A(aName = "a1", b = b1, c = c1)

            aRepository.deleteAll()
            bRepository.deleteAll()
            cRepository.deleteAll()

            aRepository.save<A>(a1)
        }
    }
}

fun main(args: Array<String>) {
    runApplication<GraphApplication>(*args)
}

Now curiously, repeated efforts to execute this code give different results.
Sometimes, an IllegalStateExcexception from the save attempt of

java.lang.IllegalStateException: Failed to execute CommandLineRunner
	at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:809) ~[spring-boot-2.4.4.jar:2.4.4]
	at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:790) ~[spring-boot-2.4.4.jar:2.4.4]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:333) ~[spring-boot-2.4.4.jar:2.4.4]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1313) ~[spring-boot-2.4.4.jar:2.4.4]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1302) ~[spring-boot-2.4.4.jar:2.4.4]
	at aoni.graph.GraphApplicationKt.main(GraphApplication.kt:39) ~[main/:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
	at java.base/java.lang.reflect.Method.invoke(Method.java:566) ~[na:na]
	at org.springframework.boot.devtools.restart.RestartLauncher.run(RestartLauncher.java:49) ~[spring-boot-devtools-2.4.4.jar:2.4.4]
Caused by: java.util.NoSuchElementException: No value present
	at java.base/java.util.Optional.get(Optional.java:148) ~[na:na]

Other times, the code runs without exception to produce two c1s with different ids
3X_7_a_7a7817d083158a38b218b98876fd365e15f19703.png
I appreciate that work is still being done in 2148 to ensure that ids of subcomponents are correctly returned in response to a save, but will this work lead to one c1 being created here ?
Or, as I suspect, should I be blaming the acetyldehyde here ?

Many thanks for any enlightenment.

2 REPLIES 2

wayne
Node Clone

for reference, I have now created a related github issue for this matter
https://github.com/spring-projects/spring-data-neo4j/issues/2223

Thanks for asking and reporting the problem on GitHub.
Quick update to avoid noisy talk on the issue. You are 100% correct with the observation of the faulty behaviour. There are several things that come together:

  1. Immutable data classes
  2. The two paths that lead to C1 within one save call
  3. The fix regarding correct set of the updated entities.

What happens here is
in the exceptional case we traverse from
A to C1 first and mark the C1 as processed (correct)
A to B to C1 but since we are starting again from A over B to C1 this C1 is the "old" one but seen as already processed because the id will be set later. SDN decides now to load the C1 from the database because it should be existing but it does not have any id to query for.
In the duplicate phase the path A - B - C1 gets processed first and C1 gets (again) registered. But now the loop for A's relationships is still in the state that C1 was never be touched and creates happy a new one.
The order is random because we get the fields from a Set-based collection from the Spring Data commons library. So in the end there are two bugs for your scenario I have to fix now
Again thanks for reporting and helping us to make SDN more stable.