CodeGym /Cours /Spring Data JPA /@Id : IDENTITY et SEQUENCE

@Id : IDENTITY et SEQUENCE

Spring Data JPA
Niveau 5, Leçon 1
Disponible

1. Le rôle de @Id dans une entité

Si @Entity est le « passeport du citoyen JPA », alors @Id en est le numéro de passeport — sans lui, le citoyen existe, mais le système ne le connaît pas. Hibernate, en tant qu'ORM, ne se contente pas de stocker un objet pour « voir plus tard » ce qu'il en fait. Il doit répondre sans ambiguïté à la question : « Cet objet est-il un nouvel enregistrement ou existe-t-il déjà ? »

Au niveau SQL, vous connaissez déjà la réponse : dans une table, un enregistrement est identifié par sa clé primaire (PRIMARY KEY). En JPA, cela se traduit par un champ annoté @Id.

Forme minimale — volontairement la plus simple possible :

import jakarta.persistence.Entity;
import jakarta.persistence.Id;

@Entity // On dit à JPA/Hibernate : cette classe doit être mappée sur une table
public class Category {

    @Id // C'est la clé primaire de l'entité (ce qui distingue les enregistrements pour Hibernate)
    private Long id; // Long, pour pouvoir avoir null avant la sauvegarde (id pas encore attribué)
}

Deux idées pratiques que les débutants oublient souvent :

Première : @Id n'est pas « n'importe quel champ », c'est le champ qui relie le monde objet à l'identité relationnelle. L'ORM ne peut pas fonctionner normalement avec une entité sans identifiant : elle ne sait alors pas quoi mettre à jour, quoi supprimer, ni comment distinguer un objet d'un autre.

Deuxième : on commence presque toujours avec le type Long plutôt que long. La raison est simple et très pragmatique : avant la sauvegarde en base, un nouvel objet n'a généralement pas encore d'id, et en Java cela s'exprime naturellement par null. Avec long, la « valeur vide » serait 0, et vous vous retrouveriez dans le petit cirque suivant : l'objet est censé être nouveau, mais son id vaut déjà 0, et vous passez du temps à vous demander « mais que se passe-t-il donc ? ». Donc Long — ce n'est pas un caprice, c'est une précaution d'ingénierie normale.

2. Attribution manuelle de l'id : simple en apparence, difficile à vivre

On est parfois tenté de faire « au plus simple » : puisque l'id est de type Long, je vais définir les valeurs moi-même, sans @GeneratedValue. C'est d'autant plus tentant après avoir découvert les objets Java classiques : « je peux bien écrire setId(1L), pourquoi pas ? ». Vous pouvez. Mais vous acceptez alors le rôle de « mini-base de données ».

Exemple d'id manuel (notez : pas de @GeneratedValue) :

import jakarta.persistence.Entity;
import jakarta.persistence.Id;

@Entity // L'entité existe...
public class Category {

    @Id // ...mais on s'engage à attribuer l'id nous-mêmes
    private Long id;
}

