🔑 ID Generation in Hibernate (Summary)¶
Hibernate/JPA gives you several ways to generate primary keys. Each has different trade-offs depending on your database and performance needs.
Here’s the all-in-one master summary of Hibernate ID generation strategies (AUTO, SEQUENCE, IDENTITY, TABLE, UUID), including everything about batching, allocation, gaps, pros/cons, and when to use what.
1. GenerationType.AUTO¶
Meaning Hibernate chooses the best strategy for the underlying database.
Behavior (typical)
- PostgreSQL / Oracle →
SEQUENCE - MySQL / MariaDB / SQL Server →
IDENTITY - If nothing else fits →
TABLE
Pros
- Very simple, portable, no DB-specific code.
Cons
- Hibernate’s choice may change when you switch DBs → unpredictable performance.
Use when
- Portability > performance. Good for prototypes or apps that may switch DBs.
2. GenerationType.SEQUENCE¶
Meaning
Use a database sequence object (nextval in PostgreSQL, seq.NEXTVAL in Oracle).
How it works
- Hibernate fetches IDs from the sequence.
- Can pre-fetch blocks of IDs (
allocationSize). - IDs are known before insert, so batching works well.
Pros
- Best performance with JDBC batching.
- IDs assigned in memory before flush.
- Efficient for high-volume inserts.
Cons
- Requires sequence object in DB (Hibernate can create one if allowed).
- With pooled allocation (
allocationSize > 1), gaps appear.
Use when
- On PostgreSQL/Oracle (databases with strong sequence support).
- High-throughput insert-heavy workloads.
Example
@Id
@SequenceGenerator(
name = "user_seq_gen",
sequenceName = "user_id_seq",
allocationSize = 50 // prefetch 50 IDs at a time
)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "user_seq_gen")
private Long id;
SQL (PostgreSQL)
3. GenerationType.IDENTITY¶
Meaning
Use DB’s auto-increment/identity column. The DB assigns IDs during INSERT.
How it works
- Hibernate issues the
INSERT. - The DB generates the ID.
- Hibernate fetches it afterward.
Pros
- Very simple.
- Fits MySQL / SQL Server defaults.
- No sequence object needed.
Cons
- Hibernate doesn’t know ID until after insert → batching is limited.
- Insert throughput lower than SEQUENCE.
Use when
- On MySQL / SQL Server (legacy schemas).
- When simplicity matters more than insert performance.
Example
DDL
4. GenerationType.TABLE¶
Meaning Use a special table as a pseudo-sequence.
How it works
- Hibernate stores current value in a table (
hibernate_sequencesor custom). - Reads/updates the row to generate IDs.
Pros
- Portable to any DB, even those without sequences or identity.
Cons
- Slowest, requires extra table reads/writes.
- High contention under concurrency.
Use when
- Extreme portability is needed.
- Rare in modern production apps.
Example
@Id
@TableGenerator(
name = "user_tbl_gen",
table = "id_generator",
pkColumnName = "gen_name",
valueColumnName = "gen_val",
pkColumnValue = "user_id",
allocationSize = 50
)
@GeneratedValue(strategy = GenerationType.TABLE, generator = "user_tbl_gen")
private Long id;
SQL
CREATE TABLE id_generator (
gen_name VARCHAR(64) PRIMARY KEY,
gen_val BIGINT NOT NULL
);
INSERT INTO id_generator (gen_name, gen_val) VALUES ('user_id', 1);
5. UUID / GUID (Hibernate-specific)¶
Meaning
Use UUID (128-bit identifier) instead of numeric IDs.
How it works
- Hibernate (6+) can generate UUIDs in the app (
@UuidGenerator). - Stored as
BINARY(16)(compact) orCHAR(36)(readable) in DB.
Pros
- Globally unique — good for distributed systems.
- IDs generated without DB round-trip.
- Known before persist — useful for references/events.
Cons
- Larger (16 bytes vs 8).
- Random UUIDs fragment B-trees → prefer time-ordered UUIDs (v7/ULID).
- Harder to debug than numbers.
Use when
- You need distributed ID generation across multiple services/nodes.
- You want IDs before hitting the DB.
- Microservices, offline clients, event-driven systems.
Example (MySQL, compact storage)
@Id
@UuidGenerator
@JdbcTypeCode(SqlTypes.BINARY) // store UUID in 16 bytes
@Column(columnDefinition = "BINARY(16)")
private UUID id;
DDL
CREATE TABLE users (
id BINARY(16) NOT NULL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
uuid_txt CHAR(36) AS (BIN_TO_UUID(id)) STORED
);
⚙️ Key Cross-Cutting Concepts¶
🔄 Batching¶
- JDBC batching = send multiple inserts in one round trip.
- Best with SEQUENCE (IDs known up front).
- Limited with IDENTITY (IDs only known after insert).
- Enable in Hibernate:
📦 Allocation / Prefetching¶
- With
SEQUENCEandTABLE, Hibernate can fetch blocks of IDs usingallocationSize. - Example:
allocationSize=50→ Hibernate grabs 50 IDs and consumes them gradually. - Benefits: fewer DB calls, better batching.
- Downsides: gaps if app crashes or restarts.
⚠️ Gaps¶
-
All strategies may produce gaps:
- Rollbacks
- Crashes
- Pooled allocations
- Don’t use IDs as business order — always add a
created_atcolumn.
✅ When to Use What (Cheat Sheet)¶
| Strategy | Best Fit | Pros | Cons |
|---|---|---|---|
| AUTO | Portability | Easiest, automatic choice | Unpredictable per DB |
| SEQUENCE | PostgreSQL / Oracle | Best batching, performance, pre-fetch | Needs sequence object, gaps |
| IDENTITY | MySQL / SQL Server (legacy schemas) | Simple, DB-native | Weak batching, slower inserts |
| TABLE | Exotic DBs, portability needed | Works everywhere | Slow, contention-heavy |
| UUID | Distributed / microservices / offline | Global uniqueness, no DB round-trip | Larger index, may fragment |
🚀 Quick Rules of Thumb¶
- High throughput inserts (Postgres/Oracle) →
SEQUENCE+ batching. - MySQL/SQL Server legacy →
IDENTITY. - Distributed/microservices →
UUID(prefer time-ordered). - Extreme portability →
TABLE. - Don’t care / just starting →
AUTO.
✅ In short:
- Use SEQUENCE when you can (best batching).
- Use IDENTITY if schema/DB requires it.
- Use UUID if you need IDs outside the DB or in distributed systems.
- Expect gaps with any method.
- Never use PK for business ordering → add timestamps.
MySQL¶
UUID Primary Keys (Compact BINARY(16) Storage)¶
When using UUIDs as primary keys in MySQL, storing them as CHAR(36) strings is common but inefficient. A better approach is to store them in a compact binary format using BINARY(16). This reduces storage space and improves index performance.
Here’s a tiny, end-to-end example of UUID primary keys on MySQL, stored compactly as BINARY(16) and generated in the app with Hibernate 6.
1) MySQL table (compact PK + easy-to-read helper)¶
CREATE TABLE users (
id BINARY(16) NOT NULL PRIMARY KEY, -- packed UUID
name VARCHAR(100) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- Optional: human-readable mirror (handy for ad-hoc queries)
uuid_txt CHAR(36) AS (BIN_TO_UUID(id)) STORED
) ENGINE=InnoDB;
You don’t have to keep
uuid_txt—it’s just convenient. The real PK isBINARY(16).
2) Hibernate/JPA entity (Hibernate 6)¶
import jakarta.persistence.*;
import java.time.Instant;
import java.util.UUID;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UuidGenerator;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;
@Entity
@Table(name = "users")
public class User {
@Id
@UuidGenerator // generates java.util.UUID in the app (no DB round-trip)
@JdbcTypeCode(SqlTypes.BINARY) // map UUID -> BINARY(16)
@Column(name = "id", columnDefinition = "BINARY(16)")
private UUID id;
@Column(nullable = false)
private String name;
@CreationTimestamp
@Column(name = "created_at", nullable = false, updatable = false)
private Instant createdAt;
// getters/setters ...
}
If your Hibernate exposes a time-ordered style, you can do
@UuidGenerator(style = UuidGenerator.Style.TIME)to reduce index fragmentation vs random v4.
3) Spring/JPA settings (example)¶
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
spring.jpa.hibernate.ddl-auto=none # or update/create at your own risk
spring.jpa.properties.hibernate.jdbc.batch_size=50
4) Using it¶
User u = new User();
u.setName("Alice");
userRepository.save(u); // Hibernate generates UUID in-memory and sends as 16 bytes
System.out.println(u.getId()); // you already have the ID before insert
5) Handy SQL when querying manually¶
-- Find by a textual UUID
SELECT id, name, created_at
FROM users
WHERE id = UUID_TO_BIN('550e8400-e29b-41d4-a716-446655440000');
-- See the UUIDs in text form (thanks to the generated column)
SELECT uuid_txt, name, created_at FROM users;
Notes & options¶
- Why
BINARY(16)? 2× smaller thanCHAR(36), faster indexes. -
Time-ordered UUIDs: Random v4 UUIDs fragment B-trees. Prefer v7 (time-ordered) if possible:
- Generate UUIDv7 in your code (library) and assign it to
idbeforesave(), or - Use a Hibernate generator with a time-ordered style if your version supports it.
- MySQL’s
UUID_TO_BIN(..., 1)trick: When you generate IDs in SQL (not here), usingUUID_TO_BIN(uuid, 1)stores bytes in a time-friendly order. If you go that route, be consistent and also read withBIN_TO_UUID(id, 1). With Hibernate generating IDs as JavaUUID, you typically stick to the straightforward mapping above (no swap flag).
- Generate UUIDv7 in your code (library) and assign it to
FAQs: UUID vs SEQUENCE vs IDENTITY¶
1) When to pick UUID / GUID (Hibernate 6)¶
Choose a UUID/GUID primary key when one or more of these are true:
- You need to generate IDs outside the DB (e.g., in services, message producers, offline/mobile clients) and later upsert/merge in the DB.
- You have multiple writer nodes / multi-region and want globally unique IDs without coordinating with the DB first.
- You want to avoid an extra DB round-trip per ID assignment (helpful when using
IDENTITY, or when you don’t want to call a sequence). - You want stable IDs before persistence (useful for building relationships in memory, emitting events, logs, URLs, etc.).
- You’re using an event-driven or CQRS setup and need IDs as soon as aggregates are created.
Trade-offs¶
- Pros: globally unique across services, no DB trip to get an ID, easy replication/sharding, IDs exist before insert.
- Cons: bigger indexes; random UUIDs (v4) fragment B-trees and bloat indexes. Prefer time-ordered IDs (UUIDv7, v1, ULID-style) to keep inserts mostly append-only and indexes tighter.
Practical mapping tips¶
- PostgreSQL: use
uuidcolumn type. - MySQL/MariaDB: use
BINARY(16)and store packed UUIDs (don’t store as 36-char strings). - SQL Server:
UNIQUEIDENTIFIER. - Oracle:
RAW(16).
Hibernate 6 examples:
// Postgres-friendly
@Id
@org.hibernate.annotations.UuidGenerator // Hibernate 6
@org.hibernate.annotations.JdbcTypeCode(org.hibernate.type.SqlTypes.UUID)
@Column(columnDefinition = "uuid")
private java.util.UUID id;
// MySQL-friendly (packed)
@Id
@org.hibernate.annotations.UuidGenerator
@org.hibernate.annotations.JdbcTypeCode(org.hibernate.type.SqlTypes.BINARY)
@Column(columnDefinition = "BINARY(16)")
private java.util.UUID id;
(If your Hibernate version exposes a “time” style for the generator, use it to get time-ordered UUIDs.)
2) “If we can pre-order IDs with SEQUENCE, why do we need IDENTITY? Can we pre-fetch 50 and use them one by one?”¶
Yes, you can pre-fetch and reuse¶
With GenerationType.SEQUENCE plus @SequenceGenerator(allocationSize = 50), Hibernate will fetch a block of 50 IDs from the DB sequence and cache them in memory. It then consumes those IDs:
- for batched inserts, or
- for individual inserts over time, until the block is exhausted,
- then it fetches the next block automatically.
This is not limited to a single “batch operation”. It’s a pool that gets used as your app persists entities. Note you may see gaps (e.g., app restarts with unused IDs in the block).
@Id
@SequenceGenerator(
name = "user_seq",
sequenceName = "user_id_seq",
allocationSize = 50 // prefetch 50 at a time
)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "user_seq")
private Long id;
So… why use IDENTITY at all?¶
IDENTITY remains common because:
- Database choice / legacy: MySQL and SQL Server schemas traditionally rely on identity/auto-increment. Many shops, tools, and DBAs default to it.
- Simplicity: No separate sequence object to manage. One column with
AUTO_INCREMENT/IDENTITY. - Existing systems & tooling: ETL, ORM defaults, migration scripts, audit triggers, etc., may expect identity columns.
- DB feature set: Some environments don’t use sequences; identity is the “native” and operationally familiar option.
Downside: with IDENTITY, Hibernate learns the key after each insert, which weakens true JDBC insert batching. (That’s exactly why sequences or UUIDs tend to perform better for heavy write loads.)
Quick chooser¶
- High write throughput on Postgres/Oracle:
SEQUENCE+allocationSize(pooled) + JDBC batching. - Multi-service / offline ID generation / pre-persistence IDs: UUID (prefer time-ordered variants).
- Legacy MySQL/SQL Server or simplest ops: IDENTITY is fine; accept weaker batching.
- Maximum portability to odd DBs:
TABLE(understand it’s slower).
Extra tips¶
- Don’t rely on PK order for business sorting; use created_at or a business key.
- Expect gaps with any strategy (rollbacks, pooling, restarts).
- If you use UUIDs on MySQL, store as
BINARY(16)and consider time-ordered UUIDs (e.g., v7) to reduce index fragmentation.