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.
Excellent!! You saved me. I owe you one beer
ReplyDeleteI am getting:
ReplyDeleteorg.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]: Invocation of init method failed; nested exception is org.hibernate.cfg.RecoverableException: Unable to find column with logical name: id in org.hibernate.mapping.Table(roles) and its related supertables and secondary tables
can you give any tips how to solve it?
roles is a name of class table
ReplyDeleteHey bro, thank you very much for your help. It was the perfect solution for my case. I had the problem with the recursion and at the sema way with Deserialize and with your example in this blog everthing ran correct. Excellent!!!
ReplyDeleteThank you very much, this is the solution that I need :)
ReplyDeleteoh my god , thank very much
ReplyDeleteI want to hug you. Thank you very much!!!
ReplyDeleteAwesome, great explanation about something that is not very easy to find
ReplyDelete