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:
IDENTITYdisables 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 preferSEQUENCE.
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:
SEQUENCEis the recommended strategy on PostgreSQL, Oracle, and H2. It supports batching and id pooling, unlikeIDENTITY.
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:
TABLEis rarely the right choice today. Use it only when your database has no native sequence and you cannot rely on identity columns.
Strategy comparison
| Strategy | How it gets the id | Extra DB object | Batch insert support | Typical use |
|---|---|---|---|---|
IDENTITY | DB auto-increments on insert | None | No (inserts can’t batch) | MySQL, quick prototypes |
SEQUENCE | Reads from a sequence, pools via allocationSize | A sequence | Yes | PostgreSQL, Oracle, H2 (preferred) |
AUTO | Hibernate picks per dialect | Depends (often a sequence) | Depends | Portable default, less predictable |
TABLE | Row in a counter table, locked | A counter table | Yes | Legacy/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/hashCodeon 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.