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
- Creating a group with a list of users works fine
- 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