🎯 OBJECTIF
Comprendre comment :
@Enumerated — pourquoi ORDINAL est une bombe à retardement silencieuse@Transient@Lob et ses pièges de performance🧠 MODÈLE MENTAL
Ces trois annotations couvrent des cas que la configuration par défaut de JPA ne gère pas : les enums, les champs calculés, et les données volumineuses. Chacune a un piège critique non évident.
@Enumerated(ORDINAL) est le piège le plus dangereux de tout JPA : ajouter un élément au milieu d'un enum corrompt silencieusement toutes les données existantes, sans erreur SQL, sans exception Java. La corruption est indétectable jusqu'à un audit complet de la base. STRING est la seule option acceptable.
@Transient et @Lob ont des sémantiques opposées : @Transient exclut un champ de la persistance (il existe en Java mais pas en base), @Lob l'y inclut massivement (TEXT ou BLOB complet chargé en mémoire à chaque accès). Le piège de @Lob : il n'y a pas de chargement partiel — 1000 entités avec 1 Mo chacune = 1 Go RAM, même si tu n'accèdes jamais au contenu.
public enum OrderStatus { PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED }
@Entity
class Order {
// ✓ STRING — stocke le nom de l'enum
@Enumerated(EnumType.STRING)
@Column(length = 50)
OrderStatus status; // stocke "PENDING", "CONFIRMED", etc.
// ❌ ORDINAL — stocke la position (0, 1, 2...)
@Enumerated(EnumType.ORDINAL)
OrderStatus status;
}java// État initial en DB : status=3 → DELIVERED
enum OrderStatus { PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED }
// 0 1 2 3 4
// Un développeur ajoute PROCESSING au milieu
enum OrderStatus { PENDING, CONFIRMED, PROCESSING, SHIPPED, DELIVERED, CANCELLED }
// 0 1 2 3 4 5java-- Après la modification :
-- status=3 qui était DELIVERED → devient SHIPPED (position 3)
-- status=4 qui était CANCELLED → devient DELIVERED (position 4)
-- Aucune erreur SQL, aucune exception, données corrompuessql🚨 ORDINAL = bombe à retardement
Cas problématiques ORDINAL : ajout au milieu, suppression, réorganisation = corruption garantie des données existantes.
// ✓ Renommer un enum avec STRING — trivial
// SHIPPED → IN_TRANSIT
UPDATE orders SET status = 'IN_TRANSIT' WHERE status = 'SHIPPED';
// ❌ Renommer avec ORDINAL — impossible sans corruption
// La position est gravée, le nom n'a aucune importancejavaRègle absolue
@Enumerated(EnumType.STRING) uniquement. Seule exception : legacy DB avec ORDINAL imposé (très rare et à isoler).
Un champ @Transient existe en Java mais n'est pas dans la base de données : ni dans les INSERT, ni dans les UPDATE, ni dans les SELECT.
@Entity
class Order {
@Id Long id;
@Column(precision = 10, scale = 2)
BigDecimal amountHT;
@Column(precision = 5, scale = 2)
BigDecimal taxRate;
// ✓ Calculé à la volée — pas de colonne amount_ttc en DB
@Transient
private BigDecimal amountTTC;
public BigDecimal getAmountTTC() {
if (amountTTC == null) {
amountTTC = amountHT.multiply(BigDecimal.ONE.add(taxRate));
}
return amountTTC;
}
}javaCas d'usage :
// @PostLoad pour remplir un champ @Transient après chargement depuis la DB
@PostLoad
void computeFullName() {
this.fullName = firstName + " " + lastName;
}java🔑 @Transient vs Java transient
@Transient JPA = exclut de la persistance JPA, mais Jackson/Gson sérialisent quand même le champ.
Java transient = exclut de la sérialisation Java (pas JPA).
Pour exclure des deux : utiliser les deux, ou @JsonIgnore (Jackson).
@Entity
class Article {
@Id Long id;
String title;
@Lob
@Column(columnDefinition = "TEXT")
String content; // texte long — peut stocker plusieurs Mo
}
@Entity
class Document {
@Id UUID id;
String filename;
@Lob
@Column(columnDefinition = "BYTEA") // PostgreSQL
byte[] data; // fichier PDF, image...
}java🚨 @Lob = chargement COMPLET en mémoire, toujours
// ❌ Catastrophique sur gros volumes
List<Document> docs = documentRepository.findAll();
// 1000 documents × 5 Mo = 5 Go en RAM chargés d'un coup
// Même si tu n'accèdes jamais à doc.getData()java@Lob n'a pas de chargement partiel. @Basic(fetch = FetchType.LAZY) sur un Lob est optionnel dans la spec JPA — la plupart des drivers l'ignorent.
Option 1 — Entité séparée (recommandé) :
@Entity
class Document {
@Id Long id;
String title;
@OneToOne(mappedBy = "document", fetch = FetchType.LAZY)
DocumentContent content; // ✓ non chargé par défaut
}
@Entity
class DocumentContent {
@Id Long id;
@OneToOne @MapsId
Document document;
@Lob
byte[] data;
}
// Listing léger — pas de chargement du contenu
List<Document> docs = documentRepository.findAll();
// Chargement du contenu uniquement quand nécessaire
byte[] data = doc.getContent().getData();javaOption 2 — Stockage externe (meilleur pour documents/images) :
@Entity
class Document {
@Id UUID id;
String title;
@Column(length = 500)
String s3Key; // ✓ DB légère, données dans S3/GCS/Azure Blob
}javaRègle d'or pour @Lob
@Lob uniquement pour : logs techniques (quelques Ko), configurations JSON/XML (< 100 Ko), données rarement accédées. Pour documents, images, vidéos → toujours stockage externe (S3, GCS, Azure Blob).
⚡ TL;DR — chaque concept en une ligne
@Enumerated(STRING)
✓ Stocke le nom de l'enum ("PENDING") — lisible en base, safe si l'enum évolue.
⚠ ORDINAL stocke la position — ajouter un élément au milieu corrompt silencieusement toutes les données existantes.
@Transient
✓ Le champ existe en Java mais est ignoré par JPA — idéal pour les valeurs calculées et les projections temporaires.
⚠ Jackson et Gson sérialisent les champs @Transient quand même — ajouter @JsonIgnore si besoin.
@Lob
✓ Stocke du texte long (TEXT) ou binaire (BLOB) en base de données.
⚠ Le BLOB/TEXT complet est chargé en mémoire à chaque accès — pour les gros fichiers, toujours utiliser un stockage externe.
🎓 À retenir
@Enumerated(ORDINAL) est le pire piège de JPA — la corruption est invisible (aucune erreur), progressive (chaque ajout d'élément déplace les suivants), et irréversible sans migration complète des données. Bannir ORDINAL et le mettre dans la checklist de code review.@Lob charge tout en mémoire — même avec fetch = LAZY, le comportement n'est pas garanti par la spec. La seule solution fiable pour les gros blobs est de les séparer dans une entité dédiée ou de les externaliser dans un object store.@Transient n'est pas transient Java — les deux mots ont la même racine mais des effets différents : l'un exclut de JPA, l'autre de la sérialisation Java native. Souvent utile de combiner les deux sur un champ de cache interne.