This tutorial will walk you through the steps of creating an example on handling circular references/dependencies of JPA and Hibernate entity bidirectional relationships with Jackson @JsonIgnoreProperties, Spring Data REST and MySQL

In practice, you may also like to handle the JPA and Hibernate circular references/dependencies problem with the DTO design pattern. Check out the following tutorial as one of the approaches
MapStruct Example of Mapping JPA/Hibernate Entity with DTO

What you'll need

  • JDK 8+ or OpenJDK 8+

  • Maven 3+

  • MySQL Server 5+

Init project structure

You can create and init a new Spring Boot project by using Spring Initializr or your IDE

Following is the final project structure with all the files we would create

├── src
│   └── main
│       ├── java
│       │   └── com
│       │       └── hellokoding
│       │           ├── jpa
│       │               ├── book
│       │               │   ├── Author.java
│       │               │   ├── AuthorRepository.java
│       │               │   ├── Book.java
│       │               │   ├── BookPublisher.java
│       │               │   ├── BookPublisherRepository.java
│       │               │   ├── BookRepository.java
│       │               │   ├── Category.java
│       │               │   ├── CategoryRepository.java
│       │               │   ├── Publisher.java
│       │               │   └── PublisherRepository.java
│       │               └── JpaApplication.java
│       └── resources
│           └── application.properties
└── pom.xml

Project dependencies

We will use the following dependencies

  • spring-boot-starter-data-rest auto-builds REST API based on JPA and Hibernate entities

  • spring-boot-starter-data-jpa provides Hibernate ORM and autoconfigure Spring DataSource

  • mysql-connector-java provides MySQL Java Client

  • lombok for generating boilerplate-code

<dependency>  
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>  
<dependency>  
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>  
<dependency>  
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>  
<dependency>  
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

Define JPA and Hibernate Entities

Suppose we are going to define and map relationships for the following entities: Book, Category, Author, and Publisher

Book has multiple bidirectional relationship with others which can cause a StackOverflow error. Here we use @JsonIgnoreProperties annotation to exclude a property of an association from JSON serializing and deserializing, so prevent the circular references issue

[Book.java]

package com.hellokoding.jpa.book;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;  
import lombok.Data;  
import lombok.EqualsAndHashCode;

import javax.persistence.*;  
import java.util.HashSet;  
import java.util.Set;

@Data @EqualsAndHashCode(exclude = {"category", "authors", "bookPublishers"})
@Entity
public class Book{  
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    private String title;

    private String description;

    @ManyToOne
    @JoinColumn(name = "category_id")
    @JsonIgnoreProperties("books")
    private Category category;

