Home > front end >  I am unable to create entity or fetch list of entities due to stack overflow error on bi-directional
I am unable to create entity or fetch list of entities due to stack overflow error on bi-directional

Time:09-14

I am working on a springboot application. I have 2 entity classes, Group and User. I also have @ManyToMany relationship defined in the Group class (Owning entity), and also in the User class, so that I can fetch all the groups a user belongs to. Unfortunately, I can't create a new group or a new user due to the following error;

{
    "timestamp": "2022-09-09T20:29:22.606 00:00",
    "status": 415,
    "error": "Unsupported Media Type",
    "message": "Content type 'application/json;charset=UTF-8' not supported"
}

When I try to fetch all groups a user belongs to by calling user.get().getGroups(); I get a a stack overflow error

Note: Currently I have @JsonManagedReference and @JsonBackReference in Group and User classes respectively. I also tried adding @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") on both classes, but this did not work either. Adding value parameter to @JsonManagedReference and @JsonBackReference as demonstrated below did not work either. What am I doing wrong? What am I missing?

This is my Group entity class

@Table(name = "`group`") // <- group is a reserved keyword in SQL
public class Group {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    @JsonView(Views.Public.class)
    private String name;
    private Integer maximumMembers;

    @ManyToMany(fetch = FetchType.LAZY, cascade = {CascadeType.ALL})
    @JoinTable(name = "group_user", joinColumns = @JoinColumn(name = "group_id"), inverseJoinColumns = @JoinColumn(name = "user_id"))
    @JsonView(Views.Public.class)
    @JsonManagedReference(value = "group-member")
    private Set<User> groupMembers;
}

This is my User entity class

public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @JsonView(Views.Public.class)
    private Long id;
    @JsonView(Views.Public.class)
    private String nickname;
    @JsonView(Views.Public.class)
    private String username; // <- Unique user's phone number
    private String password;

    @ElementCollection(targetClass = ApplicationUserRole.class)
    @CollectionTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id"))
    @Enumerated(EnumType.STRING)
    @Column(name = "role")
    private Set<ApplicationUserRole> roles;

    @ManyToMany(mappedBy = "groupMembers", fetch = FetchType.LAZY, targetEntity = Group.class)
    @JsonBackReference(value = "user-group")
    private Set<Group> groups;
}

Minimal, Reproducible Example https://github.com/Java-Techie-jt/JPA-ManyToMany

CodePudding user response:

I found a permanent solution for this problem. For anyone else facing a similar problem, This is what I found. First, my entity classes had @Data Lombok annotation. I removed this because the @Data annotation has a tendency of almost always loading collections even if you have FetchType.LAZY.

You can read more about why you should't annotate your entity class with @Data here https://www.jpa-buddy.com/blog/lombok-and-jpa-what-may-go-wrong/

After removing this annotation, I removed @JsonManagedReference and @JsonBackReference from both sides of the relationship(both entities). I then added @Jsonignore to the referencing side only(User class). This solves 2 things

  1. Creating a group with a list of users works fine
  2. Adding a list of users to a group works fine.

After this, we are left with one last problem. When we try to read a user from the api, we get a user without the associated list of groups they belong to, because we have @JsonIgnore on the user list. To solve this, I made the controller return a new object. So after fetching the user from my service, I map it to a new data transfer object, the I return this object in the controller.

From here I used @JsonView to filter my responses.

This is how my classes look, notice there is no @Data in annotations.

Group

@Builder
@Entity
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Getter
@Setter
@Table(name = "`group`") // <- group is a reserved keyword in SQL
public class Group {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String name;
    private Integer maximumMembers;

    @ManyToMany(fetch = FetchType.EAGER,
            cascade = {CascadeType.MERGE, CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH})
    @JoinTable(name = "group_user",
            joinColumns = @JoinColumn(name = "group_id"),
            inverseJoinColumns = @JoinColumn(name = "user_id"))
    @JsonView(UserViews.PublicUserDetails.class)
    private Set<User> groupMembers = new HashSet<>();

} 

User

@Builder
@Entity
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Getter
@Setter
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @JsonView(UserViews.PublicUserDetails.class)
    private Long id;
    @JsonView(UserViews.PublicUserDetails.class)
    private String nickname;
    @JsonView(UserViews.PublicUserDetails.class)
    private String username; // <- Unique user's phone number
    private String password;

    @ElementCollection(targetClass = ApplicationUserRole.class)
    @CollectionTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id"))
    @Enumerated(EnumType.STRING)
    @Column(name = "role")
    @JsonView(UserViews.PublicUserDetails.class)
    private Set<ApplicationUserRole> roles;

    @JsonIgnore
    @ManyToMany(mappedBy = "groupMembers", fetch = FetchType.LAZY, targetEntity = Group.class)
    private Set<Group> groups = new HashSet<>();
}

Method fetching user in user controller

@GetMapping("/get-groups")
    public ResponseEntity<UserRequestResponseDTO> getWithGroups(@RequestParam(name = "userId") Long userId) {
        User user = userService.getWithGroups(userId);
        UserRequestResponseDTO response = UserRequestResponseDTO.builder()
                .nickname(user.getNickname())
                .username(user.getUsername())
                .groups(user.getGroups())
                .build();
        return ResponseEntity.ok().body(response);
    }

Hopefully this helps someone

  • Related