Skip to content
Spring Boot sb data-jpa 5 min read

Primary Keys & Generation

Every JPA entity needs a primary key declared with @Id. For surrogate keys you usually pair it with @GeneratedValue, which delegates id creation to the database or Hibernate. The strategy you pick affects performance, batch inserts, and portability. This page covers the four generation strategies, UUID keys, and composite keys via @EmbeddedId and @IdClass. For the surrounding mapping annotations, see Entity Mapping.

Declaring the id

The simplest case is a numeric surrogate key generated by the database:

import jakarta.persistence.*;

@Entity
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    // JPA requires a no-args constructor
    protected Product() {}

    public Product(String name) {
        this.name = name;
    }

    public Long getId() { return id; }
    public String getName() { return name; }
}

If you omit @GeneratedValue, you are responsible for assigning the id yourself before persisting (a natural or assigned key).

Generation strategies

GenerationType has four values. Each obtains the id differently and has different implications for JDBC batching.

IDENTITY

The database auto-increments the column (MySQL AUTO_INCREMENT, PostgreSQL SERIAL/identity). Hibernate must execute the INSERT immediately to read back the generated key.

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
insert into product (name) values (?)
-- Hibernate reads the generated key via JDBC getGeneratedKeys()

Warning: IDENTITY disables JDBC batch inserts. Because Hibernate needs the key back right after each row, it cannot defer and group inserts into a batch. For high-volume inserts prefer SEQUENCE.

SEQUENCE

Uses a database sequence object. Hibernate can fetch ids ahead of time, so it works with batching and allocationSize pooling.

@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "product_seq")
@SequenceGenerator(name = "product_seq", sequenceName = "product_sequence", allocationSize = 50)
private Long id;
-- creates the sequence (ddl-auto)
create sequence product_sequence start with 1 increment by 50

-- on demand, one round trip yields 50 ids
select nextval('product_sequence')

-- inserts can then be batched
insert into product (name, id) values (?, ?)

With allocationSize = 50, Hibernate calls the sequence once and hands out 50 in-memory ids before hitting the database again. This dramatically reduces round trips for bulk inserts.

Tip: SEQUENCE is the recommended strategy on PostgreSQL, Oracle, and H2. It supports batching and id pooling, unlike IDENTITY.

AUTO

Lets Hibernate choose a strategy based on the dialect. On modern Hibernate 6 with PostgreSQL this resolves to a sequence; on MySQL it historically picks a table or sequence emulation.

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;

AUTO is convenient but the resolved strategy can differ across databases and Hibernate versions, so prefer an explicit strategy for predictable behavior.

TABLE

Emulates a sequence using a dedicated table of counters. It is the most portable but the slowest, since it requires row locking on the counter table.

@Id
@GeneratedValue(strategy = GenerationType.TABLE, generator = "product_gen")
@TableGenerator(name = "product_gen", table = "id_generator",
        pkColumnName = "gen_name", valueColumnName = "gen_value",
        allocationSize = 50)
private Long id;
create table id_generator (gen_name varchar(255) not null, gen_value bigint, primary key (gen_name))

select gen_value from id_generator where gen_name = ? for update
update id_generator set gen_value = ? where gen_name = ? and gen_value = ?

Note: TABLE is rarely the right choice today. Use it only when your database has no native sequence and you cannot rely on identity columns.

Strategy comparison

StrategyHow it gets the idExtra DB objectBatch insert supportTypical use
IDENTITYDB auto-increments on insertNoneNo (inserts can’t batch)MySQL, quick prototypes
SEQUENCEReads from a sequence, pools via allocationSizeA sequenceYesPostgreSQL, Oracle, H2 (preferred)
AUTOHibernate picks per dialectDepends (often a sequence)DependsPortable default, less predictable
TABLERow in a counter table, lockedA counter tableYesLegacy/portable fallback only

UUID keys

For globally unique, non-guessable ids, use a UUID. Hibernate 6 provides @UuidGenerator, which works without a database sequence.

import jakarta.persistence.*;
import org.hibernate.annotations.UuidGenerator;
import java.util.UUID;

@Entity
public class Order {

    @Id
    @GeneratedValue
    @UuidGenerator
    private UUID id;

    private String customer;

    protected Order() {}

    public Order(String customer) { this.customer = customer; }

    public UUID getId() { return id; }
}
insert into orders (customer, id) values (?, ?)
-- id is a generated UUID, assigned in-memory before insert

Because the UUID is generated in the application, inserts can be batched and do not need a database round trip for the key. The trade-off is wider keys (16 bytes) and randomly distributed values, which can fragment B-tree indexes.

Tip: Use @UuidGenerator(style = UuidGenerator.Style.TIME) for time-ordered (version 7-style) UUIDs that index more efficiently than fully random ones.

Composite keys

When the primary key spans multiple columns, model it with either @EmbeddedId or @IdClass. Both achieve the same result; pick one per entity.

@EmbeddedId with @Embeddable

The key is a separate @Embeddable class embedded into the entity. It must implement Serializable and override equals/hashCode.

@Embeddable
public class OrderLineId implements Serializable {

    private Long orderId;
    private Long productId;

    protected OrderLineId() {}

    public OrderLineId(Long orderId, Long productId) {
        this.orderId = orderId;
        this.productId = productId;
    }

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

    @Override
    public int hashCode() { return Objects.hash(orderId, productId); }
}

@Entity
public class OrderLine {

    @EmbeddedId
    private OrderLineId id;

    private int quantity;

    protected OrderLine() {}
}

@IdClass

Here the entity declares each key field directly with @Id, and a separate id class mirrors those fields (also Serializable with equals/hashCode).

public class OrderLineKey implements Serializable {

    private Long orderId;
    private Long productId;

    public OrderLineKey() {}

    public OrderLineKey(Long orderId, Long productId) {
        this.orderId = orderId;
        this.productId = productId;
    }

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

    @Override
    public int hashCode() { return Objects.hash(orderId, productId); }
}

@Entity
@IdClass(OrderLineKey.class)
public class OrderLine {

    @Id private Long orderId;
    @Id private Long productId;

    private int quantity;
}

Use @EmbeddedId when the composite key is a meaningful value object you pass around; use @IdClass when you prefer the key columns to appear as plain fields on the entity.

Note: Overriding equals/hashCode on the key class is mandatory. JPA relies on key equality to manage the persistence context and identity map. See Java records for an even more concise way to model immutable value keys.

Last updated June 13, 2026
Was this helpful?