This tutorial walks you through the process of mapping a Hibernate unidirectional many-to-many relationship with a joined entity and single primary key in Spring Boot, Spring Data JPA, and MySQL

What you'll need

  • JDK 1.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
│       │               ├── model
│       │               │   ├── Book.java
│       │               │   ├── BookPublisher.java
|       |               |   └── Publisher.java
│       │               ├── repository
|       |               |   ├── BookRepository.java
│       │               │   ├── BookPublisherRepository.java
│       │               │   └── PublisherRepository.java
│       │               └── Application.java
│       └──resources
│          └── application.properties
└── pom.xml

The joined entity and the Many To Many unidirectional relationship mapping would be implemented in BookPublisher.java

Project dependencies

We will use the following dependencies

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

  • mysql provides MySQL Java Client

  • lombok for generating boilerplate code

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

Many-To-Many Relationship

The book and publisher tables have a many-to-many relationship via book_publisher table.

book_publisher is the joined table with id is the single primary key, book_id and publisher_id are foreign keys

Define and map JPA Entities

The many to many unidirectional mapping in JPA and Hibernate would be able to do with a joined entity and single primary key

Let's create Book, Publisher JPA Entities corresponding to book, publisher tables in the database

[Book.java]

package com.hellokoding.jpa.model;

import lombok.Data;

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

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

    private String name;

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

[Publisher.java]

package com.hellokoding.jpa.model;

import lombok.Data;

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

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

    private String name;

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

Create BookPublisher as a joined entity

[BookPublisher.java]

package com.hellokoding.jpa.model;

import lombok.Getter;  
import lombok.NoArgsConstructor;  
import lombok.Setter;

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

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

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "book_id")
    private Book book;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "publisher_id")
    private Publisher publisher;

    private Date publishedDate;

    public BookPublisher(Book book, Publisher publisher, Date publishedDate) {
        this.book = book;
        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);
    }
}

Spring Data JPA Repository

Spring Data JPA contains some built-in Repository implemented some common functions to work with the database: findOne, findAll, save,...All we need for this example is to extend it

[BookRepository.java]

package com.hellokoding.jpa.repository;

import com.hellokoding.jpa.model.Book;  
import org.springframework.data.jpa.repository.JpaRepository;

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

[PublisherRepository.java]

package com.hellokoding.jpa.repository;

import com.hellokoding.jpa.model.Publisher;  
import org.springframework.data.jpa.repository.JpaRepository;

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

[BookPublisherRepository.java]

package com.hellokoding.jpa.repository;

import com.hellokoding.jpa.model.BookPublisher;  
import com.hellokoding.jpa.model.BookPublisherId;  
import org.springframework.data.jpa.repository.JpaRepository;

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

Application Properties

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

[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

Creating data with JPA and Hibernate

Create Application.java to try creating data with Hibernate and launch the application with @SpringBootApplication

[Application.java]

package com.hellokoding.jpa;

import com.hellokoding.jpa.model.Book;  
import com.hellokoding.jpa.model.BookPublisher;  
import com.hellokoding.jpa.model.Publisher;  
import com.hellokoding.jpa.repository.BookPublisherRepository;  
import com.hellokoding.jpa.repository.BookRepository;  
import com.hellokoding.jpa.repository.PublisherRepository;  
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 Application implements CommandLineRunner {  
    private final BookRepository bookRepository;
    private final PublisherRepository publisherRepository;
    private final BookPublisherRepository bookPublisherRepository;

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

    @Override
    public void run(String... args) {
        Book b1 = new Book("Spring Boot");
        Book b2 = new Book("Spring Data JPA");
        bookRepository.saveAll(Arrays.asList(b1, b2));

        Publisher p1 = new Publisher("HelloKoding 1");
        Publisher p2 = new Publisher("HelloKoding 2");
        publisherRepository.saveAll(Arrays.asList(p1, p2));

        BookPublisher bp1 = new BookPublisher(b1, p1, new Date());
        BookPublisher bp2 = new BookPublisher(b1, p2, new Date());
        BookPublisher bp3 = new BookPublisher(b2, p1, new Date());
        BookPublisher bp4 = new BookPublisher(b2, p2, new Date());
        bookPublisherRepository.saveAll(Arrays.asList(bp1, bp2, bp3, bp4));
    }
}

Run and Test

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

mvn clean spring-boot:run

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

mysql> describe book;  
+-------+--------------+------+-----+---------+----------------+
| Field | Type         | Null | Key | Default | Extra          |
+-------+--------------+------+-----+---------+----------------+
| id    | int(11)      | NO   | PRI | NULL    | auto_increment |
| name  | varchar(255) | YES  |     | NULL    |                |
+-------+--------------+------+-----+---------+----------------+

mysql> describe publisher;  
+-------+--------------+------+-----+---------+----------------+
| Field | Type         | Null | Key | Default | Extra          |
+-------+--------------+------+-----+---------+----------------+
| id    | int(11)      | NO   | PRI | NULL    | auto_increment |
| name  | varchar(255) | YES  |     | NULL    |                |
+-------+--------------+------+-----+---------+----------------+

mysql> show create table book_publisher\G

Create Table: CREATE TABLE `book_publisher` (  
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `published_date` datetime(6) DEFAULT NULL,
  `book_id` int(11) NOT NULL,
  `publisher_id` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `FK8ywuvxfycghsfmxvu363jllpq` (`book_id`),
  KEY `FKnihk8b6sfx2mvtstq87wpjsfu` (`publisher_id`),
  CONSTRAINT `FK8ywuvxfycghsfmxvu363jllpq` FOREIGN KEY (`book_id`) REFERENCES `book` (`id`),
  CONSTRAINT `FKnihk8b6sfx2mvtstq87wpjsfu` FOREIGN KEY (`publisher_id`) REFERENCES `publisher` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

mysql> select * from book;  
+----+-----------------+
| id | name            |
+----+-----------------+
|  1 | Spring Boot     |
|  2 | Spring Data JPA |
+----+-----------------+

mysql> select * from publisher;  
+----+---------------+
| id | name          |
+----+---------------+
|  1 | HelloKoding 1 |
|  2 | HelloKoding 2 |
+----+---------------+

mysql> select * from book_publisher;  
+----+----------------------------+---------+--------------+
| id | published_date             | book_id | publisher_id |
+----+----------------------------+---------+--------------+
|  1 | 2020-11-01 13:39:28.724000 |       1 |            1 |
|  2 | 2020-11-01 13:39:28.724000 |       1 |            2 |
|  3 | 2020-11-01 13:39:28.724000 |       2 |            1 |
|  4 | 2020-11-01 13:39:28.724000 |       2 |            2 |
+----+----------------------------+---------+--------------+

Conclusion

In this tutorial, we learned to map a JPA and Hibernate Many to Many unidirectional relationship with a joined entity and single primary key in Spring Boot and MySQL. You can find the source code on Github