Entités JPA : annotations, constructeurs et cycle de vie
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 (dateNaissancedevientdate_naissanceavec 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.LAZYsur les@ManyToOne. Par défaut, ils sont chargés systématiquement : lire une commande charge son client, qui peut charger d'autres choses… AvecLAZY, le client n'est chargé que si tu en as besoin. Les@OneToManysont déjàLAZYpar 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 :
LAZYsur 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
ajouterChoixremplit les deux côtés. C'est le champquestiondu choix qui est écrit en base, pas la liste. Un choix ajouté à la liste sans sa question donne une insertion avec unquestion_idnul, que la base refuse. AvecajouterChoix, impossible d'oublier l'un des deux côtés, et le constructeur non public deChoixempêche de créer un choix sans passer par la question. - Ne remplace jamais la liste. Un
this.choix = nouvelleListeavecorphanRemovalprovoque 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 chargementLAZY, Hibernate crée une sous-classe de ton entité (un proxy, comme Spring : voir la fiche sur les proxies Spring). Une classefinall'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 :
@Columnsur 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=updateoucreatesur un environnement partagé.createsupprime et recrée les tables au démarrage ;updatemodifie 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 = trueou@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 uneLazyInitializationExceptionhors 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
- Un constructeur sans argument
protectedpour JPA, et un constructeur métier qui crée un objet valide. EnumType.STRINGsur tous les enums.LAZYsur les@ManyToOne.- Des méthodes au nom métier plutôt que des setters pour tout :
suspendre(),annuler(). @Columndécrit la table, la validation des données reçues se fait sur le DTO.- Pas de
@DataLombok, pas detoStringsur les relations. - Pas de
recordni de classefinalpour une entité. - Toute modification d'une entité gérée est enregistrée : ne transforme jamais une entité « pour l'affichage ».
@Transactionalsur les méthodes de service qui modifient, etreadOnly = truesur celles qui lisent.- Construis le DTO dans la transaction pour éviter les
LazyInitializationException. - Des scripts de migration pour le schéma,
ddl-auto=validate, jamaisupdatenicreatesur 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 ;@NotNullest 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,@PreUpdateou l'audit de Spring Data remplissent automatiquement les dates de création et de modification.