Hibernate Composite Key

Hibernate Composite Key with @EmbeddedId: Complete Tutorial (2026) | D.Tech Academic

Hibernate Composite Key with @EmbeddedId: Complete Tutorial (2026)

Last Updated: | Author: Deep Singh

Not every table has a single-column primary key. Sometimes the uniqueness of a row depends on a combination of two or three columns. That's called a composite key (or composite primary key), and Hibernate gives you clean ways to model it.

In this tutorial, you'll learn how to map a composite key using @EmbeddedId and @Embeddable. We'll build a small Student entity whose uniqueness is defined by the combination of mobile and email. By the end, you'll know exactly how to set it up, save records, and avoid the most common traps.

What is a Composite Key?

A composite key is a primary key made up of two or more columns. In practice, this happens when no single column uniquely identifies a row, but a combination of columns does.

Common examples include:

  • An enrollment table, where the key is (student_id, course_id).
  • An order_items table, where the key is (order_id, product_id).
  • A user_permissions table, where the key is (user_id, permission_id).

Hibernate supports two ways to model composite keys:

  • @EmbeddedId — the key is a separate class embedded into the entity. This is what we'll use in this tutorial.
  • @IdClass — the key fields are declared directly on the entity, and a separate class mirrors them. Less common but occasionally useful.

For most cases, @EmbeddedId is the cleaner choice because it groups all key fields into a single, reusable class.

Maven Dependencies

Add the following dependencies to your pom.xml inside the <dependencies> block.

<dependencies>
    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-core</artifactId>
        <version>5.6.15.Final</version>
    </dependency>

    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <version>8.4.0</version>
    </dependency>
</dependencies>

Note on versions:

  • I've upgraded Hibernate to 5.6.15.Final — the latest stable 5.x release. It's the last version before Hibernate 6, which changed many APIs.
  • I've switched the MySQL connector to com.mysql:mysql-connector-j — the new group and artifact ID used since Connector/J 8.0.31. The old mysql:mysql-connector-java and version 5.1.6 from the original tutorial are years out of date.

hibernate.cfg.xml Configuration

Place this file in your src/main/resources folder.

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hibernate-configuration PUBLIC
    "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
    "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">

<hibernate-configuration>
    <session-factory>
        <property name="hibernate.connection.driver_class">
            com.mysql.cj.jdbc.Driver
        </property>
        <property name="hibernate.connection.url">
            jdbc:mysql://localhost:3306/stds?useSSL=false&serverTimezone=UTC
        </property>
        <property name="hibernate.connection.username">root</property>
        <property name="hibernate.connection.password">root</property>

        <property name="hibernate.dialect">
            org.hibernate.dialect.MySQL8Dialect
        </property>
        <property name="hibernate.hbm2ddl.auto">update</property>
        <property name="hibernate.show_sql">true</property>
        <property name="hibernate.format_sql">true</property>
    </session-factory>
</hibernate-configuration>

Fixed in this version: The original used com.mysql.jdbc.Driver, which is deprecated and prints warnings on Connector/J 8. I've replaced it with com.mysql.cj.jdbc.Driver. I've also added the MySQL dialect, which Hibernate needs to generate correct SQL.

Entity and Composite Key Classes

Two classes make up the composite key setup: the entity itself, and the key class that it embeds.

StudentKey.java — The Composite Key

package com.deepsingh44.model;

import java.io.Serializable;
import java.util.Objects;

import javax.persistence.Embeddable;

@Embeddable
public class StudentKey implements Serializable {

    private String mobile;
    private String email;

    public String getMobile() {
        return mobile;
    }

    public void setMobile(String mobile) {
        this.mobile = mobile;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof StudentKey)) return false;
        StudentKey that = (StudentKey) o;
        return Objects.equals(mobile, that.mobile)
                && Objects.equals(email, that.email);
    }

    @Override
    public int hashCode() {
        return Objects.hash(mobile, email);
    }
}

Key points about the key class:

  • It's marked @Embeddable so Hibernate knows it can be embedded into an entity.
  • It must implement Serializable. Hibernate requires this because composite keys can be used as identifiers in caches and detached entities.
  • You must override equals() and hashCode(). Without these, Hibernate's session cache can't properly identify entities, and you'll get subtle bugs where the same logical key is treated as different.
  • Use Objects.equals() and Objects.hash() to write clean implementations.

Student.java — The Entity

package com.deepsingh44.model;

import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import javax.persistence.Table;

@Entity
@Table(name = "students")
public class Student {

    @EmbeddedId
    private StudentKey studentKey;

    private String name;
    private String address;

    public StudentKey getStudentKey() {
        return studentKey;
    }

    public void setStudentKey(StudentKey studentKey) {
        this.studentKey = studentKey;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }
}

Key points about the entity:

  • @Entity marks it as a persistent entity.
  • @Table(name = "students") specifies the table name explicitly — "student" is safer as a plural anyway, and "user" / "order" / "group" would be reserved words.
  • @EmbeddedId tells Hibernate that the primary key is the embedded StudentKey.
  • The other fields (name and address) are regular columns.

Saving a Record — Test.java

Here's a complete program that bootstraps Hibernate, creates a student with a composite key, and saves it to the database.

package com.deepsingh44.model;

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.boot.Metadata;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;

public class Test {
    public static void main(String[] args) {

        // 1. Build the service registry from hibernate.cfg.xml
        StandardServiceRegistry ssr = new StandardServiceRegistryBuilder()
                .configure("hibernate.cfg.xml")
                .build();

        // 2. Register annotated classes and build metadata
        Metadata meta = new MetadataSources(ssr)
                .addAnnotatedClass(Student.class)
                .getMetadataBuilder()
                .build();

        // 3. Build the session factory
        SessionFactory factory = meta.getSessionFactoryBuilder().build();

        // 4. Open a session
        Session session = factory.openSession();

        // 5. Start a transaction (required for save/update/delete)
        Transaction tx = session.beginTransaction();

        try {
            // 6. Build the composite key
            StudentKey sk = new StudentKey();
            sk.setEmail("d@gmail.com");
            sk.setMobile("98XXXXXXXX");

            // 7. Build the entity
            Student s = new Student();
            s.setName("Deep Singh");
            s.setAddress("India");
            s.setStudentKey(sk);

            // 8. Save
            session.save(s);

            // 9. Commit
            tx.commit();
            System.out.println("Saved successfully");
        } catch (Exception e) {
            tx.rollback();
            e.printStackTrace();
        } finally {
            session.close();
            factory.close();
        }
    }
}

What's been fixed here:

  • Transaction handling. The original code called session.save(s) and then session.beginTransaction().commit() — committing a transaction that was never opened before the save. In Hibernate 5.x this usually works because save() auto-begins, but it's fragile. The corrected version opens the transaction first, commits after the save, and rolls back on failure.
  • Try/catch/finally. Ensures the session is closed even if something fails. This prevents connection leaks.
  • Swapped the swapped values. The original had sk.setEmail("98XXXXXXXX") and sk.setMobile("d@gmail.com") — the values were assigned to the wrong fields. Fixed.

How the Code Works

Let's walk through the key ideas.

1. What @Embeddable and @EmbeddedId Do

@Embeddable tells Hibernate "this class can be embedded inside another entity as part of its state." @EmbeddedId tells Hibernate "this field is my primary key, and its columns make up my composite primary key."

Hibernate looks at the fields inside the embeddable and maps each one to a column in the entity's table. So the StudentKey's mobile and email become the mobile and email columns in the students table — both of them, together, forming the primary key.

2. Why equals and hashCode Matter

Hibernate's first-level cache is keyed on the entity's identifier. When you load or save an entity, Hibernate checks the cache by calling equals() on the key. Without a proper implementation, two StudentKey objects with the same mobile and email would be treated as different keys, causing duplicate rows or unexpected session behavior.

Use Objects.equals() and Objects.hash() — they handle nulls correctly and are less error-prone than writing it out manually.

3. The Session Lifecycle

Opening a session is cheap, but you should still close it when you're done. A session holds a JDBC connection from the pool, so leaving it open can exhaust the pool over time.

The transaction ensures the save is atomic. If something throws between beginTransaction() and commit(), the rollback undoes the changes.

4. What the Database Table Looks Like

After running the program, Hibernate creates the following table (thanks to hbm2ddl.auto=update):

CREATE TABLE students (
    mobile VARCHAR(255) NOT NULL,
    email VARCHAR(255) NOT NULL,
    name VARCHAR(255),
    address VARCHAR(255),
    PRIMARY KEY (mobile, email)
);

Notice the composite primary key on (mobile, email). You can have multiple rows with the same mobile as long as the email is different, and vice versa. But the same combination can't repeat.

Common Pitfalls

  • Forgetting Serializable. Hibernate won't start if the composite key class doesn't implement it. This is a hard requirement, not a suggestion.
  • Missing equals() and hashCode(). Without them, Hibernate's session cache misbehaves and you'll see duplicate entities or incorrect updates.
  • Saving without a transaction. In modern Hibernate (5.x and 6.x), you should always open a transaction before save(), update(), or delete(). Relying on auto-begin is fragile.
  • Not closing the session. Connection leaks build up fast in production. Always use try-with-resources or a finally block.
  • Deprecated MySQL driver. The old com.mysql.jdbc.Driver is removed from Connector/J 8. Use com.mysql.cj.jdbc.Driver instead.
  • Using save() on an already-persisted entity. Use saveOrUpdate() if you're not sure whether the entity is new or detached.
  • Version mismatch. If you're on Hibernate 6 or Java 17+, you'll need to migrate from javax.persistence.* to jakarta.persistence.*. The APIs are otherwise similar.
  • Assuming a single transaction covers all operations. Each logical unit of work needs its own transaction. Don't span user interactions.

An alternative worth knowing: Hibernate 6 and JPA 2.1 introduced a simpler option — just use @Id on multiple fields if you're using @IdClass, or use a @ManyToOne relationship with @MapsId for the foreign-key-based composite keys common in join tables. The @EmbeddedId approach shown here is still the most explicit and readable.

Wrapping Up

You now know how to map a composite primary key in Hibernate using @EmbeddedId and @Embeddable. The pattern is simple: put the key fields into a small Serializable class with equals() and hashCode(), mark it @Embeddable, and reference it from the entity with @EmbeddedId.

From here, you can extend the example with:

  • Named queries that use the composite key in WHERE clauses.
  • Second-level caching with the composite key — as long as equals and hashCode are correct, caching works cleanly.
  • Relationship mapping — using the composite key as the foreign key target in a one-to-many association.
  • Migration to Hibernate 6 when you're ready — the annotations are largely the same, just the package names change.

No comments: