Contactez-nous

Mise en cache conditionnelle

Apprenez à utiliser les attributs 'condition' et 'unless' avec SpEL pour contrôler finement quand mettre en cache ou invalider des données dans Spring Boot.

Pourquoi une mise en cache conditionnelle ?

L'abstraction de cache de Spring, avec des annotations comme `@Cacheable`, simplifie grandement l'ajout de caching aux méthodes. Par défaut, `@Cacheable` essaie de récupérer une valeur du cache basée sur les arguments de la méthode. Si elle n'est pas trouvée (cache miss), la méthode est exécutée, et son résultat est stocké dans le cache avant d'être retourné.

Cependant, ce comportement par défaut (toujours mettre en cache le résultat après un cache miss) n'est pas toujours souhaitable. Il existe de nombreux scénarios où vous ne voudriez mettre en cache le résultat que si certaines conditions sont remplies. Par exemple :

  • Ne pas mettre en cache si le résultat retourné est `null`.
  • Ne pas mettre en cache si une méthode a retourné une valeur indiquant une erreur ou un état invalide.
  • Mettre en cache uniquement si les arguments d'entrée satisfont certains critères (par exemple, une chaîne de caractères d'une longueur minimale).
  • Mettre en cache uniquement pour certains types d'utilisateurs ou sous certaines conditions métier.

Spring Caching fournit des mécanismes pour gérer ces scénarios grâce à la mise en cache conditionnelle, principalement via les attributs `condition` et `unless` des annotations de caching, qui utilisent la puissance de Spring Expression Language (SpEL).

Contrôler la mise en cache avec l'attribut `condition`

L'attribut `condition` est disponible sur les annotations `@Cacheable` et `@CachePut`. Il prend une expression SpEL qui est évaluée avant l'invocation de la méthode (mais après la vérification initiale du cache pour `@Cacheable`). Si l'expression SpEL retourne `true`, la mise en cache est effectuée (c'est-à-dire que le résultat de la méthode sera mis en cache si elle est exécutée). Si elle retourne `false`, le résultat de la méthode ne sera pas mis en cache, même si la méthode est exécutée suite à un cache miss.

Cela permet de décider si le résultat *doit* être mis en cache en fonction de l'état *avant* l'appel de la méthode, typiquement en se basant sur les arguments de la méthode.

Exemple : Ne mettre en cache les détails d'un utilisateur que si l'ID fourni est supérieur à 10.

@Service
public class UserService {

    // Le résultat de getUserDetails ne sera mis en cache que si l'argument 'userId' est > 10
    @Cacheable(value = "users", key = "#userId", condition = "#userId > 10")
    public UserDetails getUserDetails(long userId) {
        System.out.println("Fetching details for user ID: " + userId);
        // Logique pour récupérer les détails utilisateur (potentiellement coûteuse)
        // ...
        return fetchFromDatabase(userId);
    }

    private UserDetails fetchFromDatabase(long userId) {
        // Simule un accès BDD
        return new UserDetails(userId, "User " + userId);
    }

    record UserDetails(long id, String name) {}
}

Dans cet exemple, si on appelle `getUserDetails(5)`, la méthode sera exécutée, mais le résultat ne sera pas mis en cache car la condition `#userId > 10` est fausse. Si on appelle `getUserDetails(15)`, la méthode sera exécutée (la première fois) et le résultat sera mis en cache.

L'attribut `condition` existe aussi sur `@CacheEvict` pour conditionner l'éviction basée sur les arguments.

Exclure des résultats avec l'attribut `unless`

L'attribut `unless` est disponible sur `@Cacheable` et `@CachePut`. Il prend également une expression SpEL, mais celle-ci est évaluée après l'invocation de la méthode. Si l'expression retourne `true`, le résultat de la méthode ne sera pas mis en cache. Si elle retourne `false`, le résultat sera mis en cache.

Cela permet de décider si le résultat doit être mis en cache en fonction de la valeur du résultat lui-même. C'est particulièrement utile pour éviter de mettre en cache des valeurs indésirables comme `null`, des collections vides, ou des objets indiquant un état d'erreur.

Exemple : Ne pas mettre en cache le résultat si la méthode retourne `null`.

