In the enterprise database architecture, it's most common to use the composite primary key. Spring Data JPA offers us two ways to map the composite primary key in the entity.
In this article, you will learn how to map the composite primary key in Spring Data JPA using the @IdClass
and @EmbeddedId
annotations, as well as the @Embeddable
annotation.
What is a composite primary key?
Before we get into the composite primary key, let's first define the primary key.
In SQL, a primary key is a constraint that is unique and not null across table rows.
Similarly, the composite key is a primary key, but the distinction between the two is that the composite key is the combination of more than one primary key in SQL to identify the unique row in the table.
Overview of Books Table
Before we go into the design of the composite key, let's first go over the table structure of the books.
In order to demonstrate this lecture, I've created a books
table with two primary keys, namely book_id and title, also known as the composite key in a table.
I have extra fields in addition to the composite key. You may construct the table on your local system by running the SQL script below, or you can use your existing table if you have one.
create database composite_key_tutorial;
use composite_key_tutorial;
create table books(
book_id BIGINT AUTO_INCREMENT,
title VARCHAR(50),
book_description VARCHAR(255),
price DOUBLE,
unit_sold BIGINT default 0,
available_quantity BIGINT default 0,
PRIMARY KEY(book_id, title)
);
Composite Key using @IdClass Annotation
Let's first use the @IdClass annotation to begin mapping the composite key in the sprind data JPA.
The jakarta.persistence
package contains the @IdClass
annotation. The @IdClass annotation defines a composite primary key class that is assigned to numerous entity fields or attributes.
Composite Primary Key Class
If we use the @IdClass annotation in the Spring Data JPA to map the composite key, we must build one POJO for the primary key fields.
I'm going to create a BooksId
class for this lecture. Remember that the books table has two primary keys: book_id
and title
. In this BooksId class, we will include both of these primary key fields.
package in.learnjavaskills.compositekeytutorial.entity;
import java.io.Serializable;
import java.util.Objects;
public class BooksId implements Serializable
{
private Long bookId;
private String title;
public BooksId()
{
}
public BooksId(Long bookId, String title)
{
this.bookId = bookId;
this.title = title;
}
@Override
public boolean equals(Object o)
{
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
BooksId booksId = (BooksId) o;
return Objects.equals(bookId, booksId.bookId) && Objects.equals(title, booksId.title);
}
@Override
public int hashCode()
{
return Objects.hash(bookId, title);
}
}
The composite primary key class must implement theSerializable
interface, have no and all-argument constructors, and define theequal
andhashcode
methods.(alert-passed)
Books Enity Class
After successfully creating the composite primary key class, ensure that you declare this ID class in your entity class using the @IdClass annotation, as shown below.
package in.learnjavaskills.compositekeytutorial.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.IdClass;
import jakarta.persistence.Table;
import java.math.BigDecimal;
import java.util.Objects;
@Entity
@Table(name = "books")
@IdClass(BooksId.class)
public class BooksEntity
{
@Id
private Long bookId;
@Id
private String title;
private String bookDescription;
private BigDecimal price;
private Long unitSold;
private Long availableQuantity;
// getter, setter, toString, hashcode, equal
}
That's all there is to it for mapping the composite primary key in Spring Data JPA using the @IdClass annotation.
Books Repository Interface
I've also made a repository to test the mapping, which is seen below. I'll run some database transactions to double-check the mapping.
package in.learnjavaskills.compositekeytutorial.repository;
import in.learnjavaskills.compositekeytutorial.entity.BooksEntity;
import in.learnjavaskills.compositekeytutorial.entity.BooksId;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface BooksRepository extends JpaRepository<BooksEntity, BooksId>
{
}
As seen in the above code, the JpaRepository interface must define generic, i.e., entity, and primary key data type.
Typically, we declare the primary key's data type. What if we use the composite primary key? In this case, we may utilize the BooksId composite key class that we generated before.
It's time to put the code to the test.
I'll use the JUnit and Mockito libraries to unit test the code. In the following test, I attempted to get all of the data from the database and insert some books into the table to confirm that the configuration was functioning properly.
package in.learnjavaskills.compositekeytutorial.repository;
import in.learnjavaskills.compositekeytutorial.entity.BooksEntity;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import java.math.BigDecimal;
import java.util.List;
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class BooksRepositoryTest
{
@Autowired
private BooksRepository booksRepository;
@Test
void testFindAll()
{
List<BooksEntity> booksRepositoryAll = booksRepository.findAll();
Assertions.assertThat(booksRepositoryAll).isNotNull()
.isNotEmpty();
}
@Test
void saveBook()
{
BooksEntity booksEntity = new BooksEntity();
booksEntity.setBookId(4L);
booksEntity.setTitle("Clean Code");
booksEntity.setBookDescription("Best book to learn writing clean code");
booksEntity.setPrice(BigDecimal.TEN);
booksEntity.setUnitSold(0L);
booksEntity.setAvailableQuantity(100L);
BooksEntity save = booksRepository.save(booksEntity);
Assertions.assertThat(save).isNotNull();
}
}
Composite Key using @EmbeddedId and @Embeddable Annotation
It's time to learn about the second approach for mapping the composite primary key in Spring Data JPA, which involves using the @Embeddable
and the @EmbeddedId
annoations.
This approach is commonly used in corporate applications. To simplify things, I'm going to utilize the same books table from the previous example to demonstrate this approach.
@Embeddable | @EmbeddedId |
---|---|
The @Embeddable annotation denotes a class whose instances are kept as an inherent element of an owner entity and share its identity. Each of the embedded object's permanent characteristics or fields is mapped to the entity's database table. |
@EmbeddedId is a unique identifier. To describe a composite primary key that is an embeddable class, this term is used to a persistent field or attribute of an entity class or mapped superclass. When the EmbeddedId annotation is used, there must be only one EmbeddedId annotation and no Id annotation |
The jakarta.persistence package has both @EmbeddedId and @Embeddable. |
Composite Primary Key Class with @Embeddable annotation
To include into the entity class, let's construct an Embeddable composite primary key class. Add each of the table's primary keys to this class.
package in.learnjavaskills.compositekeytutorial.entity;
import jakarta.persistence.Embeddable;
import java.io.Serializable;
import java.util.Objects;
@Embeddable
public class BooksId implements Serializable
{
private Long bookId;
private String title;
public BooksId()
{
}
public BooksId(Long bookId, String title)
{
this.bookId = bookId;
this.title = title;
}
@Override
public boolean equals(Object o)
{
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
BooksId booksId = (BooksId) o;
return Objects.equals(bookId, booksId.bookId) && Objects.equals(title, booksId.title);
}
@Override
public int hashCode()
{
return Objects.hash(bookId, title);
}
@Override
public String toString()
{
return "BooksId{" + "bookId=" + bookId + ", title='" + title + '\'' + '}';
}
}
In addition to implementing theSerializable
Interface and defining theequal
andhashcode
methods, the Composite primary key class needs to be annotated with the @Embeddable annotations.(alert-passed)
Books Enity Class with @EmbeddedId annotation
To map it, embed the composite primary key class into the entity class as shown below.
package in.learnjavaskills.compositekeytutorial.entity;
import jakarta.persistence.*;
import java.math.BigDecimal;
import java.util.Objects;
@Entity
@Table(name = "books")
public class BooksEntity
{
@EmbeddedId
private BooksId bookId;
private String bookDescription;
private BigDecimal price;
private Long unitSold;
private Long availableQuantity;
// getter, setter, hashcode, equal method
}
It's time to put the code to the test.
Because the BooksRepository interface remains unchanged, I have not included it in the following manner to reduce repetition. Let's run the code through unit testing to see whether the setup works as expected.
package in.learnjavaskills.compositekeytutorial.repository;
import in.learnjavaskills.compositekeytutorial.entity.BooksEntity;
import in.learnjavaskills.compositekeytutorial.entity.BooksId;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import java.math.BigDecimal;
import java.util.List;
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class BooksRepositoryTest
{
@Autowired
private BooksRepository booksRepository;
@Test
void testFindAll()
{
List<BooksEntity> booksRepositoryAll = booksRepository.findAll();
Assertions.assertThat(booksRepositoryAll).isNotNull()
.isNotEmpty();
booksRepositoryAll.forEach(System.out :: println);
}
@Test
void saveBook()
{
BooksEntity booksEntity = new BooksEntity();
BooksId booksId = new BooksId(4L, "Clean Code");
booksEntity.setBookId(booksId);
booksEntity.setBookDescription("Best book to learn writing clean code");
booksEntity.setPrice(BigDecimal.TEN);
booksEntity.setUnitSold(0L);
booksEntity.setAvailableQuantity(100L);
BooksEntity save = booksRepository.save(booksEntity);
Assertions.assertThat(save).isNotNull();
}
}
The distinctions between @IdClass and @EmbeddedId
@IdClass | @EmbeddedId | |
---|---|---|
Primary Key Redundancy | The primary key must be specified twice in the @IdClass , once in the composite primary key class and once in the enity class. |
The @EmbeddedId annotation is more robust than the @IdClass annotation since it allows us to bundle the composite primary key in one class, giving you a clear representation of the composite key fields because they are all aggregated in one single class that is only available through the enity class. |
Custom JPQL Query | It is simple to create a custom JPQL query for the primary key.
|
If you want to query the composite key, you may need to create a JPQL query like this.
|
Conclussion
We covered how to map the composite primary key in Spring Data JPA in this lecture. We study the two approaches for mapping the composite key in Spring Data JPA: @IdClass and @EmbeddedId.
If you don't mind putting more text on the custom JPQL query, don't want the redundancy of defining an ID twice, and want to bundle a composite key in one class, use @EmbeddedId.
Keep learning and keep growing.