For anyone wondering why you might be facing infinite recursion when working with MongoDB, Spring Boot and cyclical DbRefs/DocumentReferences, then hopefully I can help you solve the issue without wasting any time!
TL;DR: If you see Jackson and a StackOverflow in your console, look for a Component or method that's probably calling Jackson behind the scenes and loading the lazy property when Jackson converts your data into JSON. Before Jackson can force the lazy property to load in, you can grab your data, lazy load the property and set the lazy loaded property's cyclical reference property equal to null or an empty value.
HOW I MADE SENSE OF IT
In my case, I fell into the rabbit hole and was looking in all the wrong places.
Don't worry about using Lombok. Don't worry about creating getters and setters, or overriding equals, hashCode or toString!
Spring's Blog Post on the topic and the documentation on DbRef and DocumentReference might be helpful in the long run to read BUT didn't solve my issue.
If you're working with a One-To-Many relationship in MongoDB, my personal recommendation is to treat the data like in a typical SQL database.
Concrete Example:
A Person class that has several Dogs.
The Dog class will save the Person Object ID in its document via @DocumentReference
as follows:
@DocumentReference(lazy = true) private Person person;
The Person class will NOT save any reference to its List in its document because it will do the following:
@ReadOnlyProperty @DocumentReference(lookup="{'person':?#{#self._id} }") List<Dog> dogs;
@ReadOnlyProperty
will ensure that neither an array of Dog ObjectIDs nor Dog BSON objects will be saved or embedded in that Person document
@DocumentReference
will use MongoDB's '$lookup' aggregation function to find any Dog documents that contain a 'person' property with a value matching this Person document's '_id'. You can also include 'lazy = true' in the annotation if needed.
If you have a @RestController
that is calling Jackson behind the scenes:
Instead of a simple return dogRepository.findAll()
for example, you should do something to the effect of:
List<Dog> dogList = dogRepository.findAll();
for (Dog dog : dogList) {
dog.getPerson().setDogs(new ArrayList<>());
}
return dogList;
You can do similar for the Person side.
Don't forget that this applies to all return values where Jackson might decide to recursively travel through these cyclical reference properties when converting the data to JSON. Cut off the cyclical reference before it gets out of hand.
ALTERNATIVES
The alternative is embedded objects, which would require dropping @DocumentReference
or @DbRef
, letting Spring's MongoRepository save all of the reference's data into the document. Naturally, this leads to data duplication, which if the Dog class, for example, contains more than a 'name' property, then the Person document can explode in size as it saves a list of Dogs as follows:
name: "John Smith"
dog: [
{
name: "Fido"
},
{
name: "Chives"
}
]
This can be convenient for your REST API's clients BUT inconvenient in terms of MongoDB costs and updating both sides of the relationship on your backend. However, the updates CAN seemingly be solved by Spring's Lifecycle Events.
ALSO, for those who have heard that GraphQL is great for preventing overfetching and are thinking they should just switch from @RestController
to a GraphQL @Controller
to stop the recursive cyclical references then think again! Because under the hood, the lazy cyclical references still get loaded in without your permission. The JSON response you receive is the one your REST client wants, but your backend is still working overtime to parse through the endlessly cycling JSON response.
There's likely a bit more complex method that involves configuring the MappingJackson2HttpMessageConverter
that's running under the hood, which would keep from bogging down your controller with the cut-off logic BUT I'll have to save that for another day!
Thanks for the read if you've made it this far!