Primary Keys and Generation Strategies

The Four Strategies

StrategyHow the id is producedDatabasesBatch inserts
IDENTITYAuto-increment column; id known only after INSERTMySQL, PostgreSQL, SQL Server, H2No (each insert runs immediately)
SEQUENCEDatabase sequence object; ids fetched in blocksPostgreSQL, Oracle, H2, SQL ServerYes (best performance)
TABLEA special table holds the next valueAnySlow, avoid
UUID (JPA 3.1)Random UUID generated in JavaAnyYes
AUTO (default)Hibernate chooses; in Hibernate 6 this is a sequence (emulated by a table on MySQL)AnyDepends

Choosing per Database

// MySQL: use IDENTITY (MySQL has no sequences)
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

// PostgreSQL / Oracle: use a SEQUENCE
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "product_seq")
@SequenceGenerator(name = "product_seq", sequenceName = "product_seq", allocationSize = 50)
private Long id;

// Public, non-guessable ids (e.g. order ids in URLs): UUID
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;

Sequence log

SQL Hibernate runs on PostgreSQL with the sequence strategy:

-- persisting 3 products

Example output

Hibernate: create sequence product_seq start with 1 increment by 50
Hibernate: select nextval('product_seq')
Hibernate: insert into products (category,description,name,price,stock,id) values (?,?,?,?,?,?)
Hibernate: insert into products (category,description,name,price,stock,id) values (?,?,?,?,?,?)
Hibernate: insert into products (category,description,name,price,stock,id) values (?,?,?,?,?,?)

Note: With plain @GeneratedValue (AUTO) on MySQL, Hibernate 6 creates an extra table products_seq and runs select next_val ... for update to emulate a sequence. Most MySQL projects therefore write IDENTITY explicitly.

How allocationSize Saves Round Trips

Example

With allocationSize = 50, one sequence call reserves a block of 50 ids, which Hibernate hands out from memory. This simulation persists 120 products:

public class Main {
    public static void main(String[] args) {
        Sequence dbSequence = new Sequence(50);
        PooledIdGenerator gen = new PooledIdGenerator(dbSequence, 50);

        for (int i = 1; i <= 120; i++) {
            long id = gen.nextId();
            if (i <= 2 || i == 50 || i == 51 || i == 120) System.out.println("product #" + i + " -> id " + id);
        }
        System.out.println("Database round trips for ids: " + dbSequence.calls);
    }
}

class Sequence {                       // the database object
    private long next = 1;
    private final int increment;
    int calls = 0;
    Sequence(int increment) { this.increment = increment; }
    long nextval() { calls++; long v = next; next += increment; return v; }
}

class PooledIdGenerator {             // what Hibernate keeps in memory
    private final Sequence seq;
    private final int size;
    private long current, max;
    PooledIdGenerator(Sequence seq, int size) { this.seq = seq; this.size = size; }
    long nextId() {
        if (current == 0 || current > max) {
            current = seq.nextval();
            max = current + size - 1;
        }
        return current++;
    }
}

Output

product #1 -> id 1
product #2 -> id 2
product #50 -> id 50
product #51 -> id 51
product #120 -> id 120
Database round trips for ids: 3

Tip: The sequence increment by in the database must match allocationSize, otherwise Hibernate fails validation or produces duplicate ids.

Natural vs Surrogate Keys

A surrogate key (id generated by the database) has no business meaning. A natural key (email, SKU, PAN number) does. Prefer surrogate keys as the primary key and protect natural keys with a unique constraint — business values can change (a user changes email), primary keys should not.