Contactez-nous

Gestion des requêtes et des relations

Apprenez comment Spring Data JDBC gère les requêtes (dérivées, @Query) et les relations entre entités (@MappedCollection), en mettant l'accent sur la simplicité et le contrôle SQL.

Au-delà du CRUD : Exécuter des requêtes spécifiques

Si l'interface CrudRepository fournie par Spring Data JDBC offre les opérations de base pour la persistance, les applications réelles nécessitent souvent des requêtes plus spécifiques pour rechercher ou manipuler des données selon des critères précis. Spring Data JDBC, tout comme son homologue JPA, propose deux mécanismes principaux pour définir ces requêtes personnalisées : les méthodes de requête dérivées (Query Methods) et les requêtes explicites avec l'annotation @Query.

De plus, la manière dont Spring Data JDBC gère les relations entre les entités est fondamentalement différente de celle de JPA, privilégiant la simplicité et le contrôle explicite sur l'automatisation complexe. Comprendre ces deux aspects est essentiel pour utiliser efficacement Spring Data JDBC.

Méthodes de requête dérivées : La simplicité déclarative

Similairement à Spring Data JPA, Spring Data JDBC peut déduire des requêtes SQL simples directement à partir des noms de méthodes définies dans votre interface Repository. En respectant une convention de nommage (findBy[Propriété][Condition], countBy..., existsBy..., etc.), vous pouvez définir des opérations de recherche sans écrire une seule ligne de SQL.

Spring Data JDBC analyse le nom de la méthode et génère la requête SQL correspondante. Les mots-clés supportés (And, Or, LessThan, GreaterThan, Like, OrderBy, etc.) sont similaires à ceux de JPA.

import org.springframework.data.repository.CrudRepository;
import java.util.List;

public interface BookRepository extends CrudRepository {

    // Trouve les livres par titre (SQL: SELECT ... FROM book WHERE title = ?)
    List findByTitle(String title);

    // Trouve les livres par auteur en ignorant la casse, triés par année de publication descendante
    // SQL: SELECT ... FROM book WHERE upper(author) = upper(?) ORDER BY publication_year DESC
    List findByAuthorIgnoreCaseOrderByPublicationYearDesc(String author);

    // Compte les livres publiés après une certaine année
    // SQL: SELECT count(*) FROM book WHERE publication_year > ?
    long countByPublicationYearGreaterThan(int year);

    // Vérifie si un livre avec un certain ISBN existe
    // SQL: SELECT count(*) FROM book WHERE isbn = ?
    boolean existsByIsbn(String isbn);
}

// Entité Book (simplifiée pour JDBC)
@Table("book") // Nom de la table
class Book {
    @Id // Marqueur de clé primaire
    private Long id;
    private String title;
    private String author;
    private String isbn;
    private int publicationYear;
    // Constructeurs, Getters...
}

Cette approche est idéale pour les requêtes de recherche simples et améliore la lisibilité et la maintenabilité par rapport à l'écriture manuelle de SQL pour ces cas courants.

Requêtes personnalisées avec `@Query` : Le contrôle total du SQL

Pour les requêtes plus complexes qui ne peuvent pas être exprimées facilement par une méthode dérivée (jointures spécifiques, fonctions SQL, sous-requêtes, etc.), Spring Data JDBC permet de définir explicitement la requête SQL à exécuter via l'annotation @Query.

Vous placez l'annotation @Query sur la méthode de votre interface Repository et fournissez la chaîne de caractères SQL dans son attribut value. Vous pouvez utiliser des paramètres nommés (:nomParam) dans votre SQL, qui seront liés aux paramètres de la méthode annotés avec @Param("nomParam"). C'est l'approche recommandée par rapport aux paramètres positionnels (?1, ?2).

import org.springframework.data.jdbc.repository.query.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import java.util.List;

public interface AdvancedBookRepository extends CrudRepository {

    @Query("SELECT b.* FROM book b WHERE b.publication_year BETWEEN :startYear AND :endYear ORDER BY b.title")
    List findBooksPublishedBetween(@Param("startYear") int start, @Param("endYear") int end);

    // Exemple avec une fonction SQL spécifique (ex: lower)
    @Query("SELECT * FROM book WHERE lower(title) LIKE lower(concat('%', :keyword, '%'))")
    List searchByTitleKeyword(@Param("keyword") String keyword);
    
    // Exemple de requête de mise à jour (nécessite @Modifying)
    @Modifying // Indique une requête qui modifie des données (UPDATE, DELETE, INSERT)
    @Query("UPDATE book SET author = :newAuthor WHERE id = :bookId")
    int updateAuthor(@Param("bookId") Long id, @Param("newAuthor") String author);
}

