Hibernate & Entity Mapping

Primary Keys & @GeneratedValue

18 min Lesson 4 of 13

Primary Keys & @GeneratedValue

Every JPA entity must have a primary key — the value that uniquely identifies a row in its mapped table. How that key is generated at INSERT time is one of the first decisions you make when mapping a domain model, and the wrong choice shows up as a performance bottleneck or a concurrency bug months later. This lesson covers the mechanics of @Id, every strategy available under @GeneratedValue, and the real-world trade-offs that should drive your choice.

The Bare Minimum: @Id

Any persistent field annotated with @Id becomes the primary key. You can place the annotation on the field itself (field access) or on the getter (property access); Hibernate uses whichever style you pick for all other mappings in that entity too. Modern practice strongly prefers field access.

import jakarta.persistence.*; @Entity @Table(name = "orders") public class Order { @Id private Long id; // you are responsible for setting this value private String status; // getters, setters ... }

With no @GeneratedValue, the application must assign id before calling persist(). That is sometimes intentional (natural keys, UUIDs generated in code), but for surrogate integer keys it is impractical.

@GeneratedValue and the Four Strategies

Annotating the @Id field with @GeneratedValue delegates key generation to the JPA provider. The strategy attribute accepts four values from the GenerationType enum.

IDENTITY — Let the Database Column Auto-Increment

GenerationType.IDENTITY maps to a database AUTO_INCREMENT (MySQL/MariaDB) or GENERATED ALWAYS AS IDENTITY (PostgreSQL 10+, SQL Server) column. The database assigns the key on insert and Hibernate reads it back immediately.

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

The DDL Spring Boot generates for this on MySQL looks like:

CREATE TABLE orders ( id BIGINT NOT NULL AUTO_INCREMENT, status VARCHAR(50), PRIMARY KEY (id) );
IDENTITY disables Hibernate batch inserts. Because the key is only known after the INSERT executes, Hibernate cannot defer the INSERT to the end of a transaction and batch multiple rows together. If you insert thousands of rows in a loop, IDENTITY forces one round-trip per row. For bulk-write workloads, switch to SEQUENCE or TABLE.

SEQUENCE — A Database Sequence Object

GenerationType.SEQUENCE delegates to a named database sequence object (natively supported by PostgreSQL, Oracle, H2, and SQL Server 2012+; not available in MySQL before version 8). Hibernate calls NEXT VALUE FOR sequence_name before the INSERT, so it knows the key before the row is written and can batch inserts freely.

@Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "order_seq") @SequenceGenerator(name = "order_seq", sequenceName = "order_id_seq", allocationSize = 50) private Long id;

The allocationSize parameter is critical for performance. With allocationSize = 50, Hibernate fetches one sequence value from the database and then increments in memory for the next 49 inserts before hitting the database again. The database sequence must increment by the same amount (INCREMENT BY 50).

Choose allocationSize carefully. Too small (e.g., 1) means a database round-trip per insert — identical to IDENTITY. Too large wastes ID space on application restart (the in-memory block is discarded). 50 is the JPA default and a reasonable starting point; raise it to 100–500 for bulk-insert services.

Corresponding DDL:

CREATE SEQUENCE order_id_seq START WITH 1 INCREMENT BY 50;

AUTO — Provider Picks the Strategy

GenerationType.AUTO is the default when you omit the strategy attribute. Hibernate inspects the dialect and chooses what it considers best for the target database. On PostgreSQL it picks SEQUENCE; on older MySQL it falls back to a special shared TABLE-based sequence.

@Id @GeneratedValue // same as strategy = GenerationType.AUTO private Long id;
AUTO is a moving target. The strategy Hibernate selects for AUTO changed between Hibernate 5 and Hibernate 6, and it can change again in future major releases. In production code, always specify the strategy explicitly so a Hibernate upgrade cannot silently change your key-generation behavior.

TABLE — A Fallback for Portability

GenerationType.TABLE emulates a sequence using a regular database table. It works on every database, including MySQL, but it requires pessimistic row-level locking on each insert — one extra SELECT … FOR UPDATE plus one UPDATE per key request. It is the slowest strategy and almost never the right choice for new projects.

@Id @GeneratedValue(strategy = GenerationType.TABLE, generator = "order_table_gen") @TableGenerator(name = "order_table_gen", table = "id_generator", pkColumnName = "gen_name", valueColumnName = "gen_val", pkColumnValue = "order_id", allocationSize = 50) private Long id;
When TABLE is still useful: cross-database test suites that must run identically on MySQL (no sequences) and PostgreSQL, or legacy schemas where you cannot add a sequence object. Outside these edge cases, prefer SEQUENCE or IDENTITY.

Using UUID as a Primary Key

Hibernate 6 and JPA 3.1 add first-class UUID support. Declare the field as java.util.UUID and use GenerationType.UUID (or the Hibernate-specific @UuidGenerator). The JPA provider generates a UUID before the INSERT, giving you the same batching freedom as SEQUENCE.

import jakarta.persistence.*; import java.util.UUID; @Entity public class Product { @Id @GeneratedValue(strategy = GenerationType.UUID) private UUID id; private String name; }

UUIDs are useful when entities must be created across multiple database shards or service instances without a central sequence. The cost is larger index pages (16 bytes vs 8 bytes for BIGINT) and — with random UUIDs — highly fragmented B-tree indexes. For write-heavy tables on relational databases, BIGINT + SEQUENCE is still faster.

Strategy Selection Guide

  • PostgreSQL / Oracle / SQL Server (modern): SEQUENCE with a tuned allocationSize. Best performance, supports batching.
  • MySQL / MariaDB: IDENTITY is the pragmatic choice. Accept that batch inserts are disabled, or use SEQUENCE only if you are on MySQL 8+ and willing to create sequence objects manually (they are available but not the DDL default).
  • Distributed / multi-shard: UUID. Eliminates coordination overhead at the cost of index size.
  • Portable legacy code: TABLE as a last resort.
  • New Spring Boot projects (PostgreSQL): SEQUENCE + allocationSize = 50 is the idiomatic default.

Composite Keys

JPA also supports composite primary keys via @IdClass or @EmbeddedId. These are covered in detail in a later lesson on embeddables. Avoid composite keys on new tables — surrogate integer or UUID keys are simpler to work with and perform better on joins.

Summary

@Id marks the primary-key field; @GeneratedValue hands key assignment to the provider. IDENTITY is the simplest strategy but blocks batch inserts. SEQUENCE is the most flexible and performant — tune allocationSize to reduce database round-trips. AUTO is convenient in demos but unpredictable across Hibernate versions; always be explicit in production. UUID solves distributed ID generation at the cost of index efficiency. Pick based on your database vendor and write pattern, then move on — the rest of entity mapping does not care which strategy you chose.