0

Lets say I have a class Parent

class Parent(val id: String, val child: Child) {

    init {
        child.parent = this
    }
}

Also there is a Child class with a back reference to the parent!

class Child {

    @DBRef
    @JsonIgnore
    lateinit var parent: Parent
}

However, when I want to save and retrieve the parent with

@Autowired
lateinit var mongo: MongoOperations

val parent = Parent("1", Child())
mongo.save(parent)
mongo.findById<Parent>("1")

I get StackOverflow Exception on the mongo.findById call!

In the Exception stack it shows clearly that MongoDB has problems with resolving DBRef

at org.springframework.data.mongodb.core.convert.DefaultDbRefResolver.resolveDbRef(DefaultDbRefResolver.java:103)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.readAssociation(MappingMongoConverter.java:400)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.readProperties(MappingMongoConverter.java:354)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.populateProperties(MappingMongoConverter.java:295)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:275)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:245)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.readValue(MappingMongoConverter.java:1491)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter$MongoDbPropertyValueProvider.getPropertyValue(MappingMongoConverter.java:1389)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter$AssociationAwareMongoDbPropertyValueProvider.getPropertyValue(MappingMongoConverter.java:1438)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter$AssociationAwareMongoDbPropertyValueProvider.getPropertyValue(MappingMongoConverter.java:1401)
at org.springframework.data.mapping.model.PersistentEntityParameterValueProvider.getParameterValue(PersistentEntityParameterValueProvider.java:71)
at org.springframework.data.mapping.model.SpELExpressionParameterValueProvider.getParameterValue(SpELExpressionParameterValueProvider.java:49)
at org.springframework.data.convert.ClassGeneratingEntityInstantiator$EntityInstantiatorAdapter.extractInvocationArguments(ClassGeneratingEntityInstantiator.java:250)
at org.springframework.data.convert.ClassGeneratingEntityInstantiator$EntityInstantiatorAdapter.createInstance(ClassGeneratingEntityInstantiator.java:223)
at org.springframework.data.convert.ClassGeneratingEntityInstantiator.createInstance(ClassGeneratingEntityInstantiator.java:84)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:272)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:245)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.bulkReadAndConvertDBRefs(MappingMongoConverter.java:1556)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.readAndConvertDBRef(MappingMongoConverter.java:1516)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.potentiallyReadOrResolveDbRef(MappingMongoConverter.java:1509)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.readValue(MappingMongoConverter.java:1487)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter$MongoDbPropertyValueProvider.getPropertyValue(MappingMongoConverter.java:1389)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.getValueInternal(MappingMongoConverter.java:991)

This shown stack continues until Stackoverflow.

So, how to solve the problem with Kotlin and MongoDB handling Back-References?

Dachstein
  • 3,994
  • 6
  • 34
  • 61

2 Answers2

0

Here's the gist.

@JsonIgnore is for serialization. In your case, what you need is @JsonManagedReference and @JsonBackReference.

class Child {
    @DBRef
    @JsonBackReference
    lateinit var parent: Parent
}

And in your parent class, but it should be something along those lines:

class Parent(val id: String, @JsonManagedReference val child: Child) {
    init {
        child.parent = this
    }
}

You may need to change my example a bit, but that's the idea.

Alexey Soshin
  • 16,718
  • 2
  • 31
  • 40
  • Jackson works perfectly with this setup. I have tried this setup and successfully serialised and deserialised with Jackson. So Jackson is not the problem. My best estimate is that Spring Data MongoDB core mappers (see Stacktrace above) cannot handle the recursion. I have filed a Spring Data MongoDB bug on JIRA. – Dachstein Jun 12 '19 at 08:02
0

The main problem is that Spring Data Mongo cannot handle the primary constructor in the Parent class when it sees the Child property and it gets in a cyclic loop, hence the Stackoverflow. However, Jackson Mapper has no problem with serialisation and de-serialisation!

I opened up an issue on Spring Data MongoDB JIRA DATA MONGO 2299 where they suggested me Solution 1. However, its not really Kotlin idiomatic.

I hope that MongoDB has a more native solution out of the box since Jackson can handle cyclic references well.

Meanwhile, I investigated a bit time and found two more alternatives. All 3 solutions work to serialise and de-serialise with both frameworks: Spring Data MongoDB and Jackson Mapper.

Solution 1
Pros: very concise
Cons: not really Kotlin idiomatic (var id instead val id, open class Parent)

class Child {

    // In order to workaround the StackOverflow problem we lazy initialise the property.
    @DBRef(lazy = true)
    @JsonIgnore
    lateinit var parent: Parent
}

// However, laziness requires Spring Data Mongo framework to subclass our Parent, hence we have to delcare it open
open class Parent(var id: String, val child: Child) {
    init { child.parent = this }
}

Solution 2
Pros: Protected primary constructor used for all properties which both frameworks can handle

class Child {
    @DBRef
    @JsonBackReference
    lateinit var parent: Parent
}


// Primary constructor called by MongoDB & Jackson for de-serialisation
class Parent protected constructor(val id: String) {

    // Secondary constructor called by us
    constructor(id: String, child: Child): this(id) {
        this.child = child
        this.child.parent = this
    }

    // We need @JsonManagedReference/@JsonBackReference on the child.
    // Ohterwise we get `UninitializedPropertyAccessException`if Jackson de-serialize it and we try to access the parent property on the child
    @JsonManagedReference
    lateinit var child: Child
}

Solution 3

Pros: Full control over the constructors
Cons: Verbose

class Child {
    @DBRef
    @JsonIgnore
    lateinit var parent: Parent
}

class Parent {
    val id: String
    lateinit var child: Child


    // Called by MongoDB
    @PersistenceConstructor
    protected constructor(id: String) {
        this.id = id
    }

    // Called by Jackson Mapper & us
    constructor(id: String, child: Child) {
        this.id = id
        this.child = child
        this.child.parent = this
    }
}

Hope that helps somebody with similar problems.

Dachstein
  • 3,994
  • 6
  • 34
  • 61