Use embeddable objects to join two primary keys into one composite key
Every JPA entity has a primary key, but some entities have more than one value as their primary key. In this case, you need to use a composite key. This Java tip introduces you to using composite keys in JPA and Hibernate.
When you need a composite key
Consider a product pricing table that stores product prices based on both a region name and a product ID. In this case, your table could include multiple rows with the same product ID, but each associated with a different region. Youโll need both the product ID and the region name to uniquely differentiate between product prices in different regions.
Weโll use two JPA constructs to solve this problem:
- Embeddable Object: Weโll create a new class,
ProductPriceId, that is annotated with the@Embeddableannotation and holds both the product ID and the region name, which represents the primary key of a product price. - Embedded ID: Weโll create the
ProductPriceentity and reference theProductPriceIdas itsid, using the@EmbeddableIdannotation.
To get started, study the source code for the ProductPriceId and ProductPrice classes shown below.
Listing 1. ProductPriceId.java
package com.geekcap.javaworld.jpa.model;
import javax.persistence.Embeddable;
import java.io.Serializable;
@Embeddable
public class ProductPriceId implements Serializable {
private String region;
private Integer productId;
public ProductPriceId() {
}
public ProductPriceId(String region, Integer productId) {
this.region = region;
this.productId = productId;
}
public String getRegion() {
return region;
}
public void setRegion(String region) {
this.region = region;
}
public Integer getProductId() {
return productId;
}
public void setProductId(Integer productId) {
this.productId = productId;
}
@Override
public String toString() {
return "ProductPriceId{" +
"region='" + region + ''' +
", productId=" + productId +
'}';
}
}
Listing 2. ProductPrice.java
package com.geekcap.javaworld.jpa.model;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import javax.persistence.Table;
@Entity
@Table(name = "PRODUCT_PRICE")
public class ProductPrice {
@EmbeddedId
private ProductPriceId id;
private Double price;
public ProductPrice() {
}
public ProductPrice(ProductPriceId id, Double price) {
this.id = id;
this.price = price;
}
public ProductPriceId getId() {
return id;
}
public void setId(ProductPriceId id) {
this.id = id;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
@Override
public String toString() {
return "ProductPrice{" +
"id=" + id +
", price=" + price +
'}';
}
}
The ProductPriceId class (Listing 1) is a simple Java class that has two member variables: region and productId. It is annotated with the @Embeddable annotation.
The ProductPrice class (Listing 2) is a JPA entity that is mapped to the โPRODUCT_PRICEโ table and defines an id field of type ProductPriceId. The ProductPriceId field type is annotated with the @EmbeddedId annotation.
Example application with composite keys
Listing 3 shows the source code for an example application that creates four different product prices, executes a query for a product by its ProductPriceId, queries for all product prices, and then dumps the contents of the โPRODUCT_PRICEโ table so that we can see how Hibernate represents the data.
Listing 3. JpaExampleCompositeKey.java
package com.geekcap.javaworld.jpa;
import com.geekcap.javaworld.jpa.model.ProductPrice;
import com.geekcap.javaworld.jpa.model.ProductPriceId;
import org.hibernate.Session;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
public class JpaExampleCompositeKey {
public static void main(String[] args) {
// Create our entity manager
EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("SuperHeroes");
EntityManager entityManager = entityManagerFactory.createEntityManager();
// Create a product price
ProductPrice productPrice1 = new ProductPrice(new ProductPriceId("EAST", 1), 500.0d);
ProductPrice productPrice2 = new ProductPrice(new ProductPriceId("WEST", 1), 400.0d);
ProductPrice productPrice3 = new ProductPrice(new ProductPriceId("EAST", 2), 200.0d);
ProductPrice productPrice4 = new ProductPrice(new ProductPriceId("WEST", 2), 150.0d);
try {
// Save the product prices to the database
entityManager.getTransaction().begin();
entityManager.persist(productPrice1);
entityManager.persist(productPrice2);
entityManager.persist(productPrice3);
entityManager.persist(productPrice4);
entityManager.getTransaction().commit();
} catch (Exception e) {
e.printStackTrace();
}
// Query for product1 by its ID
ProductPrice productPrice = entityManager.find(ProductPrice.class, new ProductPriceId("EAST", 1));
System.out.println(productPrice);
// Find all product prices
List<ProductPrice> productPrices = entityManager.createQuery("from ProductPrice").getResultList();
System.out.println("nAll Product Prices:");
productPrices.forEach(System.out::println);
// DEBUG, dump our tables
entityManager.unwrap(Session.class).doWork(connection ->
JdbcUtils.dumpTables(connection, "PRODUCT_PRICE"));
// Close the entity manager and associated factory
entityManager.close();
entityManagerFactory.close();
}
}
This application creates a new EntityManagerFactory that references the SuperHeroes persistence unit from โJava persistence with JPA and Hibernate, Part 2,โ which contains the ProductPrice entity. It then creates an EntityManager from that persistence unit. It creates four product prices,:two for product 1 and two for product 2, but in two different regions: โEASTโ and โWEST.โ It then persists them to the database using the EntityManager::persist method.
Next, it queries for a ProductPrice using the EntityManager::find method, which requires the class name of the entity to retrieve and its primary key. Because weโre using a composite key, we pass it a new ProductPriceId instance with the region name set to โEASTโ and the product ID set to 1. This yields the following output:
ProductPrice{id=ProductPriceId{region='EAST', productId=1}, price=500.0}
Querying with composite keys
Querying for an entity using a composite key is just what you would expect: simply pass the primary key to the find() method. The only difference is that in this case weโre not passing a String or an Integer, but rather a ProductPriceId.
The sample application then queries for all product prices, using the JPQL query โfrom ProductPriceโ, which yields the following output:
ProductPrice{id=ProductPriceId{region='EAST', productId=1}, price=500.0}
ProductPrice{id=ProductPriceId{region='WEST', productId=1}, price=400.0}
ProductPrice{id=ProductPriceId{region='EAST', productId=2}, price=200.0}
ProductPrice{id=ProductPriceId{region='WEST', productId=2}, price=150.0}
We see all four of the product prices that weโve persisted to the database. The โPRODUCT_PRICEโ table contains the following data, from our JdbcUtils::dumpTables method call:
Table: PRODUCT_PRICE
{PRODUCTID: 1, REGION: EAST, PRICE: 500.0},
{PRODUCTID: 1, REGION: WEST, PRICE: 400.0},
{PRODUCTID: 2, REGION: EAST, PRICE: 200.0},
{PRODUCTID: 2, REGION: WEST, PRICE: 150.0},
The fields from the ProductPriceId are written directly to database columns in the โPRODUCT_PRICEโ table, along with our addition ProductPrice attribute: price. Hibernate handles populating the fields into the appropriate classes.


