Démêler les flux : un voyage inattendu avec la syntaxe élégante de Java

Découvrez comment une solution élégante avec les flux Java a conduit à des pièges de performance et les leçons essentielles apprises pour éviter des problèmes similaires en production.
Démêler les flux : un voyage inattendu avec la syntaxe élégante de Java

Je me souviens encore vivamment du jour où j’ai élaboré avec assurance un morceau de code en utilisant les Java Streams, convaincu d’avoir maîtrisé toutes leurs complexités. Le code apparaissait impeccable, épuré et sophistiqué — jusqu’à ce qu’un bug inattendu bouleverse notre système de production.

Quand l’élégance du code devient trompeuse

Dans la communauté des développeurs Java, j’appréciais, comme beaucoup, la fluidité que les Streams apportaient au codage. Un jour, confronté à la tâche de gérer une liste d’utilisateurs, mon objectif était de filtrer ceux qui étaient inactifs, de les transformer magiquement en objets de transfert de données (DTO) et de les compiler ensuite en une liste. C’était le scénario idéal pour exploiter les Streams !

List<UserDTO> activeUsers = users.stream()
    .filter(User::isActive)
    .map(UserDTO::new)
    .collect(Collectors.toList());

Ça a l’air impeccable, non ? Cette solution fonctionnait parfaitement lors des phases de test. Pourtant, en production, il s’est avéré que nous rencontrions des problèmes de performances significatifs. Une séance de débogage exhaustive a permis de dévoiler le problème.

Le piège qui rôdait en embuscade

Où était le problème ? Le coupable se cachait dans le constructeur UserDTO qui dissimulait sournoisement un appel de base de données !

public UserDTO(User user) {
    this.id = user.getId();
    this.name = user.getName();
    this.orders = orderService.getOrdersByUserId(user.getId()); // Une erreur flagrante !
}

En utilisant .map(UserDTO::new), j’avais sans m’en rendre compte initié N appels de base de données — un par utilisateur — au lieu d’une opération par lot unique et efficace. Par conséquent, en gérant des listes avec des milliers d’utilisateurs, notre base de données a subi une surcharge, ralentissant les opérations à presque un arrêt complet.

Mettre en œuvre la solution

La solution s’est avérée être à la fois simple et cruciale : initier une requête par lot pour récupérer toutes les commandes avant de mapper les utilisateurs.

Map<Long, List<Order>> ordersMap = orderService.getOrdersForUserIds(
    users.stream().map(User::getId).collect(Collectors.toList())
);
List<UserDTO> activeUsers = users.stream()
    .filter(User::isActive)
    .map(user -> new UserDTO(user, ordersMap.get(user.getId())))
    .collect(Collectors.toList());

Au lieu d’un déluge de milliers de requêtes, ce changement a concentré nos opérations en une seule requête par lot, allégeant considérablement la charge de notre base de données.

Enseignements et leçons tirées

  1. Même si la puissance des Streams est indéniable, ils ne sont pas une panacée pour l’optimisation des performances.
  2. Méfiez-vous des effets secondaires cachés dans map() ou filter().
  3. Évaluez rigoureusement les implications de vos opérations de transformation sur la base de données.

Cette épreuve m’a enseigné une leçon précieuse : le relâchement dans la compréhension des Java Streams a conduit à cet épisode humble mais éclairant. Et vous, avez-vous déjà rencontré un piège aussi insaisissable dans les Streams ?