Problem
Most examples of many-to-many relation for Spring found on the internet had a list of objects on both sides of the relation, but one of those sides was the owner of the relation. This gives us for example a list of books for a user, but we can not display a list of users for a certain book.
We were required to get a list of opposite objects on both sides (for example to get alist of books for a user, but also a list of users for one book).
On Hibernate level it wasn't a problem. Problem appeared when Jackson parser tried to change that kind of object to Json. For a linked object we obtained a neverending recursion ("user" contains a list of books, which contains a list of users, which contains a list of books etc...).
Implementation
You can find a working example-project on GitHub here:
https://github.com/bartoszkomin/hibernate-many-to-many-demo/
An example structure of the database looks as follows:
At first we've defined the entity objects in a following manner:
1: package com.blogspot.bartoszkomin.hibernate_many_to_many_demo.model;
2:
3: import java.util.List;
4: import javax.persistence.CascadeType;
5: import javax.persistence.Column;
6: import javax.persistence.Entity;
7: import javax.persistence.GeneratedValue;
8: import javax.persistence.GenerationType;
9: import javax.persistence.Id;
10: import javax.persistence.JoinColumn;
11: import javax.persistence.JoinTable;
12: import javax.persistence.ManyToMany;
13: import javax.persistence.Table;
14: import javax.persistence.UniqueConstraint;
15: import javax.validation.constraints.NotNull;
16:
17: import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
18: import com.fasterxml.jackson.annotation.JsonManagedReference;
19:
20: /**
21: * @author Bartosz Komin
22: * User entity
23: */
24: @Entity
25: @Table(name = "users")
26: public class User {
27:
28:
29: @Id
30: @GeneratedValue(strategy = GenerationType.IDENTITY)
31: private Integer id;
32:
33: @NotNull
34: @Column(nullable = false)
35: private String name;
36:
37: @ManyToMany(cascade = {CascadeType.MERGE})
38: @JoinTable(name = "UserBook",
39: joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
40: inverseJoinColumns = @JoinColumn(name = "book_id", referencedColumnName = "id"),
41: uniqueConstraints={@UniqueConstraint(columnNames={"user_id", "book_id"})})
42: private List<Book> books;
43:
44:
45:
46: public String getName() {
47: return name;
48: }
49:
50: public void setName(String name) {
51: this.name = name;
52: }
53:
54: public List<Book> getBooks() {
55: return books;
56: }
57:
58: public void setBooks(List<Book> books) {
59: this.books = books;
60: }
61:
62: public Integer getId() {
63: return id;
64: }
65:
66: }
1: package com.blogspot.bartoszkomin.hibernate_many_to_many_demo.model;
2:
3: import java.util.List;
4: import javax.persistence.CascadeType;
5: import javax.persistence.Column;
6: import javax.persistence.Entity;
7: import javax.persistence.GeneratedValue;
8: import javax.persistence.GenerationType;
9: import javax.persistence.Id;
10: import javax.persistence.JoinColumn;
11: import javax.persistence.JoinTable;
12: import javax.persistence.ManyToMany;
13: import javax.persistence.Table;
14: import javax.persistence.UniqueConstraint;
15: import javax.validation.constraints.NotNull;
16:
17: import com.fasterxml.jackson.annotation.JsonBackReference;
18: import com.fasterxml.jackson.annotation.JsonIgnore;
19: import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
20:
21: /**
22: * @author Bartosz Komin
23: * Book entity
24: */
25: @Entity
26: @Table(name = "books")
27: public class Book {
28:
29:
30: @Id
31: @GeneratedValue(strategy = GenerationType.IDENTITY)
32: private Integer id;
33:
34: @NotNull
35: @Column(nullable = false)
36: private String name;
37:
38: @ManyToMany(cascade = {CascadeType.MERGE})
39: @JoinTable(name = "UserBook",
40: joinColumns = @JoinColumn(name = "book_id", referencedColumnName = "id"),
41: inverseJoinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
42: uniqueConstraints={@UniqueConstraint(columnNames={"book_id", "user_id"})})
43: private List<User> users;
44:
45: public String getName() {
46: return name;
47: }
48:
49: public void setName(String name) {
50: this.name = name;
51: }
52:
53: public List<User> getUsers() {
54: return users;
55: }
56:
57: public void setUsers(List<User> users) {
58: this.users = users;
59: }
60:
61: public Integer getId() {
62: return id;
63: }
64:
65: }
In the presented GitHub project we can find a few REST endpoints:
POST /users
GET /users
GET /users/user_id
POST /books
GET /books
GET /books/book_id
With presented entities it works properly on Database/Hibernate level. But if we want to obtain some linked objects, we get a neverending recursion, which occurs on Jackson level, and it looks like this:
{"id":1,"name":"Albert Einstein","books":[{"id":1,"name":"The Meaning of Relativity", "users":[{"id":1,"name":"Albert Einstein","books":[{"id":1,"name":"The Meaning of Relativity", "users":{...}}]}]}]}
Solution
I've found some tips on the internet with an
@JsonIgnore or
@JsonManagedReference + @JsonBackReference annotation
, but it was not working in our case (we were able to see the list only on one side).
Our solution was to use
@JsonIgnoreProperties annotation as follows:
User class
1: @ManyToMany(cascade = {CascadeType.MERGE})
2: @JoinTable(name = "UserBook",
3: joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
4: inverseJoinColumns = @JoinColumn(name = "book_id", referencedColumnName = "id"),
5: uniqueConstraints={@UniqueConstraint(columnNames={"user_id", "book_id"})})
6: @JsonIgnoreProperties("users")
7: private List<Book> books;
Book class
1: @ManyToMany(cascade = {CascadeType.MERGE})
2: @JoinTable(name = "UserBook",
3: joinColumns = @JoinColumn(name = "book_id", referencedColumnName = "id"),
4: inverseJoinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
5: uniqueConstraints={@UniqueConstraint(columnNames={"book_id", "user_id"})})
6: @JsonIgnoreProperties("books")
7: private List<User> users;
Working example result:
GET /users/1
GET /books/1
Summary
In the GitHub demo project you can find a working example in master branch.
There is also one more branch: many-to-many-recursion-problem, where you can check how application behaves, when neverending recursion appears. My IDE got laggy, and that might cause a problem with getting a response. I removed tests from this branch to not overload the computer when trying to build a project.
On GitHub "readme" there is also some information on how to run the project and how to send a request to api with curl.