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