L'annotation @Modifying est requise pour les requêtes qui modifient des données (UPDATE, DELETE). La méthode peut retourner void ou un int indiquant le nombre de lignes affectées.

@Query vous donne un contrôle total sur le SQL exécuté, ce qui est essentiel pour l'optimisation fine ou l'utilisation de fonctionnalités spécifiques à votre base de données.

Gestion des relations : L'approche centrée sur les agrégats

La différence la plus significative entre Spring Data JDBC et JPA réside dans la gestion des relations. Spring Data JDBC ne gère pas automatiquement les relations complexes comme le fait JPA (pas de lazy loading, pas de cascading automatique complexe, pas de dirty checking sur les collections).

Il adopte une approche plus simple et plus explicite, souvent alignée sur les concepts d'Agrégats de Domain-Driven Design (DDD). Un agrégat est un groupe d'objets (entités) qui sont traités comme une seule unité. Vous chargez et sauvegardez généralement la racine de l'agrégat.

Relations One-to-Many / Many-to-Many : Pour représenter une collection d'entités liées (par exemple, les `Livres` d'un `Auteur`), vous utilisez l'annotation @MappedCollection sur le champ de la collection dans l'entité racine (`Auteur`).

import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.MappedCollection;
import org.springframework.data.relational.core.mapping.Table;
import java.util.Set;

@Table("author")
class Author {
    @Id
    private Long id;
    private String name;

    // La collection de livres liés
    // 'idColumn' spécifie la colonne dans la table 'book' qui référence l'ID de l'auteur.
    @MappedCollection(idColumn = "AUTHOR_ID", keyColumn = "ID") 
    private Set books;
    
    // Constructeurs, Getters, Setters...
}

@Table("book")
class Book {
    @Id
    private Long id;
    private String title;
    private int publicationYear;
    // Colonne pour la clé étrangère vers Author (nom spécifié dans idColumn)
    // Spring Data JDBC N'UTILISE PAS cette propriété pour le mapping de la relation,
    // mais elle est utile pour la cohérence du modèle objet et potentiellement pour d'autres requêtes.
    // private Long authorId; 
    
    // Constructeurs, Getters, Setters...
}

Comment ça fonctionne :

  • Chargement : Lorsque vous chargez un Author via son repository (par exemple, authorRepository.findById(id)), Spring Data JDBC exécute une première requête pour l'auteur, puis une seconde requête séparée pour charger tous les Book où la colonne AUTHOR_ID correspond à l'ID de l'auteur chargé. Il n'y a pas de lazy loading ici ; la collection est chargée immédiatement.
  • Sauvegarde : Lorsque vous appelez authorRepository.save(author), Spring Data JDBC sauvegarde l'entité Author. Cependant, il ne sauvegarde PAS automatiquement les changements effectués dans la collection books. Si vous avez ajouté, modifié ou supprimé des livres dans le Set, vous devez explicitement appeler bookRepository.save(book) ou bookRepository.delete(book) pour ces livres. Spring Data JDBC gère uniquement la mise à jour de la colonne de clé étrangère (AUTHOR_ID) si nécessaire lors de la sauvegarde de l'entité Book.

Relations One-to-One : Peuvent souvent être gérées avec @Embedded si l'objet lié n'a pas sa propre identité et peut être stocké dans les colonnes de la même table. Si ce sont des entités distinctes, la gestion est similaire à celle d'une relation ToMany, mais sans collection.

Cette approche force le développeur à être explicite sur la manière dont les relations sont chargées et sauvegardées, offrant plus de contrôle mais nécessitant plus de code manuel pour gérer le cycle de vie des entités liées par rapport à JPA.

Accès direct via `JdbcTemplate`

Bien que les repositories offrent une abstraction pratique, Spring Boot configure également un JdbcTemplate et un NamedParameterJdbcTemplate. Si vous avez besoin d'effectuer des opérations JDBC très spécifiques, des requêtes batch complexes, ou d'interagir avec des procédures stockées de manière plus directe, vous pouvez toujours injecter ces templates dans vos services et les utiliser directement.

Cela offre une porte de sortie pour les scénarios où l'abstraction du repository ne suffit pas ou lorsque vous avez besoin de performances maximales en contrôlant chaque aspect de l'interaction JDBC.

Conclusion : Contrôle et simplicité pour les relations

En résumé, Spring Data JDBC gère les requêtes via les méthodes dérivées pour les cas simples et l'annotation @Query pour un contrôle SQL total. Sa gestion des relations, basée sur @MappedCollection et un chargement explicite, diffère nettement de l'approche ORM de JPA. Elle favorise la simplicité, la prévisibilité et le contrôle par le développeur sur le cycle de vie des entités liées, au détriment de l'automatisation fournie par JPA.