Sunday, January 15, 2017

Many to many relation with hibernate and json never ending recursion problem

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.


8 comments:

  1. Excellent!! You saved me. I owe you one beer

    ReplyDelete
  2. I am getting:
    org.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?

    ReplyDelete
  3. Hey 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!!!

    ReplyDelete
  4. Thank you very much, this is the solution that I need :)

    ReplyDelete
  5. I want to hug you. Thank you very much!!!

    ReplyDelete
  6. Awesome, great explanation about something that is not very easy to find

    ReplyDelete