This tutorial will walk you through the steps of mapping a JPA and Hibernate Many to Many extra columns relationship with single primary key in Spring Boot, Spring Data JPA, Lombok, and MySQL

What you will need

  • JDK 8+ or OpenJDK 8+

  • Maven 3+

  • MySQL Server 5+

  • Your favorite IDE

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
│       │               │   ├── Book.java
│       │               │   ├── BookPublisher.java
│       │               │   ├── BookRepository.java
│       │               │   ├── Publisher.java
│       │               │   └── PublisherRepository.java
│       │               └── JpaApplication.java
│       │
│       └── resources
│           └── application.properties
└── pom.xml

The Many To Many Extra Columns relationship mapping will be implemented in Book.java and Publisher.java

Project dependencies

We will use the following dependencies

  • spring-boot-starter-data-jpa provides Hibernate 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-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

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.

book_publisher is a join table of book and publisher with id as a single primary key column, and published_date is an extra column

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

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 {  
    @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(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 a 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>{  
}

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

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 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    |                |
+-------+--------------+------+-----+---------+----------------+
2 rows in set (0.01 sec)

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

mysql> describe book_publisher;  
+----------------+-------------+------+-----+---------+----------------+
| Field          | Type        | Null | Key | Default | Extra          |
+----------------+-------------+------+-----+---------+----------------+
| id             | int(11)     | NO   | PRI | NULL    | auto_increment |
| published_date | datetime(6) | YES  |     | NULL    |                |
| book_id        | int(11)     | YES  | MUL | NULL    |                |
| publisher_id   | int(11)     | YES  | MUL | NULL    |                |
+----------------+-------------+------+-----+---------+----------------+
4 rows in set (0.01 sec)

mysql> select * from book;  
+----+--------+
| id | name   |
+----+--------+
|  1 | Book 1 |
|  2 | Book 2 |
+----+--------+
2 rows in set (0.00 sec)

mysql> select * from publisher;  
+----+-------------+
| id | name        |
+----+-------------+
|  1 | Publisher A |
|  2 | Publisher B |
+----+-------------+
2 rows in set (0.00 sec)

mysql> select * from book_publisher;  
+----+----------------------------+---------+--------------+
| id | published_date             | book_id | publisher_id |
+----+----------------------------+---------+--------------+
|  1 | 2020-10-27 13:44:17.045000 |       1 |            1 |
|  2 | 2020-10-27 13:44:17.045000 |       1 |            2 |
|  3 | 2020-10-27 13:44:17.057000 |       2 |            1 |
+----+----------------------------+---------+--------------+
3 rows in set (0.00 sec)

Conclusion

In this tutorial, we learned to map a JPA and Hibernate Many to Many extra columns relationship in Spring Boot and MySQL. You can find the source code on Github

You may also like the following tutorials