Composite keys in JPA and Hibernate

Use embeddable objects to join two primary keys into one composite key

Conceptual image of a digital circuit-board key amid binary code.
liulolo / Getty Images

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 the ProductPriceId as its id, 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.

Copyright © 2019 IDG Communications, Inc.