Entités JPA : annotations, constructeurs et cycle de vie

Publié le · Mis à jour le

L'idée en une phrase

Une entité est une classe Java qui correspond à une table de la base de données : chaque objet est une ligne, chaque champ une colonne. Des annotations décrivent cette correspondance, un constructeur sans argument est obligatoire, et l'objet traverse un cycle de vie géré par Hibernate. C'est ce cycle de vie qui explique la plupart des comportements surprenants de JPA.

Une image pour comprendre : l'archiviste et ses fiches

Imagine un service d'archives, avec un archiviste (Hibernate) qui gère une armoire de fiches (la base de données).

  • Tu remplis une nouvelle fiche sur un brouillon : elle n'existe pas encore dans l'armoire.
  • Tu la donnes à l'archiviste : il la range et lui attribue un numéro.
  • Quand tu demandes une fiche, l'archiviste la pose sur son bureau. Tant qu'elle y est, toute correction que tu y fais est recopiée dans l'armoire à la fin de la journée, sans que tu aies rien à demander.
  • Si tu emportes une photocopie chez toi, tes corrections sur la photocopie ne changent rien dans l'armoire.
  • Une fiche peut être marquée « à détruire » : elle disparaîtra de l'armoire à la fin de la journée.

Garde ces quatre situations en tête : elles correspondent exactement aux quatre états d'une entité, détaillés plus bas.

Une entité minimale

@Entity
@Table(name = "client")
public class Client {

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

    @Column(name = "nom", nullable = false, length = 100)
    private String nom;

    @Column(nullable = false, unique = true)
    private String email;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private StatutClient statut;

    protected Client() {
        // pour JPA uniquement
    }

    public Client(String nom, String email) {
        this.nom = nom;
        this.email = email;
        this.statut = StatutClient.ACTIF;
    }

    // getters, et méthodes métier
}

Ce qui correspond, en base, à :

CREATE TABLE client (
    id     BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    nom    VARCHAR(100) NOT NULL,
    email  VARCHAR(255) NOT NULL UNIQUE,
    statut VARCHAR(255) NOT NULL
);

Les annotations de base

Identifier l'entité

Annotation Rôle
@Entity « cette classe correspond à une table » ; sans elle, Hibernate l'ignore
@Table(name = "client") le nom de la table ; facultatif si c'est le même que la classe, mais le préciser évite les surprises
@Id le champ qui sert de clé primaire
@GeneratedValue la valeur de l'identifiant est générée par la base, pas par toi

Pour @GeneratedValue, deux stratégies sont courantes :

Stratégie Fonctionnement
IDENTITY la base génère l'identifiant à l'insertion (colonne auto-incrémentée) : simple et répandu
SEQUENCE Hibernate demande un numéro à une séquence de la base avant l'insertion ; plus performant pour les insertions en masse

Utilise celle déjà en place dans le projet. Pour un nouveau projet avec PostgreSQL, IDENTITY est un bon point de départ.

Décrire les colonnes

@Column(name = "date_naissance", nullable = false)
private LocalDate dateNaissance;
Attribut de @Column Signification
name le nom de la colonne en base
nullable = false la colonne est NOT NULL
length = 100 la longueur maximale d'un texte (VARCHAR(100))
unique = true la colonne a une contrainte d'unicité
precision, scale pour un montant : precision = 10, scale = 2 donne NUMERIC(10,2)

Note : sans @Column, le champ est quand même enregistré, dans une colonne au nom déduit du champ (dateNaissance devient date_naissance avec la configuration par défaut de Spring Boot). Pour qu'un champ ne soit pas enregistré, il faut l'annoter @Transient.

Les enums : toujours EnumType.STRING

@Enumerated(EnumType.STRING) // ✅ enregistre "ACTIF", "SUSPENDU"…
private StatutClient statut;

Sans cette annotation, JPA enregistre le numéro d'ordre de la valeur dans l'enum : 0 pour la première, 1 pour la deuxième…

enum StatutClient { ACTIF, SUSPENDU }            // ACTIF = 0, SUSPENDU = 1
enum StatutClient { ACTIF, EN_ATTENTE, SUSPENDU } // un collègue ajoute une valeur au milieu

Désormais, tous les clients suspendus enregistrés avant (1) sont relus comme EN_ATTENTE. Aucune erreur, des données silencieusement fausses. Mets toujours EnumType.STRING.

Les dates et les montants

Les types modernes de Java sont reconnus directement, sans annotation :

Type Java Usage
LocalDate une date sans heure (date de naissance)
LocalDateTime une date et une heure, sans fuseau
Instant un instant précis, indépendant du fuseau (date de création, horodatage)
BigDecimal un montant (jamais double)

N'utilise plus java.util.Date ni @Temporal : ce sont les anciennes façons de faire.

Les relations entre entités

Une commande appartient à un client ; un client a plusieurs commandes.

@Entity
public class Commande {

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

    @ManyToOne(fetch = FetchType.LAZY, optional = false) // plusieurs commandes → un client
    @JoinColumn(name = "client_id")
    private Client client;
}
@Entity
public class Client {

    @OneToMany(mappedBy = "client") // un client → plusieurs commandes
    private List<Commande> commandes = new ArrayList<>();
}
Annotation Se lit Où est la clé étrangère ?
@ManyToOne plusieurs de ces objets pointent vers un autre dans cette table (commande.client_id)
@OneToMany(mappedBy = ...) un objet en a plusieurs dans l'autre table ; mappedBy indique le champ qui la gère

Deux règles à retenir :

  • mets fetch = FetchType.LAZY sur les @ManyToOne. Par défaut, ils sont chargés systématiquement : lire une commande charge son client, qui peut charger d'autres choses… Avec LAZY, le client n'est chargé que si tu en as besoin. Les @OneToMany sont déjà LAZY par défaut ;
  • ne renvoie pas ces entités directement en JSON : la relation dans les deux sens provoque une boucle infinie (voir la fiche sur la mémoire en Java). Passe par un DTO.

Note : les relations sont un sujet à part entière (chargement, problème des « N+1 requêtes », cascades). Retiens ici le minimum : LAZY sur les @ManyToOne, et des DTO pour les réponses.

La cascade : enregistrer un parent avec ses enfants

Certains objets forment un tout : une question et ses choix de réponse, une facture et ses lignes. Un choix n'existe pas sans sa question, il n'est partagé avec aucune autre, et il naît et disparaît avec elle. On dit que la question possède ses choix.

Dans ce cas, la cascade est la bonne solution : enregistrer ou supprimer la question fait de même pour ses choix.

Le côté parent : la question

@Entity
public class Question {

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

    @Column(nullable = false)
    private String intitule;

    @OneToMany(mappedBy = "question", cascade = CascadeType.ALL, orphanRemoval = true)
    @OrderBy("ordre")
    private List<Choix> choix = new ArrayList<>();

    protected Question() {
    }

    public Question(String intitule) {
        this.intitule = intitule;
    }

    public void ajouterChoix(String libelle, boolean correct) {
        choix.add(new Choix(this, libelle, correct, choix.size() + 1));
    }

    public void retirerChoix(Choix aRetirer) {
        choix.remove(aRetirer); // orphanRemoval → la ligne est supprimée en base
    }

    public List<Choix> getChoix() {
        return Collections.unmodifiableList(choix);
    }
}

Le côté enfant : le choix

@Entity
public class Choix {

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

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "question_id")
    private Question question;

    @Column(nullable = false)
    private String libelle;

    private boolean correct;
    private int ordre;

    protected Choix() {
    }

    Choix(Question question, String libelle, boolean correct, int ordre) { // visible du package seulement
        this.question = question;
        this.libelle = libelle;
        this.correct = correct;
        this.ordre = ordre;
    }
}

Dans le service, un seul save()

@Transactional
public Long creer(CreationQuestionDto dto) {
    Question question = new Question(dto.intitule());
    dto.choix().forEach(c -> question.ajouterChoix(c.libelle(), c.correct()));
    return questionRepository.save(question).getId(); // ✅ enregistre la question ET ses choix
}

Pas besoin de ChoixRepository pour créer ou supprimer des choix : tout passe par la question.

Option Effet
cascade = CascadeType.ALL enregistrer ou supprimer la question fait de même pour ses choix
orphanRemoval = true un choix retiré de la liste est supprimé en base
mappedBy = "question" c'est le champ question de Choix qui porte la clé étrangère

Les points qui comptent :

  • La méthode ajouterChoix remplit les deux côtés. C'est le champ question du choix qui est écrit en base, pas la liste. Un choix ajouté à la liste sans sa question donne une insertion avec un question_id nul, que la base refuse. Avec ajouterChoix, impossible d'oublier l'un des deux côtés, et le constructeur non public de Choix empêche de créer un choix sans passer par la question.
  • Ne remplace jamais la liste. Un this.choix = nouvelleListe avec orphanRemoval provoque une erreur Hibernate (« A collection with cascade=all-delete-orphan was no longer referenced »). Pour tout remplacer, vide la liste existante puis ajoute les nouveaux choix.
  • Pas de setter qui expose la liste. Le getter renvoie une liste non modifiable, pour que tout ajout passe par ajouterChoix.

Quand ne pas mettre de cascade. La cascade va du propriétaire vers ce qu'il possède, jamais vers un objet partagé :

@ManyToOne(cascade = CascadeType.ALL) // ❌ supprimer une commande supprimerait le client !
private Client client;

Un client a plusieurs commandes et existe sans elles : pas de cascade. Même chose pour un @ManyToMany vers des objets partagés (des thèmes rattachés à plusieurs questions) : une cascade de suppression effacerait des thèmes encore utilisés ailleurs.

Relation Cascade ?
question → choix, commande → lignes, facture → lignes ✅ oui, avec orphanRemoval
commande → client, choix → question ❌ non, c'est l'enfant qui pointe vers le parent
question ↔ thèmes partagés ❌ non

Les constructeurs

Le constructeur sans argument est obligatoire

Quand Hibernate lit une ligne en base, il doit créer un objet avant de le remplir. Il a donc besoin d'un constructeur sans argument. JPA l'exige.

Mais ce constructeur crée un objet vide, donc invalide (un client sans nom ni e-mail). Il ne doit pas être utilisé par ton code. La solution : le rendre protected.

protected Client() {
    // pour JPA uniquement
}

protected suffit à Hibernate, et empêche le reste de l'application d'écrire new Client().

Un constructeur métier qui crée un objet valide

public Client(String nom, String email) {
    this.nom = Objects.requireNonNull(nom);
    this.email = Objects.requireNonNull(email);
    this.statut = StatutClient.ACTIF; // ✅ un nouveau client est toujours actif
}

Ce constructeur garantit qu'un Client créé par ton code est toujours complet et dans un état cohérent. L'identifiant n'y figure pas : c'est la base qui le génère.

Quand la création demande une logique plus riche, une méthode statique au nom explicite se lit mieux :

public static Commande creer(Client client, List<Ligne> lignes) {
    if (lignes.isEmpty()) {
        throw new CommandeVideException();
    }
    Commande commande = new Commande();
    commande.client = client;
    commande.lignes = new ArrayList<>(lignes);
    commande.statut = Statut.EN_COURS;
    return commande;
}

Pas de setter pour tout

Une entité avec un setter public par champ permet à n'importe quel code de la mettre dans un état incohérent (setStatut(ANNULEE) sur une commande déjà livrée). Préfère des méthodes au nom métier, qui vérifient leurs règles (voir la fiche sur la validation métier) :

public void suspendre(String motif) {
    if (statut == StatutClient.SUSPENDU) {
        throw new ClientDejaSuspenduException(id);
    }
    this.statut = StatutClient.SUSPENDU;
    this.motifSuspension = motif;
}

Hibernate n'a pas besoin de setters : quand @Id est placé sur un champ, il lit et écrit directement les champs.

Ce qu'une entité ne peut pas être

  • un record : un record n'a pas de constructeur sans argument et ses champs ne peuvent pas changer. Les records sont parfaits pour les DTO, pas pour les entités ;
  • une classe final : pour le chargement LAZY, Hibernate crée une sous-classe de ton entité (un proxy, comme Spring : voir la fiche sur les proxies Spring). Une classe final l'en empêche.

Contraintes JPA ou annotations de validation ?

C'est la source de confusion la plus fréquente. Ces deux lignes se ressemblent, mais ne font pas la même chose :

@Column(nullable = false)
private String nom;
@NotBlank
private String nom;
@Column(nullable = false, length = 100) @NotNull, @NotBlank, @Size(max = 100)
Vient de JPA (jakarta.persistence) Bean Validation (jakarta.validation)
Décrit la structure de la table une règle sur la valeur de l'objet
Vérifié par la base de données, au moment de l'écriture Hibernate Validator, avant l'écriture
En cas d'erreur erreur SQL de la base, peu lisible ConstraintViolationException, qui nomme le champ

