HelloKoding

Practical coding guides

JPA and Hibernate Many To Many Extra Columns Relationship Mapping Example with Spring Boot, Spring Data JPA and MySQL

This tutorial will walk you through the steps of mapping a JPA and Hibernate Many to Many extra columns bidirectional entity relationships example with Spring Boot, Spring Data JPA, Lombok, MySQL and Docker

What you will 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
│       │           │   │   ├── Book.java
│       │           │   │   ├── BookPublisher.java
│       │           │   │   ├── BookRepository.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-many-to-many-extra-columns-mysql</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-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 and Repositories

Many-To-Many Relationship

Many-to-many relationship refers to the relationship between two entities/tables A and B in which one element/row of A may be linked with many elements of B, and vice versa, one member of B may be linked to many elements of A.

In this example, the book and publisher tables have a many-to-many relationship. One book may be published by many publishers and one publisher may publish many books.

JPA Many to Many Extra Columns

book_publisher is a join table of book and publisher.

book_publisher.book_id is a foreign key references to book.id, book_publisher.publisher_id is a foreign key references to publisher.id.

book_id and publisher_id is also a composite primary key of book_publisher.

Beside 2 foreign key columns, the join table book_publisher also has extra column published_date

Try this example if the join table has no extra columns JPA/Hibernate Many To Many Example of Bidirectional Relationship Mapping

Define JPA and Hibernate Entities

JPA Entity is defined with @Entity annotation, represent a table in your database.

Book.java

package com.hellokoding.jpa.book;

import lombok.Data;

import javax.persistence.*;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Data

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

    @Column(name = "name")
    private String name;

    @OneToMany(mappedBy = "book", cascade = CascadeType.ALL)
    private Set<BookPublisher> bookPublishers;

    public Book(String name, BookPublisher... bookPublishers) {
        this.name = name;
        for(BookPublisher bookPublisher : bookPublishers) bookPublisher.setBook(this);
        this.bookPublishers = Stream.of(bookPublishers).collect(Collectors.toSet());
    }
}

Publisher.java

package com.hellokoding.jpa.book;

import lombok.*;

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

@Data

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

    private String name;

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

    public Publisher(String name) {
        this.name = name;
    }
}

BookPublisher.java

package com.hellokoding.jpa.book;

import lombok.*;

import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
import java.util.Objects;

@Getter
@Setter
@NoArgsConstructor

@Entity
public class BookPublisher implements Serializable {
    @Id
    @ManyToOne
    @JoinColumn
    private Book book;

    @Id
    @ManyToOne
    @JoinColumn
    private Publisher publisher;

    private Date publishedDate;

    public BookPublisher(Publisher publisher, Date publishedDate) {
        this.publisher = publisher;
        this.publishedDate = publishedDate;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof BookPublisher)) return false;
        BookPublisher that = (BookPublisher) o;
        return Objects.equals(book.getName(), that.book.getName()) &&
                Objects.equals(publisher.getName(), that.publisher.getName()) &&
                Objects.equals(publishedDate, that.publishedDate);
    }

    @Override
    public int hashCode() {
        return Objects.hash(book.getName(), publisher.getName(), publishedDate);
    }
}

@Id declares the entity identifier.

@Column maps the entity’s field with the table’s column. If @Column is omitted, the field name of the entity will be used as column name by default.

@OneToMany and @ManyToOne defines a one-to-many relationship between 2 entities. @JoinColumn indicates the entity is the owner of the relationship and the corresponding table has a column with a foreign key to the referenced table. mappedBy indicates the entity is the inverse of the relationship.

Spring Data JPA Repository

Spring Data JPA contains some built-in Repository abstracting common functions based on EntityManager to work with database such as findAll, findById, save, delete, deleteById. All we need for this example is extends JpaRepository.

BookRepository.java

package com.hellokoding.jpa.book;

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

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

PublisherRepository.java

package com.hellokoding.jpa.book;

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

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

Define properties and creating data

Application Properties

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

hk-mysql refers to Docker Compose service defined in the below docker-compose.yml file

spring.jpa.hibernate.ddl-auto=create allows JPA/Hibernate auto create database and table schema for you.

In practice, you may like to disable the DDL Auto feature by using spring.jpa.hibernate.ddl-auto=validate or spring.jpa.hibernate.ddl-auto=none (default). Check out this example as one of the approaches Spring Boot Flyway Example of Database Evolution

Creating data with JPA and Hibernate

Thanks to CascadeType.ALL, associated entity BookPublisher will be saved at the same time with Book without the need of calling its save function explicitly.

JpaApplication.java

package com.hellokoding.jpa;

import com.hellokoding.jpa.book.*;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import java.util.Arrays;
import java.util.Date;

@RequiredArgsConstructor
@SpringBootApplication
public class JpaApplication implements CommandLineRunner {
    private final BookRepository bookRepository;
    private final PublisherRepository publisherRepository;

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

    @Override
    public void run(String... args) {
        // Create a couple of Book, Publisher and BookPublisher
        Publisher publisherA = new Publisher("Publisher A");
        Publisher publisherB = new Publisher("Publisher B");
        publisherRepository.saveAll(Arrays.asList(publisherA, publisherB));

        bookRepository.save(new Book("Book 1", new BookPublisher(publisherA, new Date()), new BookPublisher(publisherB, new Date())));
        bookRepository.save(new Book("Book 2", new BookPublisher(publisherA, new Date())));
    }
}

Run the example

Run with Docker

Prepare Dockerfile for Java/Spring Boot application and docker-compose.yml for MySQL Server

Dockerfile

FROM maven:3.5-jdk-8

docker-compose.yml

version: '3'
services:
  hk-mysql:
    container_name: hk-mysql
    image: mysql/mysql-server:5.7
    environment:
      MYSQL_DATABASE: test
      MYSQL_ROOT_PASSWORD: hellokoding
      MYSQL_ROOT_HOST: '%'
    ports:
    - "3306:3306"
    restart: always

  app:
    build: .
    volumes:
    - .:/app
    - ~/.m2:/root/.m2
    working_dir: /app
    command: mvn clean spring-boot:run
    depends_on:
    - hk-mysql

Type the below command at the project root directory, make sure your local Docker is running

docker-compose up

Access to MySQL Server docker container by issuing below bash command and key in hellokoding on Enter password:

docker exec -it hk-mysql mysql -p

Query the schema and data created by JPA/Hibernate based on your mapping.

JPA Many to Many Extra Columns

JPA Many to Many Extra Columns

Run with JDK/OpenJDK, Maven and MySQL Server local

Update hk-mysql on application.properties to localhost and type the below command at the project root directory

mvn clean spring-boot:run

Source code

https://github.com/hellokoding/hellokoding-courses/tree/master/jpa-hibernate-examples/jpa-hibernate-many-to-many-extra-columns-mysql

Follow HelloKoding