简体   繁体   中英

Hibernate: Infinite recursion, how to solve it?

I've very annoying problem with infinite recursion. My app is being build as Spring Rest API, and I use Lombok to generate constructors and getters and setter. I've few models.

AppUser - probably the biggest one.

@NoArgsConstructor
@AllArgsConstructor
@Data
@Entity
public class AppUser {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String login;
    private String name;
    private String surname;
    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    private String password;

    public AppUser(String login, String password, UserRole userRole) {
        this.login = login;
        this.password = password;
        this.userRoleSet = new HashSet<>();
        this.userRoleSet.add(userRole);
    }

    public AppUser(String login, String name, String surname, String password) {
        this.login = login;
        this.name = name;
        this.surname = surname;
        this.password = password;
    }

    @OneToMany(mappedBy = "appUser",fetch = FetchType.EAGER)
    private Set<UserRole> userRoleSet;

    @OneToMany(mappedBy = "createdBy")
    private List<Incident> createdIncidentsList;

    @OneToMany(mappedBy = "assignedTo")
    private List<Incident> assignedToIncidentsList;

    @OneToMany(mappedBy = "postedBy")
    private List<Comment> commentList;

    @OneToMany(mappedBy = "appUser")
    private List<IncidentChange> incidentChangeList;

}

Incident

@Data
@Entity
public class Incident {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String title;
    private String description;
    private LocalDateTime creationDate;
    private IncidentStatus status;

    @OneToMany(mappedBy = "incident", cascade = CascadeType.ALL)
    private List<Comment> commentList;
    @ManyToOne
    private AppUser createdBy;
    @ManyToOne
    private AppUser assignedTo;
    @OneToOne(cascade = CascadeType.ALL)
    private ChangeLog changeLog;

    public Incident(String title, String description) {
        this.title = title;
        this.description = description;
        this.creationDate = LocalDateTime.now();
        this.status = IncidentStatus.NEW;
        this.commentList = new ArrayList<>();
        this.changeLog = new ChangeLog();
    }
    public Incident(){

    }
    public Incident(String title, String description, LocalDateTime creationDate, IncidentStatus status, List<Comment> commentList, AppUser assignedTo, AppUser createdBy, ChangeLog changeLog) {
        this.title = title;
        this.description = description;
        this.creationDate = creationDate;
        this.status = status;
        this.commentList = commentList;
        this.changeLog = changeLog;
    }

}

Comment

@Data
@NoArgsConstructor
@Entity
public class Comment {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String description;
    private LocalDateTime creationDate;

    @ManyToOne
    private AppUser postedBy;
    @ManyToOne
    private Incident incident;

}

ChangeLog

@Data
@Entity
public class ChangeLog {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @OneToOne
    private Incident incident;
    @OneToMany(mappedBy = "changeLog")
    private List<IncidentChange> incidentChangeList;

    public ChangeLog(){
        this.incidentChangeList = new ArrayList<>();
    }

    public ChangeLog(Incident incident, List<IncidentChange> incidentChangeList) {
        this.incident = incident;
        this.incidentChangeList = incidentChangeList;
    }

}

IncidentChange

@Data
@NoArgsConstructor
@Entity
public class IncidentChange {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String description;
    private LocalDateTime changeDate;

    @ManyToOne
    private ChangeLog changeLog;
    @ManyToOne
    private AppUser appUser;
}

And it works like this in short:

AppUser has
OneToMany relation to Incident, Comment and IncidentChange.

Incident has
ManyToOne relation to AppUser (TWICE that's important),
OneToMany to Comments,
OneToOne to ChangeLog

Comment has
ManyToOne relation to AppUser,
ManyToOne to incident

ChangeLog has
OneToOne relation to Incident,
OneToMany to IncidentChange

IncidentChange has
ManyToOne relation to ChangeLog,
ManyToOne to AppUser

I know that AppUser is in Incident, and is in Comment that is nested in Incident, same situation with IncidentChange, but it's nested even deeper.

Here is code for people that would like to check how AppUser and Incident is created https://bitbucket.org/StabloPL/backendsimpleticketsystem/src/76e6bd107e68c82614fbc040b95f041fd7f51d28/src/main/java/com/djagiellowicz/ticketsystem/backendsimpleticketsystem/model/?at=master IncidentController, AppUserControler, and IncidentService, AppUserService are things that could interest somebody.

When I create user (Via Postman as JSON) and after that I fetch it, there is no problem everything works. When I create Incident and assign it to specific person, I can't fetch Incident, also I can't fetch this particual user. Other pages, where there are users without created incidents fetch without any issues. The same applies to Incidents without assigned createdBy users. Everything saves to database without any issues.

Here is error thrown by Hibernate/Spring when I try to fetch Incident. Of course a little bit cut.

java.lang.IllegalStateException: Cannot call sendError() after the response has been committed
    at org.apache.catalina.connector.ResponseFacade.sendError(ResponseFacade.java:472) ~[tomcat-embed-core-8.5.29.jar:8.5.29]
    at javax.servlet.http.HttpServletResponseWrapper.sendError(HttpServletResponseWrapper.java:129) ~[tomcat-embed-core-8.5.29.jar:8.5.29]
    at javax.servlet.http.HttpServletResponseWrapper.sendError(HttpServletResponseWrapper.java:129) ~[tomcat-embed-core-8.5.29.jar:8.5.29]


2018-04-24 11:39:43.731 ERROR 18300 --- [nio-8080-exec-4] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Infinite recursion (StackOverflowError); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError) (through reference chain: com.djagiellowicz.ticketsystem.backendsimpleticketsystem.model.AppUser["createdIncidentsList"]->org.hibernate.collection.internal.PersistentBag[0]->com.djagiellowicz.ticketsystem.backendsimpleticketsystem.model.Incident["createdBy"]->com.djagiellowicz.ticketsystem.backendsimpleticketsystem.model.AppUser["createdIncidentsList"]->org.hibernate.collection.internal.PersistentBag[0]->
java.lang.StackOverflowError: null

How can I solve it? I have to know which Comment has been posted by AppUser, the same applies to Incident and IncidentChange, and I don't want to remove these info from Incident/IncidentChange/Comment

You have to override Lombok's annotations... when you use @Data you implicitly call @EqualsAndHashCode using all fields in your classes. The infinite recursion raises from the fields you use to map relationships. @EqualsAndHashCode in each class to exclude collections fields. For example you can add the annotatio @EqualsAndHashCode(exclude={"userRoleSet", "createdIncidentsList","assignedToIncidentsList","commentList","incidentChangeList"}) on AppUser class to override @Data default policy. You have to do this on each model class until you have no infinite recursion error.

Also... you have circular dependencies between your classes. When you serialize objects you should remove all circular dependencies. The best solution is to serialize DTOs and not Entities. Creating a DTO with no circular dependencies should solve this issue. In serialization/deserialization fase you can use an AppUserDTO which contains the incident list, a list of IncidentDTO with no reference to AppUser.

You can put the @JsonIgnore annotation on fields that you don't want to include in the JSON.

If you want more flexibility you can use the @JsonView annotation.

There are also @JsonManagedReference and @JsonBackReference that you could use.

What you use is up to you. If you really don't need a property in the frontend I would suggest you use @JsonIgnore . If there are instances where you might need one property in one case and not in another I would use @JsonView .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM