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@Embeddable
annotation 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
ProductPrice
entity and reference theProductPriceId
as itsid
, using the@EmbeddableId
annotation.
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.
This story, "Composite keys in JPA and Hibernate" was originally published by JavaWorld.