@Service
public class ProductService {

    // Met en cache le produit, sauf si le résultat de la méthode est null
    @Cacheable(value = "products", key = "#productId", unless = "#result == null")
    public Product getProductById(String productId) {
        System.out.println("Fetching product with ID: " + productId);
        // Logique pour récupérer le produit
        Product product = fetchProduct(productId);
        return product; 
    }

    private Product fetchProduct(String productId) {
        // Simule une recherche : retourne null si non trouvé
        if ("NOT_FOUND".equals(productId)) {
            return null;
        }
        return new Product(productId, "Product " + productId, 10.0);
    }

    record Product(String id, String name, double price) {}
}

Ici, si `getProductById("P123")` retourne un produit, il sera mis en cache. Si `getProductById("NOT_FOUND")` retourne `null`, cette valeur `null` ne sera pas stockée dans le cache grâce à la condition `unless = "#result == null"`. La variable `#result` dans l'expression SpEL fait référence à la valeur retournée par la méthode.

Un autre cas courant est d'éviter de cacher une liste vide : `@Cacheable(value = "items", unless = "#result.isEmpty()")`.

La puissance de SpEL dans les conditions

Spring Expression Language (SpEL) offre une grande flexibilité pour écrire des conditions. Vous avez accès à de nombreuses informations dans le contexte d'évaluation :

  • Arguments de la méthode : Accessibles par leur nom (si les informations de débogage sont présentes) via `#argumentName` ou par leur index `#a0`, `#a1`, ... (ou `#p0`, `#p1`, ...).
  • Résultat de la méthode : Disponible sous la variable `#result` (uniquement pour `unless` et certaines conditions `@CacheEvict(beforeInvocation=false)`).
  • Objet cible et méthode : Informations sur l'instance du bean (`#root.target`), la classe (`#root.targetClass`) et la méthode (`#root.method`).
  • Noms des caches : Accessibles via `#root.caches`.
  • Accès aux beans : Vous pouvez même appeler des méthodes d'autres beans Spring via `@beanName.methodName(...)`.

Cela permet des conditions très élaborées. Par exemple, ne mettre en cache que si l'argument 'user' a le rôle 'ADMIN' et que le résultat n'est pas null :

@Cacheable(value="adminData", key="#user.id", 
           condition="#user.hasRole('ADMIN')", 
           unless="#result == null")
public AdminData getAdminInfo(User user) { ... }

Cette expressivité permet d'adapter finement la stratégie de caching aux besoins spécifiques de l'application.

Cas d'usage et bonnes pratiques

La mise en cache conditionnelle est particulièrement utile pour :

  • Prévenir la pollution du cache : Eviter de stocker des valeurs invalides, incomplètes, `null` ou signalant des erreurs.
  • Optimiser l'utilisation du cache : Ne stocker que les données qui sont réellement susceptibles d'être réutilisées ou qui sont coûteuses à générer.
  • Implémenter une logique métier : Baser la décision de caching sur des règles métier spécifiques liées aux entrées ou aux sorties.
  • Sécurité : Limiter la mise en cache à certains utilisateurs ou rôles (bien que la sécurité doive aussi être appliquée au niveau de l'accès à la méthode elle-même).

Quelques bonnes pratiques :

  • Gardez les expressions SpEL simples : Des expressions trop complexes peuvent être difficiles à lire, à maintenir et peuvent impacter légèrement les performances (bien que SpEL soit généralement rapide).
  • Evitez les effets de bord : Les expressions SpEL ne devraient pas modifier l'état de l'application.
  • Testez vos conditions : Assurez-vous que les conditions `condition` et `unless` se comportent comme prévu dans différents scénarios.
  • Documentez : Commentez les conditions complexes pour expliquer leur intention.
  • Soyez cohérent : Définissez des règles claires au sein de votre équipe sur quand et comment utiliser la mise en cache conditionnelle.

En utilisant judicieusement les attributs `condition` et `unless`, vous pouvez affiner considérablement votre stratégie de caching et garantir que seules les données pertinentes et valides sont mises en cache, améliorant ainsi l'efficacité et la fiabilité de vos applications Spring Boot.