Showing posts with label many-to-many. Show all posts
Showing posts with label many-to-many. Show all posts

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.