This tutorial walks you through the process of mapping a Hibernate many-to-many extra columns relationship with a joined entity and composite primary keys 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
│ │ │ ├── BookPublisherId.java
| | | └── Publisher.java
│ │ ├── repository
| | | ├── BookRepository.java
│ │ │ ├── BookPublisherRepository.java
│ │ │ └── PublisherRepository.java
│ │ └── Application.java
│ └──resources
│ └── application.properties
└── pom.xml
The Many To Many Extra Columns relationship mapping would be implemented in Book.java, Publisher.java, and BookPublisher.java
The joined entity would be defined in BookPublisher.java
The composite primary keys would be defined in BookPublisherId.java and embedded into BookPublisher.java as a primary key
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.
Besides two composite primary keys: book_id
and publisher_id
, the joined table book_publisher
has published_date
as an extra column
Define and map JPA Entities
The many to many with extra columns mapping would be able to do with a joined entity and composite primary keys
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;
@OneToMany(mappedBy = "book", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<BookPublisher> bookPublishers = new HashSet<>();
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;
@OneToMany(mappedBy = "publisher")
private Set<BookPublisher> bookPublishers = new HashSet<>();
public Publisher(String name){
this.name = name;
}
}
The composite primary keys would be defined in a Serializable class with @Embeddable class annotation and injected into the joined entity as a primary key with @EmbeddedId
Let's create BookPublisherId to define composite primary keys
[BookPublisherId.java]
package com.hellokoding.jpa.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import java.io.Serializable;
@Data @AllArgsConstructor @NoArgsConstructor
@Embeddable
public class BookPublisherId implements Serializable {
@Column(name = "book_id")
private Integer bookId;
@Column(name = "publisher_id")
private Integer publisherId;
}
Create BookPublisher as a joined entity and define its primary key as BookPublisherId with @EmbeddedId
[BookPublisher.java]
package com.hellokoding.jpa.model;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.persistence.*;
import java.util.Date;
@Getter @Setter @NoArgsConstructor
@Entity
@Table(name = "book_publisher")
public class BookPublisher {
@EmbeddedId
private BookPublisherId id;
@ManyToOne
@MapsId("bookId")
@JoinColumn(name = "book_id")
private Book book;
@ManyToOne
@MapsId("publisherId")
@JoinColumn(name = "publisher_id")
private Publisher publisher;
@Column(name = "published_date")
private Date publishedDate;
public BookPublisher(Book book, Publisher publisher, Date publishedDate) {
this.id = new BookPublisherId(book.getId(), publisher.getId());
this.book = book;
this.publisher = publisher;
this.publishedDate = 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
To persist the joined entity in many-to-many, the @EmbededId value need to be filled manually as Hibernate would not be able to set the value via reflection, otherwise, you would get the following error in the console
Caused by: org.hibernate.PropertyAccessException: Could not set field value by reflection
On the above entity mapping for BookPublisher, we did the configuration via its constructor
@Entity
public class BookPublisher {
public BookPublisher(Book book, Publisher publisher, Date publishedDate) {
this.id = new BookPublisherId(book.getId(), publisher.getId());
this.book = book;
this.publisher = publisher;
this.publishedDate = publishedDate;
}
}
Let's 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 javax.transaction.Transactional;
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(Application.class, args);
}
@Override
@Transactional
public void run(String... strings) throws Exception {
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` (
`book_id` int(11) NOT NULL,
`publisher_id` int(11) NOT NULL,
`published_date` datetime(6) DEFAULT NULL,
PRIMARY KEY (`book_id`,`publisher_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 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;
+---------+--------------+----------------------------+
| book_id | publisher_id | published_date |
+---------+--------------+----------------------------+
| 1 | 1 | 2020-10-31 21:45:40.814000 |
| 1 | 2 | 2020-10-31 21:45:40.814000 |
| 2 | 1 | 2020-10-31 21:45:40.814000 |
| 2 | 2 | 2020-10-31 21:45:40.814000 |
+---------+--------------+----------------------------+
Conclusion
In this tutorial, we learned to map a JPA and Hibernate Many to Many extra columns relationship with a joined entity and composite primary keys in Spring Boot and MySQL. You can find the source code on Github