Et quelque part dans le code, vous faites quelque chose du genre (simple illustration de l'idée) :

Category category = new Category();
category.setId(100L); // Vous inventez l'id et assumez la responsabilité de son unicité

Pourquoi c'est généralement une mauvaise idée dans un service applicatif :

Vous êtes responsable de l'unicité. Dans la table, la PRIMARY KEY exigera de toute façon l'unicité. Si vous assignez accidentellement un id déjà existant, vous obtenez un conflit. Si ce n'est pas accidentel mais que « deux threads en parallèle » ont choisi la même valeur, le conflit sera encore plus garanti. Et si un jour vous souhaitez faire évoluer l'application, ne serait-ce qu'en lançant deux instances, ce conflit commencera à arriver « à heure fixe ».

Il existe des situations où un id manuel se justifie. Par exemple, vous importez un référentiel depuis un système externe, et l'id externe constitue bien un identifiant stable. Mais dans notre mini-shop d'apprentissage, nous ne construisons pas le système autour d'identifiants externes — l'attribution manuelle est donc davantage une démonstration de ce qui est possible qu'un style à adopter « par défaut ».

3. @GeneratedValue et la génération de l'id

Dans la vraie vie, on souhaite presque toujours que l'identifiant technique soit créé automatiquement par la base de données — c'est précisément pour cela qu'elle existe. En JPA, on utilise @GeneratedValue. Et ici, il est utile d'écarter d'emblée une illusion : @GeneratedValue n'est pas de la « magie Hibernate », c'est simplement une déclaration : la source de la valeur de l'id n'est pas moi, mais la stratégie de génération.

Le bout de code minimal ressemble à ceci :

import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Id // Le champ est la clé primaire de l'entité
@GeneratedValue(strategy = GenerationType.IDENTITY) // La valeur sera générée par la base lors de l'INSERT
private Long id; // Généralement null avant l'insertion en BDD

Il existe plusieurs stratégies, mais aujourd'hui nous nous intéressons aux deux vraiment importantes dans le monde PostgreSQL, que vous devez distinguer dès maintenant :

GenerationType.IDENTITY — la base génère l'id au moment de l'insertion d'une ligne dans la table.

GenerationType.SEQUENCE — la base fournit l'id depuis un objet générateur séparé appelé séquence.

On peut rencontrer GenerationType.AUTO, qui signifie « laisse le provider choisir ». Mais dans un projet d'apprentissage, c'est souvent une mauvaise idée : il est important de comprendre pourquoi le SQL ressemble à ça, plutôt que de se demander « quelle stratégie a-t-il choisie aujourd'hui ? ». Nous essaierons donc de toujours écrire la stratégie explicitement.

Pour ne pas tout garder en tête, il est utile de fixer la comparaison dans un petit tableau.

Critère IDENTITY SEQUENCE
Où vit le générateur Dans la colonne de la table (identity column) Objet BDD séparé : sequence
Quand l'id est connu Après l'INSERT (la BDD a retourné la valeur) Avant l'INSERT (on a récupéré nextval)
Schéma SQL typique insert ... returning id select nextval(...) → insert ... (id, ...)
Style PostgreSQL Correct, mais syntaxe « un peu plus récente » Classique PostgreSQL, très répandu
« Douleur » fréquente du débutant « Pourquoi l'id n'apparaît qu'après l'insertion ? » « Pourquoi les id sautent/ont des trous ? »

Nous allons ensuite examiner chaque stratégie séparément, de façon très concrète — pour qu'ensuite vous puissiez ouvrir le log SQL et comprendre exactement ce qui s'y passe.

4. GenerationType.IDENTITY : « d'abord l'INSERT, ensuite on connaît l'id »

IDENTITY — c'est l'approche « laisse la table elle-même donner un nouveau numéro de ligne ». Humainement, ça ressemble à un café : vous passez commande, et seulement après, la caisse imprime un ticket avec un numéro. Tant que le ticket n'est pas imprimé, vous ne connaissez pas le numéro — il naît au moment de l'opération.

En JPA, ça ressemble à ceci :

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity // On mappe la classe sur une table
public class Category {

    @Id // PK en termes JPA
    @GeneratedValue(strategy = GenerationType.IDENTITY) // l'id ne sera connu qu'après l'INSERT
    private Long id;
}

Ce qui se passe généralement dans PostgreSQL au niveau DDL : la colonne est déclarée comme identity. De façon simplifiée, ça peut ressembler à :

create table category (
  id bigint generated by default as identity primary key
);

(La syntaxe peut varier dans les détails, mais l'idée est la même : l'id est généré automatiquement par la base.)

L'effet pratique le plus important d'IDENTITY : l'id est connu quand la base a exécuté l'INSERT. Autrement dit, Hibernate ne peut pas le connaître à l'avance, car cette valeur naît à l'intérieur de la BDD. C'est pourquoi, lors de la sauvegarde d'une nouvelle entité, il est obligé d'abord d'exécuter l'insertion pour obtenir la clé.

Si vous activez le logging SQL, vous verrez souvent quelque chose de similaire par le sens :

insert into category (code, name) values (?, ?) returning id
-- La BDD retourne l'id, Hibernate le place dans le champ de l'objet (après l'INSERT)

Et là surgit la question classique du débutant : « Alors quand est-ce que l'id apparaît dans l'objet ? » Dans le cadre de cette leçon, retenons le modèle mental simple : après que l'enregistrement est réellement inséré en base. N'essayez pas de « deviner » l'id à l'avance et ne le considérez pas comme un beau numéro séquentiel — c'est un piège en soi, on y reviendra ci-dessous.

IDENTITY a des avantages : pas besoin de penser aux objets sequence, le modèle semble plus simple. Mais il y a aussi des inconvénients qu'il est utile de connaître, même si vous n'optimisez pas encore. Avec IDENTITY, Hibernate « manœuvre » souvent moins lors des insertions, car il doit immédiatement exécuter l'INSERT pour obtenir l'id. Dans un petit projet d'apprentissage, ce n'est pas catastrophique, mais comprendre la mécanique sera utile très bientôt, quand vous commencerez à regarder le SQL avec les yeux d'un ingénieur plutôt que comme du « bruit dans les logs ».

5. GenerationType.SEQUENCE : « donnez-moi un numéro d'avance »

Si IDENTITY, c'est « la caisse imprime le numéro après la commande », alors SEQUENCE, c'est le distributeur de tickets dans une file d'attente électronique : vous prenez d'abord votre numéro, puis vous effectuez les actions. Dans PostgreSQL, les séquences font partie intégrante de l'écosystème : ce sont des objets BDD séparés capables de fournir des nombres uniques via la commande nextval(...).

En JPA, cela se fait généralement ainsi :

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.SequenceGenerator;

@Entity // On mappe sur la table (généralement category)
public class Category {

    @Id // PK en JPA
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "category_seq") // On prend l'id depuis la sequence
    @SequenceGenerator(name = "category_seq", sequenceName = "category_seq") // name — pour JPA, sequenceName — nom en BDD
    private Long id;
}

Ici, @SequenceGenerator apparaît pour la première fois — pas de panique : rien de mystérieux. On dit simplement : « le générateur en JPA s'appelle category_seq, et la base a une séquence nommée category_seq ».

Dans PostgreSQL, une séquence est généralement créée ainsi (exemple conditionnel pour la compréhension) :

create sequence category_seq start with 1 increment by 1;

Ce que vous verrez dans les logs SQL lors de la sauvegarde d'une nouvelle entité (encore une fois, par le sens, sans promettre que ce sera mot pour mot identique) :

select nextval('category_seq');    -- on a obtenu un nouveau nombre, ex. 1 (cela se produit AVANT l'insert)
insert into category (id, code, name) values (?, ?, ?);  -- on insère la ligne avec cet id

Et voilà une différence importante par rapport à IDENTITY : avec SEQUENCE, Hibernate peut obtenir l'id avant l'insertion de la ligne. Autrement dit, en mémoire, l'id de l'objet peut devenir non nul avant que vous ne voyiez l'INSERT, même si les données n'arrivent en base qu'après l'écriture réelle.

Une autre chose qui va surprendre presque tous les débutants : les séquences ne garantissent pas un ordre « sans trous ». Même si la séquence délivre les nombres 1, 2, 3… cela ne signifie pas que les id dans la table seront parfaitement consécutifs sans lacunes. Les trous apparaissent pour des raisons tout à fait normales : une transaction a été annulée, un enregistrement a été supprimé, l'application a réservé un id et ne l'a pas utilisé. Ce n'est pas un drame, si vous considérez l'id comme un identifiant technique plutôt que comme un beau numéro pour le client.

6. @SequenceGenerator : noms et allocationSize

@SequenceGenerator — c'est là que les gens commettent le plus souvent une erreur du style « presque tout est correct, mais ça ne marche pas ». Le problème, c'est qu'il y a simultanément deux « noms », et le débutant confond facilement lequel est pour Java et lequel est pour la base. En plus, il y a le paramètre allocationSize qui influence directement l'aspect des valeurs d'id et peut créer l'effet de nombres « qui sautent ».

D'abord, les noms. Regardez attentivement cette construction :

import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.SequenceGenerator;

@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "category_seq") // generator — nom du générateur DANS JPA
@SequenceGenerator(name = "category_seq", sequenceName = "category_seq") // sequenceName — nom de la sequence DANS LA BASE
private Long id; // Le champ de la clé primaire lui-même

generator = "category_seq" — c'est une référence au nom du générateur à l'intérieur de JPA. C'est-à-dire un « alias » que vous utiliserez dans les annotations.

sequenceName = "category_seq" — c'est le vrai nom de la séquence dans PostgreSQL. Ce n'est plus un alias, c'est un objet concret dans la base.

L'erreur classique ressemble à ceci : en Java on a écrit sequenceName = "categories_seq", mais la séquence qui existe réellement en base s'appelle category_seq. Du coup, Hibernate essaie honnêtement d'appeler nextval('categories_seq') et reçoit de PostgreSQL le message : « cette séquence n'existe pas ».

Maintenant, allocationSize. Si vous ne le définissez pas, le défaut selon la spécification JPA n'est généralement pas égal à 1 — c'est souvent 50. Cela signifie qu'Hibernate peut prendre des nombres « par lot » et les distribuer en mémoire. Pour les performances, c'est parfois utile, mais en apprentissage cela conduit presque à coup sûr à la question : « Pourquoi mes id sont 1, puis 51, puis 101… j'ai cassé la base ? »

C'est pourquoi, dans le projet d'apprentissage, on fixe souvent allocationSize = 1 pour que le comportement soit le plus transparent possible :

import jakarta.persistence.SequenceGenerator;

@SequenceGenerator(
        name = "category_seq", // Nom du générateur dans JPA (référencé par @GeneratedValue(generator=...)
        sequenceName = "category_seq", // Nom de la sequence dans PostgreSQL (ce qui existe vraiment en BDD)
        allocationSize = 1 // On ne prend pas les valeurs par blocs, pour éviter les "sauts" dans le projet d'apprentissage
)

Assemblons cela en un petit fragment cohérent, pratique à copier dans Category et Product :

import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.SequenceGenerator;

@Id // Clé primaire de l'entité
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "category_seq") // On prend la valeur depuis la sequence
@SequenceGenerator(name = "category_seq", sequenceName = "category_seq", allocationSize = 1) // Comportement transparent dans le projet d'apprentissage
private Long id;

