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