2

I have the following classes :

public class Person
{
    @Id
    ObjectId Id;

    String name;

    @DBRef(lazy = true)
    List<Entity> entities;

    /* Getters and setters omitted for brevity. */
}



public class Entity
{
    @Id
    ObjectId Id;

    String entityName;

    @DBRef(lazy = true)
    List<Person> people;

    /* Getters and setters omitted for brevity. */
}

Now, for some reason I am getting an infinite loop when I try to work with these links... I thought lazy = true prevented this, does anybody know what I am doing wrong?

IWishIWasABarista
  • 413
  • 2
  • 4
  • 19

2 Answers2

1

There is an issue on Spring Data Mongo which discusses the option to make DbRefs lazy by default in order to avoid such stack overflows. It also refers to two other issues that trigger the same infinite recursion:

a StackOverflow exception can only happen when DBRef s, whether lazy or not, are placed in the constructor.

Not sure if the "only" is a typo. And

if someone overrides Object's methods ("equals", "hashCode" or "toString") in its @Document entity that is likely to trigger the resolution of the DBRef

So a possible reason for your problem is, that something else is triggering the recursive resolution.

You might find the culprit by inspecting the top of the stack trace.

UPDATE

Based on your comment, this does not seem to be Spring Data related, but a problem with Jackson.

There are actually answers available on SO how to solve this problem. This one sounds promising to me https://stackoverflow.com/a/4126846/66686

Community
  • 1
  • 1
Jens Schauder
  • 77,657
  • 34
  • 181
  • 348
  • Hey! All of the stack trace info seems to just mention jackson. All I am actually doing is an insert of an Entity and an insert of a Person. Insert gives me the instances created (with Ids, etc) back upon completion... so then I add the entity to the entities list in the Person instance, and the Person to the peoples list in Entity, and do a save on both. At which point, it seems to go nuts... Am I doing that wrong? – IWishIWasABarista May 21 '17 at 17:45
  • That sounds like Spring Data Mongo is working just fine. But you get a problem with Jackson. Updated the answer with a link to a possible solution. – Jens Schauder May 22 '17 at 07:25
0

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!

Nick C
  • 141
  • 1
  • 5