    @ManyToMany(cascade = CascadeType.ALL)
    @JoinTable(name = "book_author", joinColumns = @JoinColumn(name = "book_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "author_id", referencedColumnName = "id"))
    @JsonIgnoreProperties("books")
    private Set<Author> authors;

    @OneToMany(mappedBy = "book", cascade = CascadeType.ALL, orphanRemoval = true)
    @JsonIgnoreProperties("book")
    private Set<BookPublisher> bookPublishers = new HashSet<>();
}

[Category.java]

package com.hellokoding.jpa.book;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;  
import lombok.Data;  
import lombok.EqualsAndHashCode;

import javax.persistence.*;  
import java.util.Set;

@Entity
@Data
@EqualsAndHashCode(exclude = "books")
public class Category {  
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    private String name;

    @OneToMany(mappedBy = "category", cascade = CascadeType.ALL)
    @JsonIgnoreProperties("category")
    private Set<Book> books;
}

[Author.java]

package com.hellokoding.jpa.book;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;  
import lombok.Data;  
import lombok.EqualsAndHashCode;

import javax.persistence.*;  
import java.util.Set;

@Entity
@Data
@EqualsAndHashCode(exclude = "books")
public class Author {  
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    private String name;

    @ManyToMany(mappedBy = "authors")
    @JsonIgnoreProperties("authors")
    private Set<Book> books;
}

[BookPublisher.java]

package com.hellokoding.jpa.book;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;  
import lombok.Data;

import javax.persistence.*;  
import java.util.Date;

@Entity
@Data
public class BookPublisher {  
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    @ManyToOne
    @JoinColumn(name = "book_id")
    @JsonIgnoreProperties("bookPublishers")
    private Book book;

    @ManyToOne
    @JoinColumn(name = "publisher_id")
    @JsonIgnoreProperties("bookPublishers")
    private Publisher publisher;

    private Date publishedDate;
}

[Publisher.java]

package com.hellokoding.jpa.book;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;  
import lombok.Data;  
import lombok.EqualsAndHashCode;

import javax.persistence.*;  
import java.util.HashSet;  
import java.util.Set;

@Entity
@Data
@EqualsAndHashCode(exclude = "bookPublishers")
public class Publisher {  
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    private String name;

    @OneToMany(mappedBy = "publisher", cascade = CascadeType.ALL, orphanRemoval = true)
    @JsonIgnoreProperties("publisher")
    private Set<BookPublisher> bookPublishers = new HashSet<>();
}

Jackson @JsonIgnoreProperties will prevent specified fields from being serialized or deserialized.

Lombok @EqualsAndHashCode with exclude will ignore specified fields on the generated equals and hashCode function of Lombok @Data

Define Repositories

[BookRepository.java]

package com.hellokoding.jpa.book;

import org.springframework.data.jpa.repository.JpaRepository;

public interface BookRepository extends JpaRepository<Book, Integer>{  
}

[CategoryRepository.java]

package com.hellokoding.jpa.book;

import org.springframework.data.jpa.repository.JpaRepository;

public interface CategoryRepository extends JpaRepository<Category, Integer> {  
}

[AuthorRepository.java]

package com.hellokoding.jpa.book;

import org.springframework.data.jpa.repository.JpaRepository;

public interface AuthorRepository extends JpaRepository<Author, Integer>{  
}

[BookPublisherRepository.java]

package com.hellokoding.jpa.book;

import org.springframework.data.jpa.repository.JpaRepository;

public interface BookPublisherRepository extends JpaRepository<BookPublisher, Integer>{  
}

[PublisherRepository.java]

package com.hellokoding.jpa.book;

import org.springframework.data.jpa.repository.JpaRepository;

public interface PublisherRepository extends JpaRepository<Publisher, Integer>{  
}

Spring Data REST will auto create RESTful APIs based on your domain model and repository.

Application Properties

Configure the Spring Datasource JDBC URL, user name, and password of your local MySQL server in application.properties

spring.datasource.url=jdbc:mysql://localhost:3306/test?useSSL=false  
spring.datasource.username=root  
spring.datasource.password=hellokoding  
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

spring.jpa.hibernate.ddl-auto=create  
spring.jpa.database-platform=org.hibernate.dialect.MySQL57Dialect  
spring.jpa.generate-ddl=true  
spring.jpa.show-sql=true

Create the test database in your local MySQL server if not exists

We don't have to create table schemas, the ddl-auto=create config allows JPA and
Hibernate does that based on the entity-relationship mappings. In practice, consider to use ddl-auto=none (default) and use a migration tool such as Flyway for better database management

spring.jpa.show-sql=true for showing generated SQL queries in the application logs, consider to disable it on production environment

Run the application

We use @SpringBootApplication to launch the application

package com.hellokoding.jpa;

import org.springframework.boot.SpringApplication;  
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class JpaApplication {  
    public static void main(String[] args) {
        SpringApplication.run(JpaApplication.class, args);
    }
}

Type the mvn command at the project root directory to start the application

mvn clean spring-boot:run

Test circular references handling and RESTful APIs with curl

1) Create a couple of new books

Create a first book

curl -X POST http://localhost:8080/books \  
-H 'Content-Type: application/json; charset=utf-8' \
--data-binary @- << EOF
{
    "title": "Spring Boot In Practice",
    "description": "Spring Boot tutorials and guides"
}
EOF

Output

{
  "title" : "Spring Boot In Practice",
  "description" : "Spring Boot tutorials and guides",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/books/1"
    },
    "book" : {
      "href" : "http://localhost:8080/books/1"
    },
    "category" : {
      "href" : "http://localhost:8080/books/1/category"
    },
    "authors" : {
      "href" : "http://localhost:8080/books/1/authors"
    },
    "bookPublishers" : {
      "href" : "http://localhost:8080/books/1/bookPublishers"
    }
  }
}

