Steven Haines
Contributor

Composite keys in JPA and Hibernate

tip
Apr 25, 20196 mins

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

Conceptual image of a digital circuit-board key amid binary code.
Credit: 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.

Steven Haines

Steven Haines is a senior technologist, accomplished architect, author, and educator. He currently is a principal software engineer at Veeva Systems, where he builds Spring-based Java applications. Steven previously worked on two startups: Chronosphere, where he helped customers design and implement large-scale observability strategies, and Turbonomic, where he was a principal software architect for cloud optimization products. He's also worked for Disney as a technical architect and lead solution architect, building out the next generation of Disney's guest experience and other Disney solutions. Steven specializes in performance and scalability, cloud-based architectures, high-availability, fault tolerance, business analytics, and integration with new and emerging technologies.

As an author, he has written two books on Java programming and more than 500 articles for publications such as InfoWorld, InformIT.com (Pearson Education), JavaWorld, and Dr. Dobb's Journal. He has also written over a dozen white papers and ebooks on performance management and cloud-based architectures.

Steven has taught computer science and Java programming at Learning Tree University and the University of California, Irvine. He also maintains a personal website dedicated to helping software developers and architects grow in their knowledge: www.geekcap.com (by Geeks for Geeks).

More from this author