@Id and @GeneratedValue
@Id marks an entity's primary key; @GeneratedValue lets the database or Hibernate assign it using IDENTITY, SEQUENCE, UUID or AUTO.
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "")- Added in Spring Boot JPA 1.0 (javax.persistence); GenerationType.UUID since Jakarta Persistence 3.1
Parameters
| Parameter | Type | Description |
|---|---|---|
$strategyoptional | GenerationType= GenerationType.AUTO | IDENTITY (database identity column), SEQUENCE (database sequence), UUID (random UUID), TABLE (a counter table, rarely used) or AUTO (the provider decides). |
$generatoroptional | String= "" | Name of a generator declared with @SequenceGenerator or @TableGenerator. |
Return value
n/a@Id marks the identifier field. @GeneratedValue says the value is assigned on persist rather than by your code, and chooses how.
Errors
An entity without @Id fails at startup. A sequence whose INCREMENT BY does not match allocationSize makes Hibernate hand out clashing or wasted ids.
Changelog
- PHP Jakarta Persistence 3.1Added GenerationType.UUID for java.util.UUID identifiers.
Every entity has exactly one @Id. Use a wrapper type such as Long or UUID, not a primitive, so that null means "not saved yet". Spring Data relies on that: save() calls persist when the id is null and merge otherwise.
Choosing a strategy
| Strategy | How it works | Trade-off |
|---|---|---|
IDENTITY | Auto-increment or identity column; the database assigns the value during the INSERT. | Simple, but Hibernate must run each insert immediately to learn the id, so JDBC insert batching is off. |
SEQUENCE | Hibernate asks a sequence for ids, in blocks of allocationSize (default 50). | Allows batching and few round trips. The sequence's INCREMENT BY must equal allocationSize. |
UUID | Hibernate generates a random UUID before the insert. | No database round trip and safe to expose, but 16 bytes and random order hurts index locality. |
AUTO | Hibernate picks; for a numeric id it uses a sequence named after the entity. | Convenient for demos; be explicit in production so the schema is predictable. |
On PostgreSQL, SEQUENCE with a matching migration is the strongest default for numeric keys. If a Flyway script creates create sequence book_seq increment by 1 while Hibernate keeps its default allocationSize of 50, the two disagree about which block of ids is free. Declare the generator explicitly and keep both numbers the same.
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "book_seq")
@SequenceGenerator(name = "book_seq", sequenceName = "book_seq", allocationSize = 50)
private Long id;Assigned ids and the extra SELECT
Without @GeneratedValue you set the id yourself, for example a UUID created in the constructor. Then the id is never null, Spring Data assumes the entity already exists, and save() calls merge, which first runs a SELECT to look for the row. Every insert costs two statements. Fix it by adding a @Version field (a null version also means new) or by implementing Persistable with your own isNew().
Do not expose sequential ids where guessing them matters: /invoices/1043 invites trying 1042. Authorise every lookup, or publish a separate UUID while keeping a compact numeric key internally.
Example
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.SequenceGenerator;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
class Book {
(strategy = GenerationType.SEQUENCE, generator = "book_seq")
(name = "book_seq", sequenceName = "book_seq", allocationSize = 50)
private Long id;
private String title;
protected Book() { }
Book(String title) { this.title = title; }
Long getId() { return id; }
}
interface BookRepository extends JpaRepository<Book, Long> { }
public class BookService {
private final BookRepository books;
public BookService(BookRepository books) { this.books = books; }
public Long add(String title) {
Book saved = books.save(new Book(title));
return saved.getId(); // assigned from the sequence block, before the INSERT is flushed
}
}When to use it
- Importing a large CSV of products with sequence ids so Hibernate can batch the inserts.
- Exposing order references in public URLs as UUIDs so customers cannot enumerate other orders.
- Storing events whose id is supplied by the producer, without a lookup before each insert.
Frequently asked questions
Should I use IDENTITY or SEQUENCE on PostgreSQL?
SEQUENCE, unless the schema is fixed to identity columns. Sequences let Hibernate allocate ids in blocks and batch inserts; IDENTITY forces one INSERT per entity as soon as it is persisted.
Why does save() run a SELECT before my INSERT?
Your id is set before saving, so Spring Data assumes the row exists and calls merge, which loads it first. Use a generated id, add a @Version field, or implement Persistable and return true from isNew() for new instances.
Can I use a primitive long as the id?
It works, but 0 then has to mean 'new' and it is easy to get wrong. A Long that is null until saved is clearer and is what Spring Data's default new-entity check expects.
More examples
Flyway migration matching the generator
INCREMENT BY 50 matches allocationSize = 50. Hibernate reads one value from the sequence and hands out the next 50 ids from memory, so inserting 50 books costs one sequence call.
-- V1__books.sql
create sequence book_seq start with 1 increment by 50;
create table book (
id bigint primary key,
title varchar(255) not null
);A UUID key generated by Hibernate
The id is still null until save(), so Spring Data correctly treats the entity as new and calls persist. The UUID is safe to put in URLs without revealing how many keys exist.
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository;
@Entity
class ApiKey {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
private String owner;
protected ApiKey() { }
ApiKey(String owner) { this.owner = owner; }
UUID getId() { return id; }
}
public interface ApiKeyRepository extends JpaRepository<ApiKey, UUID> { }Assigned ids without the extra SELECT
The id comes from the caller (for example an idempotency key), so it is never null. Persistable tells Spring Data the entity is new until it has been persisted or loaded, so save() issues one INSERT instead of a SELECT followed by an INSERT.
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.PostLoad;
import jakarta.persistence.PostPersist;
import jakarta.persistence.Transient;
import java.util.UUID;
import org.springframework.data.domain.Persistable;
@Entity
public class Shipment implements Persistable<UUID> {
@Id
private UUID id;
private String carrier;
@Transient
private boolean isNew = true;
protected Shipment() { }
public Shipment(UUID id, String carrier) {
this.id = id;
this.carrier = carrier;
}
@Override
public UUID getId() { return id; }
@Override
public boolean isNew() { return isNew; }
@PostLoad
@PostPersist
void markNotNew() { this.isNew = false; }
}
Discussion