HelloKoding

Practical coding guides

JPA and Hibernate Circular References Handling in Spring Data REST

This tutorial will walk you through the steps of creating a REST API Example with Spring Data REST and handling circular references / dependencies of JPA and Hibernate entity bidirectional relationships with Jackson JsonIgnoreProperties

What you’ll need

  • JDK 8+ or OpenJDK 8+
  • Maven 3+
  • MySQL Server 5+ or Docker CE 18+

Init project structure and dependencies

Project structure

├── 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
│       │           └── springboot
│       └── resources
│           └── application.properties
├── Dockerfile
├── docker-compose.yml
└── pom.xml

Project dependencies

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.hellokoding.jpa</groupId>
    <artifactId>jpa-hibernate-circular-reference-jackson</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.1.RELEASE</version>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <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>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

Define JPA Entities

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;

@Entity

@Data
@EqualsAndHashCode(exclude = {"category", "authors", "bookPublishers"})
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<>();
}

Define Repositories

Spring Data REST will auto create RESTful APIs based on your domain model and repository, all you have to do in this example is to extend JpaRepository

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>{
}

Config and Run

Application Configurations

application.properties

spring.datasource.url=jdbc:mysql://hk-mysql: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

JpaApplication.java

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);
    }
}

Run with JDK/OpenJDK and Maven

Type the below command at the project root directory

mvn clean spring-boot:run

Test circular references handling and RESTful APIs with curl

Create a new book

curl -i -X POST -H "Content-Type:application/json" -d "{\"title\" : \"Hello Koding\", \"description\": \"Simple coding examples and tutorials\"}" http://localhost:8080/books 

Spring Data REST

Create a new author

curl -i -X POST -H "Content-Type:application/json" -d "{\"name\":\"Author 1\"}" http://localhost:8080/authors

Spring Data REST

Assign the author to the book

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

Find all books

curl http://localhost:8080/books

Find authors of book id 1

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

Spring Data REST

Find books of author id 1

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

Spring Data REST

Source code

https://github.com/hellokoding/hellokoding-courses/tree/master/jpa-hibernate-examples/jpa-hibernate-circular-reference-jackson

Follow HelloKoding