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 DataSourcemysql
provides MySQL Java Clientlombok
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