0

I defined these simple models

@NodeEntity(label = "Api")
class Api {
    @Id
    @Property(name = "name")
    var name: String = "name"

    @Suppress("unused")
    @Relationship(type = "EXPOSES_API", direction = Relationship.Direction.INCOMING)
    val sandboxes: MutableList<Sandbox> = mutableListOf()
}

@NodeEntity(label = "Sandbox")
class Sandbox {
    @Id
    @Property(name = "name")
    var name: String = "name"

    @Relationship(type =  "EXPOSES_API", direction = Relationship.Direction.OUTGOING)
    val apis: MutableList<Api> = mutableListOf()
}

When I load all apis and modify the sandboxes in one api (remove one sandbox and another one) instance and save this instance , the removed sandbox still remain in the list on next load.

@Test
fun checkConsistency() {
    val session = neoSession.sessionFactory.openSession()

    session.clear()

    val s1 = createSandbox("s1")
    val s2 = createSandbox("s2")
    val s3 = createSandbox("s3")
    val a1 = createApi("a1", listOf(s1, s2))
    val a2 = createApi("a2", listOf(s1, s2))

    session.save(listOf(a1, a2))

    var sandboxes = session.loadAll(Sandbox::class.java, 2)
    assertThat(sandboxes.count()).isEqualTo(2)

    var apis = session.loadAll(Api::class.java, 2)
    assertThat(apis.size).isEqualTo(2)
    assertThat(apis.fold(emptySet<Sandbox>()) { acc, a -> acc + a.sandboxes }.size).isEqualTo(2)

    a2.sandboxes.clear()
    a2.sandboxes.addAll(listOf(s1, s3))

    // Here saving without depth introduce inconsistency
    session.save(a2)

    sandboxes = session.loadAll(Sandbox::class.java, 2)
    apis = session.loadAll(Api::class.java, 2)

    assertThat(sandboxes.count()).isEqualTo(3)
    // The removed sandbox is still there
    assertThat(apis.first { it.name == "a2" }?.sandboxes?.size).isEqualTo(3)

    // So try again ...
    a2.sandboxes.clear()
    a2.sandboxes.addAll(listOf(s1, s3))

    // Here saving with depth to remove inconsistency
    session.save(a2, 2)
    assertThat(apis.first { it.name == "a2" }?.sandboxes?.size).isEqualTo(2)
}

fun createApi(name: String, sandboxList: List<Sandbox>) = Api().apply {
    this.name = name
    sandboxes += sandboxList
}

fun createSandbox(name: String) = Sandbox().apply {
    this.name = name
}

Full sample code available in my github repo here my reproducer

Thanks in advance

Patrice

1
  • Same behaviour with quarkus 3.29.3/ neo4j-ogm-quarkus 4.1.0 and quarkus 3.27.0 / neo4j-ogm-quarkus 3.16.1 Commented Nov 19 at 16:02

1 Answer 1

0

I cannot see any unexpected behaviour in there.
Let's walk through the important operations step-by-step:

session.save(listOf(a1, a2)) // first save

The resulting graph would be A1 and A2 each connected to S1 and S2.
But also worth noting that you have defined a bi-directional dependency EXPOSES_API by defining it in the Sandbox.

a2.sandboxes.clear()
a2.sandboxes.addAll(listOf(s1, s3))

// Here saving without depth introduce inconsistency
session.save(a2)

The save here will correctly set the relationships but since we are traversing all reachable paths it will also take A2->S1->A1->S2 which ends up in "recreating" (not deleting) the relationship.

sandboxes = session.loadAll(Sandbox::class.java, 2)

Instead of loading the APIs from the database, you could also do something like this and it will yield the same result (three sandboxes for "A2").

apis = sandboxes.fold(emptySet<Api>()) { acc, a -> acc + a.apis }

Why the assertion at the end works after invoking?

session.save(a2, 2)

It only updates the paths reachable for depth=2. Compared to the traversed path above, it will not come to the section "A1->S2" and re-create the relationship. Since you're not loading the Apis again in this case, the current state of A2 only describes the two incoming relationships. If you would fetch them again like you do above, Neo4j-OGM will hydrate the relationships again (outgoing ones from the Sandbox).
And how to fix this: Remove the API also from the Sandbox.

s2.apis.remove(a2)

Takeaways (or tl;dr;)

  1. Keep relationships programatically in sync when they're defined on both sides

  2. Best practice on session usage: Use a session as you would use a transaction. Load, manipulate, save with a unit of work. Don't re-use session for multiple unrelated operations/unit of work to avoid surprises when working with cached data.

(might be placed better as an issue on GitHub at https://github.com/neo4j/neo4j-ogm if there is a follow up discussion because my assumptions are wrong)

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.