Et un avertissement important : même avec allocationSize = 1, vous n'êtes pas obligé de voir une séquence d'id parfaitement continue sans trous. C'est normal. Votre objectif, c'est l'unicité et l'identité stable de l'enregistrement, pas l'esthétique des chiffres.

7. Choix de la stratégie dans shop-data-jpa

Quand vous faites un projet d'apprentissage, le plus dangereux est de tout laisser « dans l'état par défaut » puis d'être surpris par les logs et de penser que l'ORM « vit sa propre vie ». C'est pourquoi, dans shop-data-jpa, nous fixons une décision explicite : pour PostgreSQL, on prend GenerationType.SEQUENCE et on donne aux séquences des noms clairs selon le modèle <table>_seq.

Ci-dessous, ce n'est pas l'entité complète, mais uniquement le fragment sur la génération de l'id. Le reste de la classe est intentionnellement omis ici : c'est la source de la clé qui nous intéresse maintenant, pas tous les champs à la fois.

Voici à quoi ça ressemble environ — fragment court mais concret pour Category :

import jakarta.persistence.*;

@Entity // C'est une entité JPA
@Table(name = "category") // On fixe explicitement le nom de la table pour ne pas deviner le naming
public class Category {

    @Id // Clé primaire
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "category_seq") // Source de l'id — sequence
    @SequenceGenerator(name = "category_seq", sequenceName = "category_seq", allocationSize = 1) // Nom JPA et nom en BDD + pas transparent
    private Long id;
}