Create a second book

curl -X POST http://localhost:8080/books \  
-H 'Content-Type: application/json; charset=utf-8' \
--data-binary @- << EOF
{
    "title": "Java",
    "description": "Java tutorials and guides"
}
EOF

Output

{
  "title" : "Java",
  "description" : "Java tutorials and guides",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/books/2"
    },
    "book" : {
      "href" : "http://localhost:8080/books/2"
    },
    "category" : {
      "href" : "http://localhost:8080/books/2/category"
    },
    "authors" : {
      "href" : "http://localhost:8080/books/2/authors"
    },
    "bookPublishers" : {
      "href" : "http://localhost:8080/books/2/bookPublishers"
    }
  }
}

2) Create a new author

curl -X POST http://localhost:8080/authors \  
-H 'Content-Type: application/json; charset=utf-8' \
--data-binary @- << EOF
{
    "name": "Giau Ngo"
}
EOF

Output

{
  "name" : "Giau Ngo",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/authors/1"
    },
    "author" : {
      "href" : "http://localhost:8080/authors/1"
    },
    "books" : {
      "href" : "http://localhost:8080/authors/1/books"
    }
  }
}

3) Assign an author to a book

curl -i -X PUT http://localhost:8080/books/1/authors \  
-H 'Content-Type: text/uri-list;' \
-d http://localhost:8080/authors/1

Output

HTTP/* 204

4) Find all books

curl http://localhost:8080/books

Output

{
  "_embedded" : {
    "books" : [ {
      "title" : "Spring Boot In Practice",
      "description" : "Spring Boot tutorials and guides",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/books/1"
        },
        "book" : {
          "href" : "http://localhost:8080/books/1"
        },
        "category" : {
          "href" : "http://localhost:8080/books/1/category"
        },
        "authors" : {
          "href" : "http://localhost:8080/books/1/authors"
        },
        "bookPublishers" : {
          "href" : "http://localhost:8080/books/1/bookPublishers"
        }
      }
    }, {
      "title" : "Java",
      "description" : "Java tutorials and guides",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/books/2"
        },
        "book" : {
          "href" : "http://localhost:8080/books/2"
        },
        "category" : {
          "href" : "http://localhost:8080/books/2/category"
        },
        "authors" : {
          "href" : "http://localhost:8080/books/2/authors"
        },
        "bookPublishers" : {
          "href" : "http://localhost:8080/books/2/bookPublishers"
        }
      }
    } ]
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/books"
    },
    "profile" : {
      "href" : "http://localhost:8080/profile/books"
    }
  },
  "page" : {
    "size" : 20,
    "totalElements" : 2,
    "totalPages" : 1,
    "number" : 0
  }
}

5) Find authors of book id 1

curl http://localhost:8080/books/1/authors

Output

{
  "_embedded" : {
    "authors" : [ {
      "name" : "Giau Ngo",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8081/authors/1"
        },
        "author" : {
          "href" : "http://localhost:8081/authors/1"
        },
        "books" : {
          "href" : "http://localhost:8081/authors/1/books"
        }
      }
    } ]
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:8081/books/1/authors"
    }
  }
}

6) Find books of author id 1

curl http://localhost:8080/authors/1/books

Output

{
  "_embedded" : {
    "books" : [ {
      "title" : "Spring Boot In Practice",
      "description" : "Spring Boot tutorials and guides",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/books/1"
        },
        "book" : {
          "href" : "http://localhost:8080/books/1"
        },
        "category" : {
          "href" : "http://localhost:8080/books/1/category"
        },
        "authors" : {
          "href" : "http://localhost:8080/books/1/authors"
        },
        "bookPublishers" : {
          "href" : "http://localhost:8080/books/1/bookPublishers"
        }
      }
    } ]
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/authors/1/books"
    }
  }
}

Conclusion

In this tutorial, we learned to use @JsonIgnoreProperties to handle the circular references issue in JPA and Hibernate bidirectional relationship mapping in Spring Boot, Spring Data REST and MySQL. You can find the source code on Github