Freki
Freki

Reputation: 167

Infinite loop when saving multiple entities with a post-method in Spring boot

To explain the problem I'm dealing with I will first provide the code.

RecipeController

@RequestMapping(path = "/addrecipe")
public void addNewRecipe(@RequestBody AddRecipeDto addRecipeDto){
    Recipe newRecipe = new Recipe();
    EvaUser user = evaUserRepository.findOne(addRecipeDto.getUserId());
    for(Ingredient ingredient: addRecipeDto.getIngredients()){
        ingredientRepository.save(ingredient);
    }
    newRecipe.setTitle(addRecipeDto.getTitle());
    newRecipe.setAuthor(user);
    newRecipe.setDescription(addRecipeDto.getDescription());
    newRecipe.setIngredients(addRecipeDto.getIngredients());
    recipeRepository.save(newRecipe);
    user.getMyRecipes().add(newRecipe);
    evaUserRepository.save(user);
}

UserController

@RequestMapping("/getusers")
public Iterable<EvaUser> getAllUsers() {
    return evaUserRepository.findAll();
}

EvaUser

@OneToMany
private List<Recipe> myRecipes;

@ManyToMany
private List<Recipe> favoriteRecipes;

Recipe

@ManyToOne
private EvaUser author;

Exception

Failed to write HTTP message: 
org.springframework.http.converter.HttpMessageNotWritableException: Could 
not write content: Infinite recursion

Problem

So when I call the method to add a recipe, I want the database to know that there is a new recipe and that the new recipe is linked to the user who added it. When I drop the part where I save the user-entity, the mapping isn't made at all. But when I use the userRepository to tell the database that there has been made a change (adding the recipe to their list) it seems like there is an infinite loop of adding new users.

Upvotes: 1

Views: 1306

Answers (1)

J-Alex
J-Alex

Reputation: 7117

Answering to your question and including the last requirements from your comments.

If you want to break the loop, but some somehow want to keep also nested objects, I would recommend to write a custom serializer and replace the the object which causes the endless recursion with some other field (I used author username which is String instead of Author object in the example below).

To reproduce the case I created a mock model which is similar to yours.

Recipe:

public class Recipe {

    private EvaUser author;
    private String name = "test";
    private String ingridients = "carrots, tomatos";

    public EvaUser getAuthor() {
        return author;
    }

    public void setAuthor(EvaUser author) {
        this.author = author;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getIngridients() {
        return ingridients;
    }

    public void setIngridients(String ingridients) {
        this.ingridients = ingridients;
    }
}

EvaUser:

public class EvaUser {

    private List<Recipe> myRecipes = new ArrayList<>();
    private List<Recipe> favoriteRecipes = new ArrayList<>();
    private String username;

    public List<Recipe> getMyRecipes() {
        return myRecipes;
    }

    public void setMyRecipes(List<Recipe> myRecipes) {
        this.myRecipes = myRecipes;
    }

    public List<Recipe> getFavoriteRecipes() {
        return favoriteRecipes;
    }

    public void setFavoriteRecipes(List<Recipe> favoriteRecipes) {
        this.favoriteRecipes = favoriteRecipes;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }
}

Creating a custom serializer:

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;

import java.io.IOException;
import java.util.Optional;

public class RecipeSerializer extends StdSerializer<Recipe> {

    protected RecipeSerializer() {
        this(null);
    }

    protected RecipeSerializer(Class<Recipe> t) {
        super(t);
    }

    @Override
    public void serialize(Recipe recipe, JsonGenerator gen, SerializerProvider provider) throws IOException {
        gen.writeStartObject();

        gen.writeStringField("name", recipe.getName());
        gen.writeStringField("author", Optional.ofNullable(recipe.getAuthor().getUsername()).orElse("null"));
        gen.writeStringField("ingridients", recipe.getIngridients());

        gen.writeEndObject();
    }
}

Applying serializer:

@JsonSerialize(using = RecipeSerializer.class)
public class Recipe {
    // model entity
}

JSON response body of EvaUser from controller (previous one was StackOverflowError):

{
    "myRecipes": [
        {
            "name": "soup",
            "author": "user1",
            "ingridients": "carrots, tomatos"
        },
        {
            "name": "steak",
            "author": "user1",
            "ingridients": "meat, salt"
        }
    ],
    "favoriteRecipes": [
        {
            "name": "soup",
            "author": "user1",
            "ingridients": "carrots, tomatos"
        },
        {
            "name": "steak",
            "author": "user1",
            "ingridients": "meat, salt"
        }
    ],
    "username": "user1"
}

Upvotes: 1

Related Questions