Et de même pour Product : on montre uniquement le fragment sur l'id.

import jakarta.persistence.*;

@Entity // C'est une entité JPA
@Table(name = "product") // On fixe explicitement la table
public class Product {

    @Id // Clé primaire
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "product_seq") // On prend l'id depuis la sequence product_seq
    @SequenceGenerator(name = "product_seq", sequenceName = "product_seq", allocationSize = 1) // allocationSize=1 pour des id prévisibles dans le projet d'apprentissage
    private Long id;
}

Pourquoi c'est un bon choix pour « un projet d'apprentissage mais réaliste » :

Premièrement, PostgreSQL fonctionne très naturellement avec les séquences, et vous verrez facilement dans les logs SQL l'enchaînement nextval → insert. Cela aide à garder en tête le lien « annotation → SQL ».

Deuxièmement, vous obtenez un style uniforme pour toutes les entités du catalogue. Quand, plus tard dans le projet, d'autres entités apparaîtront — commandes, lignes de commande, etc. — vous n'aurez pas à vous rappeler à chaque fois : « est-ce que j'utilise identity ou sequence ici ? » Pour une équipe, c'est un détail, mais un détail agréable : l'uniformité réduit la charge cognitive.

Troisièmement, vous vous entraînez à la discipline dès maintenant : la base contient des objets concrets (category_seq, product_seq), ils sont nommés explicitement dans le code, et il n'y a aucun « devinette ». Pour un débutant, c'est vraiment salvateur : plus facile à déboguer, plus facile à vérifier, plus facile à s'expliquer à soi-même.

