
Mapping d'entités simplifié avec Spring Data JDBC
Comprenez comment Spring Data JDBC simplifie le mapping objet-relationnel en privilégiant les conventions et en utilisant un jeu réduit d'annotations (@Table, @Id, @MappedCollection).
Une approche différente du mapping ORM
Contrairement à Spring Data JPA qui s'appuie sur la spécification JPA et des ORM riches comme Hibernate, Spring Data JDBC adopte une approche délibérément plus simple et plus directe du mapping objet-relationnel. L'objectif n'est pas de fournir toutes les fonctionnalités complexes d'un ORM complet (comme le lazy loading transparent, la gestion fine du cycle de vie dans un contexte de persistance complexe, ou le mapping d'héritage élaboré), mais plutôt de faciliter le mapping de base entre vos objets Java et les tables relationnelles avec un minimum de "magie".
Cette simplification repose fortement sur des conventions et un nombre limité d'annotations. Spring Data JDBC encourage une vision où vos entités sont de simples conteneurs de données (proches des POJOs ou des Records Java), et où les relations, en particulier celles entre différents agrégats (ensembles d'objets traités comme une unité), sont gérées de manière plus explicite.
Conventions de nommage et annotations de base
Spring Data JDBC utilise des conventions pour déduire les noms des tables et des colonnes à partir de vos classes et champs Java :
- Nom de table : Par défaut, le nom de la table est dérivé du nom simple de la classe d'entité, converti en snake_case (ex: la classe
UserProfilesera mappée à la tableuser_profile). Vous pouvez surcharger ce comportement avec l'annotation@Table("nom_specifique_table")placée sur la classe. - Nom de colonne : Par défaut, le nom de la colonne est dérivé du nom du champ Java, converti en snake_case (ex: le champ
firstNamesera mappé à la colonnefirst_name). L'annotation@Column("nom_specifique_colonne")peut être utilisée pour spécifier un nom différent, bien qu'elle soit moins fréquemment nécessaire qu'en JPA si vous suivez les conventions. - Identifiant Primaire : Vous devez marquer le champ qui correspond à la clé primaire de votre table avec l'annotation
@Id(deorg.springframework.data.annotation.Id). Spring Data JDBC s'attend à ce qu'il y ait un champ@Id.
Exemple d'entité simple :
package com.certiquizz.jdbc.entity;
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Table;
// import org.springframework.data.relational.core.mapping.Column; // Si besoin
@Table("app_users") // Spécifie le nom de la table
public class SimpleUser {
@Id // Marque la clé primaire
private Long userId;
private String username;
private String emailAddress; // Sera mappé à 'email_address' par convention
private boolean active;
// Constructeur(s), Getters, Setters sont nécessaires pour l'instanciation et l'accès
// Un constructeur avec tous les arguments est souvent pratique
public SimpleUser(Long userId, String username, String emailAddress, boolean active) {
this.userId = userId;
this.username = username;
this.emailAddress = emailAddress;
this.active = active;
}
// Getters et Setters (ou utiliser Lombok)
public Long getUserId() { return userId; }
// ... autres getters/setters ...
// toString() pour le débogage
}
Gestion des relations : Le concept d'Agrégat
La gestion des relations est l'un des points où Spring Data JDBC diffère le plus significativement de JPA. Il s'appuie fortement sur le concept d'Agrégat issu du Domain-Driven Design (DDD). Un agrégat est un groupe d'objets (entités) qui sont traités comme une seule unité conceptuelle et transactionnelle. L'entité principale de l'agrégat est appelée Aggregate Root.
- Relations au sein d'un Agrégat (One-to-Many/One-to-One interne) :
- Spring Data JDBC permet de mapper des relations simples (généralement One-to-Many) *à l'intérieur* des limites d'un agrégat.
- Pour une relation One-to-Many (ex: une
Orderayant plusieursOrderItem), vous utilisez un champ de type collection (SetouList) dans l'Aggregate Root (Order). - Ce champ de collection doit être annoté avec
@MappedCollection(idColumn = "FK_COL_IN_CHILD_TABLE", keyColumn = "KEY_COL_FOR_MAP"). L'attributidColumnest crucial : il spécifie le nom de la colonne dans la table de l'entité *enfant* (order_item) qui contient la clé étrangère pointant vers l'Aggregate Root (ex:order_id). L'attributkeyColumnest utilisé uniquement si la collection est uneMap. - Lorsque vous sauvegardez l'Aggregate Root (ex:
Order), Spring Data JDBC sauvegarde aussi automatiquement les entités enfants (OrderItem) présentes dans la collection marquée avec@MappedCollection. De même, la suppression de l'Aggregate Root peut entraîner la suppression des enfants.
- Références entre Agrégats (Relation externe) :
- Spring Data JDBC ne mappe pas directement les références objet vers d'autres Aggregate Roots. L'objectif est de maintenir des limites claires entre les agrégats.
- Si une entité (ex:
Order) a besoin de référencer un autre Aggregate Root (ex:Customer), vous ne déclarez pas un champprivate Customer customer;dansOrder. - A la place, vous stockez simplement l'identifiant de l'autre Aggregate Root :
private Long customerId;dans l'entitéOrder. - Pour charger l'objet
Customerassocié à uneOrder, vous devrez explicitement utiliser leCustomerRepositoryen passant lecustomerIdrécupéré depuis l'Order.
Exemple d'Agrégat (Order / OrderItem) :
package com.certiquizz.jdbc.entity;
import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.MappedCollection;
import org.springframework.data.relational.core.mapping.Table;
import java.time.LocalDate;
import java.util.HashSet;
import java.util.Set;
@Table("customer_orders")
public class CustomerOrder { // Aggregate Root
@Id
private Long id;
private LocalDate orderDate;
private Long customerId; // Référence à un autre Aggregate Root (Customer) via ID
// Relation One-to-Many au sein de l'agrégat
@MappedCollection(idColumn = "ORDER_ID") // FK dans la table 'order_items'
private Set items = new HashSet<>();
// Méthode pour ajouter des items (maintient la cohérence)
public void addItem(String productCode, int quantity, double price) {
this.items.add(new OrderItem(productCode, quantity, price));
}
// Constructeur, Getters, Setters...
// ...
}
@Table("order_items")
public class OrderItem { // Entité enfant dans l'agrégat Order
// @Id // PAS d'ID ici, car fait partie de l'agrégat Order
// L'idColumn de @MappedCollection dans CustomerOrder (ORDER_ID) sert de lien
private String productCode;
private int quantity;
private double price;
// Constructeur, Getters, Setters...
public OrderItem(String productCode, int quantity, double price) {
this.productCode = productCode;
this.quantity = quantity;
this.price = price;
}
// ...
}
Dans cet exemple, la sauvegarde d'un CustomerOrder via son repository entraînera la sauvegarde de ses OrderItem associés grâce à @MappedCollection. Cependant, pour obtenir les informations du Customer lié, il faudra utiliser le customerId et un CustomerRepository.
Autres annotations utiles
D'autres annotations peuvent être utilisées occasionnellement :
@Transient(org.springframework.data.annotation.Transient) : Marque un champ pour qu'il soit ignoré par le processus de persistance (similaire à@Transientde JPA).@Version(org.springframework.data.annotation.Version) : Utilisé pour le verrouillage optimiste. Marque un champ (généralement numérique) qui sera incrémenté à chaque mise à jour.@PersistenceCreator: Permet de marquer un constructeur ou une méthode de fabrique statique spécifique que Spring Data JDBC doit utiliser pour créer des instances d'entités lors de la lecture depuis la base de données. Souvent non nécessaire si un constructeur public ou package-private approprié existe.
Conclusion : Un mapping centré sur les agrégats et les conventions
Le mapping d'entités dans Spring Data JDBC est volontairement plus simple que celui de JPA. Il favorise les conventions de nommage pour les tables et les colonnes, utilise un jeu réduit d'annotations (@Table, @Id, @MappedCollection principalement), et met l'accent sur le concept d'Agrégat. Les relations au sein d'un agrégat sont gérées de manière relativement simple via @MappedCollection, tandis que les relations entre agrégats sont exprimées par la conservation d'identifiants plutôt que par des références objet directes dans le mapping. Cette approche offre moins d'abstraction ORM mais donne potentiellement plus de contrôle et peut être plus facile à comprendre pour ceux qui préfèrent rester plus proches du modèle relationnel et de SQL.