Les deux, ou un seul ?

Une règle simple qui marche bien :

  • @Column sur l'entité : pour décrire fidèlement la table (nullable, length, unique, precision). La base reste la dernière protection ;
  • la validation sur le DTO : c'est là qu'on vérifie les données reçues, avec des messages clairs pour l'utilisateur (voir la fiche sur la validation des DTO) ;
  • pas de doublon systématique d'annotations de validation sur l'entité. Si l'entité n'est créée que par des constructeurs et des méthodes métier qui garantissent un état valide, elle n'en a pas besoin.

Certaines équipes ajoutent aussi @NotNull ou @Size sur les entités, comme filet de sécurité supplémentaire : Hibernate les vérifie alors automatiquement avant chaque insertion ou modification. Ce n'est pas une erreur ; suis la convention du projet.

Qui crée les tables ?

Dans un vrai projet, les tables sont créées par des scripts de migration (Flyway ou Liquibase), versionnés avec le code. Les annotations @Column ne créent alors rien : elles doivent simplement correspondre aux scripts.

spring.jpa.hibernate.ddl-auto=validate # Hibernate vérifie que les entités correspondent aux tables

Note : n'utilise jamais ddl-auto=update ou create sur un environnement partagé. create supprime et recrée les tables au démarrage ; update modifie le schéma de façon incomplète et imprévisible.

Les index

Un index se déclare aussi depuis l'entité, sur @Table :

@Entity
@Table(
    name = "commande",
    indexes = {
        @Index(name = "idx_commande_client", columnList = "client_id"),
        @Index(name = "idx_commande_statut_date", columnList = "statut, date_creation")
    },
    uniqueConstraints = @UniqueConstraint(name = "uk_commande_numero", columnNames = "numero")
)
public class Commande { ... }

columnList prend les noms des colonnes en base, pas ceux des champs Java. Pour un index sur plusieurs colonnes, l'ordre compte : statut, date_creation sert aux recherches par statut, ou par statut et date, mais pas par date seule.

Comme @Column, ces annotations ne servent qu'à générer le schéma (ddl-auto=create ou update). Avec Flyway ou Liquibase, elles ne créent rien. Et ddl-auto=validate vérifie les tables et les colonnes, pas les index : si l'annotation et le script divergent, rien ne te prévient.

En pratique, les index se gèrent donc dans les scripts de migration :

-- V12__index_commandes.sql
CREATE INDEX idx_commande_client ON commande (client_id);
CREATE INDEX idx_commande_statut_date ON commande (statut, date_creation);

Certaines équipes gardent aussi @Index sur l'entité comme documentation. C'est possible, à condition de mettre les deux à jour ensemble.

Deux points utiles :

  • une contrainte d'unicité (unique = true ou @UniqueConstraint) crée déjà un index : inutile d'en ajouter un sur la même colonne ;
  • PostgreSQL n'indexe pas automatiquement les clés étrangères. Une colonne comme client_id, utilisée pour charger les commandes d'un client, mérite presque toujours un index. C'est un oubli fréquent, qui ralentit les requêtes quand la table grossit.

equals, hashCode et toString

equals et hashCode

Par défaut, deux objets Java sont égaux s'ils sont le même objet en mémoire. Pour une entité, ça convient la plupart du temps : Hibernate garantit qu'une même ligne lue deux fois dans la même transaction donne le même objet.

Si tu as besoin de les redéfinir (pour mettre des entités dans un Set, par exemple), le piège est l'identifiant : il vaut null tant que l'entité n'est pas enregistrée, puis il change. Une version sûre :

@Override
public boolean equals(Object autre) {
    if (this == autre) return true;
    if (!(autre instanceof Client client)) return false;
    return id != null && id.equals(client.getId());
}

@Override
public int hashCode() {
    return getClass().hashCode(); // ✅ constant, même quand l'id change
}

Le hashCode constant peut surprendre, mais c'est ce qui permet de ne pas « perdre » une entité dans un HashSet quand son identifiant passe de null à une valeur.

toString et Lombok @Data

Un toString() qui affiche les relations pose deux problèmes :

  • avec des relations dans les deux sens, il boucle à l'infini (StackOverflowError) ;
  • sur une relation LAZY, il déclenche des requêtes en base, ou une LazyInitializationException hors transaction.

Lombok @Data génère précisément equals, hashCode et toString sur tous les champs, relations comprises. N'utilise pas @Data sur une entité. Si le projet utilise Lombok, @Getter suffit, et un toString écrit à la main n'affiche que l'identifiant et quelques champs simples.

Le cycle de vie d'une entité

Voici l'archiviste de l'introduction, avec les vrais noms. Une entité est toujours dans l'un de ces quatre états :

           new Client(...)
                 │
                 ▼
            ┌─────────┐   save()   ┌─────────┐   fin de la transaction   ┌──────────┐
            │ NOUVELLE│ ─────────► │  GÉRÉE  │ ────────────────────────► │ DÉTACHÉE │
            └─────────┘            └─────────┘                           └──────────┘
                                     ▲     │                                  │
                         findById()  │     │ delete()                         │ save()
                                     │     ▼                                  │ (copie dans
                                         ┌───────────┐                        │  une entité gérée)
                                         │ SUPPRIMÉE │                        ▼
                                         └───────────┘                     GÉRÉE
État Dans l'image Ce que ça veut dire
Nouvelle (transient) le brouillon créée avec new, Hibernate ne la connaît pas ; pas encore d'identifiant
Gérée (managed) la fiche sur le bureau Hibernate la surveille : ses modifications seront enregistrées automatiquement
Détachée (detached) la photocopie emportée elle a existé en base, mais Hibernate ne la surveille plus
Supprimée (removed) la fiche marquée « à détruire » elle sera effacée de la base à la fin de la transaction

L'ensemble des entités gérées s'appelle le contexte de persistance : c'est le bureau de l'archiviste. Dans Spring Boot, il dure le temps d'une transaction (@Transactional).

Conséquence n°1 : pas besoin de save() pour modifier

C'est la surprise la plus fréquente :

@Transactional
public void renommer(Long id, String nouveauNom) {
    Client client = clientRepository.findById(id).orElseThrow(); // l'entité est GÉRÉE
    client.renommer(nouveauNom);
    // ✅ aucun save() : la modification est enregistrée à la fin de la transaction
}

À la fin de la transaction, Hibernate compare chaque entité gérée avec son état au moment de la lecture. S'il voit une différence, il écrit un UPDATE. C'est ce qu'on appelle le dirty checking (la détection des modifications).

Un save() en plus ne fait pas de mal, mais il est inutile. Le savoir t'évite surtout l'inverse : une modification non voulue d'une entité gérée sera, elle aussi, enregistrée.

@Transactional
public ClientDto afficher(Long id) {
    Client client = clientRepository.findById(id).orElseThrow();
    client.renommer(client.getNom().toUpperCase()); // ❌ « juste pour l'affichage »… enregistré en base
    return ClientDto.depuis(client);
}

Pour transformer des données à l'affichage, fais-le dans le DTO, jamais sur l'entité.

Astuce : pour les méthodes qui ne font que lire, @Transactional(readOnly = true) indique à Hibernate qu'il n'y aura pas d'écriture. Il ne vérifie alors pas les modifications, ce qui est aussi plus rapide.

Conséquence n°2 : hors transaction, les modifications sont perdues

public void renommer(Long id, String nouveauNom) { // ⚠️ pas de @Transactional
    Client client = clientRepository.findById(id).orElseThrow();
    client.renommer(nouveauNom);
    // ❌ rien n'est enregistré : la transaction de findById est déjà terminée, l'entité est DÉTACHÉE
}

C'est la photocopie emportée chez soi. Deux solutions : ajouter @Transactional à la méthode (la bonne en général), ou appeler save() explicitement.

Conséquence n°3 : save() sur une entité détachée renvoie un autre objet

Client enregistre = clientRepository.save(clientDetache);

Pour une entité détachée, save() copie ses valeurs dans une entité gérée et renvoie cette dernière. L'objet que tu as passé reste détaché. Utilise toujours l'objet renvoyé par save().

Conséquence n°4 : LazyInitializationException

org.hibernate.LazyInitializationException: failed to lazily initialize a collection
of role: Client.commandes: could not initialize proxy - no Session

Une relation LAZY n'est chargée qu'au moment où on y accède. Si l'entité est détachée à ce moment-là, l'archiviste n'est plus là pour aller chercher les données : erreur.

La solution est de charger ce dont tu as besoin pendant la transaction, dans le service, et de construire le DTO avant d'en sortir :

@Transactional(readOnly = true)
public ClientDto afficher(Long id) {
    Client client = clientRepository.findById(id).orElseThrow();
    return new ClientDto(client.getNom(), client.getCommandes().size()); // ✅ accès pendant la transaction
}

Note : Spring Boot active par défaut une option, spring.jpa.open-in-view, qui garde la session ouverte jusqu'à la fin de la requête HTTP et masque souvent ces erreurs. Un avertissement le signale au démarrage. Beaucoup d'équipes la désactivent (spring.jpa.open-in-view=false) pour que les accès en base restent dans les services. Suis la convention du projet.

Réagir aux étapes du cycle de vie

JPA permet d'exécuter une méthode de l'entité à certains moments de son cycle de vie, grâce à des annotations :

Annotation La méthode est appelée…
@PrePersist juste avant la première insertion en base
@PreUpdate juste avant une mise à jour
@PreRemove juste avant une suppression
@PostLoad juste après la lecture depuis la base

L'usage le plus courant : remplir automatiquement des dates.

@Entity
public class Commande {

    private Instant creeeLe;
    private Instant modifieeLe;

    @PrePersist
    void avantCreation() {
        creeeLe = Instant.now();
        modifieeLe = creeeLe;
    }

    @PreUpdate
    void avantModification() {
        modifieeLe = Instant.now();
    }
}

Garde ces méthodes courtes et simples : remplir un champ calculé, une date. N'y mets ni appel à un repository, ni envoi d'e-mail, ni logique métier : elles s'exécutent à un moment choisi par Hibernate, pas par toi.

L'audit avec Spring Data

Pour les dates de création et de modification, Spring Data propose une solution toute prête, qui évite de répéter ces méthodes dans chaque entité :

@Configuration
@EnableJpaAuditing
public class JpaConfig {
}
@Entity
@EntityListeners(AuditingEntityListener.class)
public class Commande {

    @CreatedDate
    @Column(nullable = false, updatable = false)
    private Instant creeeLe;

    @LastModifiedDate
    private Instant modifieeLe;
}

Spring remplit les deux champs automatiquement. Avec un peu de configuration supplémentaire, @CreatedBy et @LastModifiedBy enregistrent aussi qui a fait la modification.

Et @Version ?

Le champ @Version, qui protège contre deux modifications simultanées de la même ligne, fait aussi partie du cycle de vie : Hibernate l'augmente à chaque mise à jour. Il est détaillé dans la fiche sur la concurrence.

Les bonnes habitudes

  1. Un constructeur sans argument protected pour JPA, et un constructeur métier qui crée un objet valide.
  2. EnumType.STRING sur tous les enums.
  3. LAZY sur les @ManyToOne.
  4. Des méthodes au nom métier plutôt que des setters pour tout : suspendre(), annuler().
  5. @Column décrit la table, la validation des données reçues se fait sur le DTO.
  6. Pas de @Data Lombok, pas de toString sur les relations.
  7. Pas de record ni de classe final pour une entité.
  8. Toute modification d'une entité gérée est enregistrée : ne transforme jamais une entité « pour l'affichage ».
  9. @Transactional sur les méthodes de service qui modifient, et readOnly = true sur celles qui lisent.
  10. Construis le DTO dans la transaction pour éviter les LazyInitializationException.
  11. Des scripts de migration pour le schéma, ddl-auto=validate, jamais update ni create sur un environnement partagé.

À retenir

  • Une entité est une classe qui correspond à une table, décrite par des annotations : @Entity, @Id, @Column, @Enumerated(EnumType.STRING), @ManyToOne(fetch = LAZY).
  • JPA exige un constructeur sans argument : rends-le protected, et crée tes objets avec un constructeur métier.
  • @Column(nullable = false) décrit la table ; @NotNull est une règle de validation. Ce ne sont pas les mêmes outils.
  • Une entité est nouvelle, gérée, détachée ou supprimée. Une entité gérée voit ses modifications enregistrées automatiquement à la fin de la transaction ; une entité détachée, non.
  • @PrePersist, @PreUpdate ou l'audit de Spring Data remplissent automatiquement les dates de création et de modification.

Avis sur l'article

Pas encore de note : soyez le premier à donner votre avis.

Les avis sont publiés après validation par le modérateur.

Votre note