8. Erreurs courantes avec @Id et la génération de clés

Erreur n° 1 : utiliser long au lieu de Long et s'étonner ensuite d'un « étrange id = 0 ».
Tant que l'objet n'est pas encore sauvegardé, l'id est logiquement « non défini ». En Java, cela s'exprime par null. Le primitif long ne connaît pas le null, vous obtenez donc 0 et commencez à le percevoir comme une vraie valeur. Cela conduit presque toujours à la confusion et à de mauvaises décisions du genre « considérons 0 comme le signe d'un nouvel objet ».

Erreur n° 2 : mettre à la fois @GeneratedValue et attribuer l'id manuellement.
C'est un conflit de responsabilités classique : vous avez dit à JPA « laisse la base générer », puis vous avez vous-même assigné une valeur via setId(...). Résultat : soit vous obtenez une tentative d'insertion avec votre id et un conflit sur la PRIMARY KEY, soit vous vous retrouvez dans une situation où l'ORM attend une chose et vous en faites une autre.

Erreur n° 3 : attendre que les id soient strictement séquentiels et sans trous.
IDENTITY comme SEQUENCE peuvent produire des « trous » dans les nombres — à cause d'annulations, de suppressions, de parallélisme, de réservations de valeurs et de la vie normale de la base. L'id est une clé technique, pas un « beau numéro pour le client ». Si un beau numéro est nécessaire, on le fait avec un champ séparé et des règles séparées.

Erreur n° 4 : confondre name et sequenceName dans @SequenceGenerator.
name — c'est le nom du générateur dans JPA, sequenceName — le nom de la séquence dans la base. Parfois on change l'un, on oublie de changer l'autre et on obtient l'erreur « sequence not found », alors qu'« on a bien nommé presque pareil ». À retenir : un nom est pour Java, l'autre pour PostgreSQL.

Erreur n° 5 : ne pas contrôler allocationSize et s'effrayer des « sauts » d'id.
Si Hibernate commence à délivrer des id 1, puis 51, puis 101 — ce n'est pas un bug de la base, mais une optimisation courante où les identifiants sont pris par lots. Dans un projet d'apprentissage, mieux vaut fixer allocationSize = 1 pour que le comportement soit plus lisible, et dans un vrai projet en production vous choisirez cela consciemment, pas par accident.

1
Mission
Spring Data JPA, niveau 5, leçon 1
Bloqué
`IDENTITY` pour l'entité `Coupon`
`IDENTITY` pour l'entité `Coupon`
1
Mission
Spring Data JPA, niveau 5, leçon 1
Bloqué
`SEQUENCE` pour l'entité `InventorySnapshot`
`SEQUENCE` pour l'entité `InventorySnapshot`
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION