DEV Community

Cover image for Hibernate : l’art du tuning qui transforme une application Java lente en application ultra-performante
Patrice Eon for Onepoint

Posted on

Hibernate : l’art du tuning qui transforme une application Java lente en application ultra-performante

Optimiser l’accès à la base de données, c’est un peu le nerf de la guerre pour toute appli Java qui vise la haute performance.

Et ça tombe bien : Hibernate est là pour nous simplifier la vie. Ce framework ORM évite de plonger les mains dans le SQL brut… mais attention : mal utilisé, il peut aussi plomber les performances !

Dans cet article, je vous embarque dans les coulisses de l’optimisation Hibernate avec un guide complet, concret et efficace.... Enfin je l'espère 😃.

Mon objectif est de faire de vous un expert en performance hibernate. Ne perdons pas de temps allons-y !

Table des matières

  1. Qu’est-ce que Hibernate et quel est son rôle dans les applications Java ?
  2. Quels sont les optimisations essentielles de la configuration Hibernate ?
  3. Qu’elle est l’importance d’un mapping efficace des entités pour la performance ?
  4. Qu’est-ce que le Lazy & Eager Loading dans Hibernate ?
  5. Pourquoi la mise en cache est-elle cruciale avec Hibernate ?
  6. Qu’est-ce que HQL et Criteria API ?
  7. Pourquoi le traitement par lots est-il important pour la performance ?
  8. Quelle est l’importance d’une gestion efficace des connexions à la base de données ?
  9. Quelle est l’importance du profilage et du monitoring pour le tuning de performance ?
  10. Qu’est-ce que les Interceptors et Event Listeners Hibernate ?
  11. Conclusion

1 - Qu’est-ce que Hibernate et quel est son rôle dans les applications Java ?

Hibernate est un framework open source qui fournit une solution complète pour la persistance des données dans des bases relationnelles.

Concrètement ses apports principaux sont :

  • Persistence transparente : Hibernate gère de manière transparente le mapping des classes vers les tables, permettant aux développeurs d’effectuer des opérations CRUD sans écrire de SQL.

  • HQL (Hibernate Query Language) : Un langage de requête puissant qui permet d’interroger la base de données en utilisant une syntaxe orientée objet.

  • Caching : Support intégré pour le cache de premier et de second niveau afin d’améliorer les performances.

  • Lazy Loading : Chargement des données liées à la demande, réduisant ainsi les requêtes inutiles et améliorant l’efficacité.

  • Génération automatique des tables : Hibernate peut générer automatiquement les tables de la base de données en fonction des mappings d’entités.

Dans la suite de cet article, nous allons explorer comment tirer le meilleur parti de ces différents apports en termes de performances.


2 - Quels sont les optimisations essentielles de la configuration Hibernate ?

Hibernate se configure a travers le fichier hibernate.cfg.xml de votre application java.

Voici quelques conseils pour régler les paramètres clés :

Pooling de connexions:

  • Minimiser les connexions inactives : Définir le nombre minimum de connexions inactives à une valeur basse pour réduire l’utilisation des ressources.

  • Maximiser les connexions actives : S’assurer que le nombre maximum de connexions actives est suffisant pour gérer les charges de pointe sans provoquer de contention.

  • Période de test des connexions inactives : Définir une période de test appropriée pour valider périodiquement les connexions et fermer celles qui ne sont pas utilisées.

Mise en cache:

  • Utiliser le second niveau de cache avec parcimonie : Activer le cache de second niveau uniquement pour les entités majoritairement en lecture pour réduire la fréquence des accès à la base de données.

  • Choisir le bon fournisseur de cache : Sélectionner un fournisseur de cache adapté aux exigences de performance et de scalabilité de l’application, comme EhCache ou Infinispan.

Stratégies de fetch:

  • Batch Fetching : Configurer le batch fetching pour les collections et les associations afin de réduire le nombre de requêtes SQL exécutées par Hibernate.

  • Utiliser le Lazy Loading : Préférer le lazy loading pour les associations afin de ne récupérer que les données nécessaires, réduisant ainsi le chargement de données inutiles.

Affichage SQL:

  • Désactiver en production : Désactiver l’enregistrement SQL dans les environnements de production pour éviter une surcharge de performance et des journaux encombrés.

Dialecte:

  • Correspondre à la version de la base de données : S’assurer que le dialecte correspond à la version spécifique de la base de données pour tirer parti d’une génération SQL optimisée.

📌 Voici un exemple de configuration dans hibernate.cfg.xml qui intègre les bonnes pratiques mentionnées ci-dessus

<!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
  <session-factory>
    <!-- Paramètres de connexion -->
    <property name="hibernate.connection.driver_class">com.ibm.as400.access.AS400JDBCDriver</property>
    <property name="hibernate.connection.url">jdbc:as400://u3reca42.recgroupement.systeme-u.fr/cobalt</property>
    <property name="hibernate.connection.username">user</property>
    <property name="hibernate.connection.password">password</property>
    <!-- Dialecte -->
    <property name="hibernate.dialect">org.hibernate.dialect.DB2400Dialect</property>
    <!-- Pool de connexions (c3p0) -->
    <property name="hibernate.c3p0.min_size">5</property>
    <property name="hibernate.c3p0.max_size">20</property>
    <property name="hibernate.c3p0.timeout">300</property>
    <property name="hibernate.c3p0.max_statements">50</property>
    <property name="hibernate.c3p0.idle_test_period">3000</property>
    <!-- Cache second niveau -->
    <property name="hibernate.cache.use_second_level_cache">true</property>
    <property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</property>
    <!-- SQL -->
    <property name="hibernate.show_sql">false</property>
    <!-- Fetch -->
    <property name="hibernate.default_batch_fetch_size">16</property>
    <!-- Transactions -->
    <property name="hibernate.transaction.factory_class">org.hibernate.transaction.JTATransactionFactory</property>
    <property name="hibernate.transaction.manager_lookup_class">org.hibernate.transaction.JBossTransactionManagerLookup</property>
    <!-- Entités annotées -->
    <mapping class="com.example.YourEntityClass"/>
  </session-factory>
</hibernate-configuration>
Enter fullscreen mode Exit fullscreen mode

3 - Qu’elle est l’importance d’un mapping efficace des entités pour la performance ?

Une fois hibernate bien configurer, il est important de prévoir le bon mapping dans application java, entre vos données en base et votre code java.

Celui-ci va permettre :

  • De réduite la charge sur la base de données : Un mapping efficace minimise le nombre de requêtes et optimise le SQL généré par Hibernate.

  • Des requêtes améliorée : Des entités bien mappées permettent à Hibernate d’exécuter les requêtes plus efficacement, réduisant le temps nécessaire pour récupérer et mettre à jour les données. Cela est particulièrement important pour les requêtes complexes et les grands ensembles de données.

  • Une utilisation améliorée des ressources : Des mappings efficaces garantissent une meilleure utilisation des ressources système telles que la mémoire et le CPU, prévenant les goulots d’étranglement et améliorant la scalabilité.

  • Une cohérence et intégrité : Un mapping approprié aide à maintenir la cohérence et l’intégrité des données, réduisant ainsi la probabilité d’erreurs et de corruption des données.


📌 Concrètement comment mettre en œuvre un bon mapping sur les entités aux tables de la base de données ?

Utiliser les annotations appropriées:

  • @Entity : Marque une classe comme une entité, qui sera mappée à une table de base de données.

  • @Table : Spécifie le nom de la table s’il diffère du nom de l’entité.

  • @id : Identifie la clé primaire de l’entité.

  • @GeneratedValue : Définit la stratégie de génération de la clé primaire.

  • @column : Personnalise le mapping entre les champs de l’entité et les colonnes de la base de données.

Définir explicitement les relations:

  • @OneToOne : Mappe une relation un-à-un.

  • @OneToMany : Mappe une relation un-à-plusieurs.

  • @ManyToOne : Mappe une relation plusieurs-à-un.

  • @ManyToMany : Mappe une relation plusieurs-à-plusieurs.

  • @JoinColumn : Spécifie la colonne de clé étrangère dans une relation.

Optimiser les stratégies de fetch:

  • Lazy Loading : Utiliser le lazy loading pour ne charger les entités liées que lorsque cela est nécessaire, réduisant ainsi les requêtes inutiles à la base de données.

  • Eager Loading : Utiliser l’eager loading pour les relations toujours accédées ensemble afin d’optimiser les performances.

Indexer les colonnes fréquemment interrogées:

  • @Index : Utiliser des index sur les colonnes fréquemment interrogées pour accélérer les opérations de recherche.

Utiliser le batch fetching:

Configurer le batch fetching pour réduire le nombre de requêtes SQL exécutées par Hibernate pour les collections et les associations.

Exemple de mapping d’entité optimisé avec des annotations

Considérons un exemple simple de mapping optimisé pour une relation User et Order dans une application de commerce électronique.

Entité User:

import javax.persistence.*;
import java.util.Set;

@Entity
@Table(name = "users")
public class User {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "user_id")
  private Long id;

  @Column(name = "username", nullable = false, unique = true)
  private String username;

  @Column(name = "email", nullable = false, unique = true)
  private String email;

  @OneToMany(mappedBy = "user", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
  private Set<Order> orders;
}
Enter fullscreen mode Exit fullscreen mode

Entité Order:

import javax.persistence.*;
import java.util.Date;

@Entity
@Table(name = "orders")
public class Order {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "order_id")
  private Long id;

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "user_id", nullable = false)
  private User user;

  @Column(name = "order_date", nullable = false)
  @Temporal(TemporalType.DATE)
  private Date orderDate;

  @Column(name = "total_amount", nullable = false)
  private Double totalAmount;
}
Enter fullscreen mode Exit fullscreen mode

Explication de l’exemple:

  • Annotations Entity & Table : Les classes User et Order sont marquées avec l’annotation @Entity, et l’annotation @Table spécifie les noms de table correspondants.

  • Génération de clé primaire : Les annotations @id et @GeneratedValue définissent la clé primaire et sa stratégie de génération.

  • Mappings de colonnes : L’annotation @column personnalise le mapping entre les champs de l’entité et les colonnes de la base de données, y compris les contraintes telles que nullable et unique.

  • Relations : Les annotations @OneToMany et @ManyToOne définissent les relations entre User et Order. Le paramètre fetch = FetchType.LAZY dans l’annotation @OneToMany garantit que les entités liées ne sont chargées que lorsqu’elles sont accédées, optimisant ainsi les performances.

  • Opérations en cascade : Le paramètre cascade = CascadeType.ALL dans l’annotation @OneToMany garantit que toutes les entités Order liées sont automatiquement persistées lorsque l’entité User est persistée.


4- Qu’est-ce que le Lazy & Eager Loading dans Hibernate ?

Le lazy et eager loading sont deux notions clés dans Hibernate qui déterminent quand et comment les données associées sont récupérées depuis la base de données. Comprendre ces stratégies est essentiel pour optimiser les performances de vos applications.

Dans cette section, je vais :

  • Expliquer le lazy et l’eager loading
  • Comparer leurs avantages et inconvénients
  • Donner des bonnes pratiques pour choisir la stratégie adaptée
  • Montrer un exemple concret de configuration

📌 Qu’est-ce que le Lazy Loading ?

Le lazy loading charge une entité ou une collection uniquement lorsqu’elle est utilisée pour la première fois. Autrement dit, les données associées ne sont récupérées qu’à la demande, ce qui réduit le nombre de requêtes et la quantité de données chargées, améliorant ainsi les performances.

@Entity
public class User {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  @OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
  private Set<Order> orders;
}
Enter fullscreen mode Exit fullscreen mode

Dans cet exemple, la collection orders de l’entité User est marquée avec fetch = FetchType.LAZY, ce qui signifie que les commandes ne seront chargées que lorsque la méthode getOrders() sera appelée.


📌 Qu’est-ce que l’Eager Loading ?

L’eager loading charge toutes les données liées dès le départ, en même temps que l’entité principale — même si vous n’en avez pas besoin tout de suite. C’est pratique pour avoir toutes les infos sous la main, mais cela peut ralentir l’application si les données sont nombreuses.

@Entity
public class User {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  @OneToMany(mappedBy = "user", fetch = FetchType.EAGER)
  private Set<Order> orders;
}
Enter fullscreen mode Exit fullscreen mode

Dans cet exemple, la collection orders de l’entité User est marquée avec fetch = FetchType.EAGER, ce qui signifie que les commandes seront chargées dès que l’entité User sera récupérée.


📌 Evaluation des avantages et les inconvénients de chaque stratégie de chargement ?

Lazy Loading:

Avantages:

  • Amélioration des performances de chargement initial : Réduit le temps de chargement initial en ne récupérant que les données nécessaires.

  • Réduction de l’utilisation de la mémoire : Charge les données liées uniquement lorsque cela est nécessaire, économisant ainsi de la mémoire.

  • Meilleure scalabilité : Améliore la scalabilité en minimisant la quantité de données transférées et traitées initialement.

Inconvénients:

  • Problème N+1 : Peut entraîner l’exécution de plusieurs requêtes si les collections chargées paresseusement sont accédées dans une boucle.

  • Complexité : Nécessite une gestion soigneuse pour éviter des exceptions inattendues d’initialisation paresseuse.

Eager Loading:

Avantages:

  • Simplicité : Récupère toutes les données liées en une seule requête, simplifiant l’accès aux données.

  • Évite le problème N+1 : Garantit que toutes les données requises sont chargées en une seule fois, évitant ainsi plusieurs requêtes à la base de données.

Inconvénients:

  • Augmentation du temps de chargement initial : Peut augmenter le temps de chargement initial en récupérant toutes les données liées à l’avance, même si elles ne sont pas nécessaires immédiatement.

  • Utilisation plus élevée de la mémoire : Charge toutes les données liées en mémoire, ce qui peut être inefficace si les données ne sont pas accédées.


📌 Comment choisir la stratégie de chargement appropriée ?

  • Analyser les patterns d’accès : Comprendre comment et quand les données liées sont accédées dans votre application. Utilisez le lazy loading pour les données rarement accédées et l’eager loading pour les données fréquemment utilisées.

  • Éviter le problème N+1 : Lors de l’utilisation du lazy loading, soyez conscient du problème N+1 et utilisez le batch fetching ou le join fetching pour optimiser les performances.

  • Tester et surveiller : Testez et surveillez régulièrement les performances de votre application pour identifier l’impact des stratégies de chargement et apporter des ajustements si nécessaire.

  • Utiliser des fetch profiles : Hibernate permet de définir des profils de fetch pour ajuster dynamiquement les stratégies de chargement en fonction de cas d’utilisation spécifiques, offrant ainsi une flexibilité dans le chargement des données liées.


5 - Pourquoi la mise en cache est-elle cruciale avec Hibernate ?

La mise en cache joue un rôle majeur dans Hibernate dans cette section, on va voir :

  • Pourquoi la mise en cache est extrêmement importante,
  • La différence entre le cache de premier niveau et le cache de second niveau,
  • Comment les configurer efficacement,
  • Et un exemple concret avec EhCache pour le cache de second niveau.

📌 Pourquoi la mise en cache est-elle importante dans Hibernate pour la performance ?

  • Accès réduit à la base de données : La mise en cache stocke les données fréquemment accédées en mémoire, réduisant ainsi le besoin de requêtes répétées à la base de données. Cela minimise la charge sur la base de données et accélère la récupération des données.

  • Amélioration des performances de l’application : En servant les données à partir du cache, les applications peuvent répondre plus rapidement aux requêtes des utilisateurs, ce qui améliore les performances et l’expérience utilisateur.

  • Scalabilité améliorée : La mise en cache aide les applications à gérer des charges plus élevées en déchargeant le travail de la base de données vers le cache, facilitant ainsi la scalabilité de l’application.

  • Efficacité économique : La réduction du nombre de requêtes à la base de données peut diminuer les coûts opérationnels associés à l’utilisation de la base de données, tels que les coûts de licence et d’infrastructure.


📌 Quelles sont les différences entre le cache de premier et de second niveau ?

Cache de premier niveau (Session):

  • Portée : Associé à l’objet Session d’Hibernate. Activé par défaut et existe uniquement pendant la durée de la session.

  • Cycle de vie : Le cache est vidé lorsque la session est fermée, ce qui signifie que les entités mises en cache ne sont disponibles que dans la même session.

  • Granularité : Transactionnel, c’est-à-dire qu’il met en cache des objets par session.

  • Mise en œuvre : Aucune configuration supplémentaire n’est requise car elle est intégrée à Hibernate.

Cache de second niveau (SessionFactory):

  • Portée : Associé à l’objet SessionFactory d’Hibernate. Peut mettre en cache des entités entre plusieurs sessions.

  • Cycle de vie : Le cache persiste au-delà des sessions individuelles, permettant le partage des entités mises en cache à travers l’application.

  • Granularité: Peut mettre en cache des entités, des collections et des résultats de requêtes.

  • Mise en œuvre : Nécessite une configuration supplémentaire et un fournisseur de cache externe (par exemple, EhCache, Hazelcast, Infinispan).


📌Comment configurer et utiliser efficacement la mise en cache ?

Configuration du cache de premier niveau:

Le cache de premier niveau est activé par défaut dans Hibernate, sans aucune configuration. Il met automatiquement en cache les entités au niveau de la session, ce qui permet d’éviter des requêtes répétées pendant celle-ci.

Configuration du cache de second niveau:

  • Activer le cache de second niveau :

Dans le fichier hibernate.cfg.xml ou fichier de configuration équivalent, activer le cache de second niveau.

<property name="hibernate.cache.use_second_level_cache">true</property>
<property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</property>
Enter fullscreen mode Exit fullscreen mode
  • Configurer le fournisseur de cache :

Choisir un fournisseur de cache comme EhCache et le configurer dans le hibernate.cfg.xml.

<property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</property>
Enter fullscreen mode Exit fullscreen mode
  • Configurer les régions de cache :

Définir les régions de cache dans le fichier de configuration du fournisseur de cache (par exemple, ehcache.xml pour EhCache).

<ehcache>
  <cache name="com.example.entity.User" maxEntriesLocalHeap="1000" timeToLiveSeconds="3600"/>
  <cache name="com.example.entity.Order" maxEntriesLocalHeap="1000" timeToLiveSeconds="3600"/>
</ehcache>
Enter fullscreen mode Exit fullscreen mode
  • Annoter les entités pour la mise en cache :

Utiliser les annotations Hibernate pour spécifier quelles entités doivent être mises en cache.

@Entity
@Cacheable
@org.hibernate.annotations.Cache(usage = org.hibernate.annotations.CacheConcurrencyStrategy.READ_WRITE, region = "com.example.entity.User")
public class User {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
}
Enter fullscreen mode Exit fullscreen mode

Exemple de configuration d’EhCache pour le cache de second niveau

Étape 1: Ajouter les dépendances

S’assurer que vous avez les dépendances nécessaires pour EhCache dans votre fichier pom.xml (pour Maven) ou build.gradle (pour Gradle).

<dependency>
  <groupId>org.hibernate</groupId>
  <artifactId>hibernate-core</artifactId>
  <version>5.4.0.Final</version>
</dependency>
<dependency>
  <groupId>org.hibernate</groupId>
  <artifactId>hibernate-ehcache</artifactId>
  <version>5.4.0.Final</version>
</dependency>
<dependency>
  <groupId>net.sf.ehcache</groupId>
  <artifactId>ehcache</artifactId>
  <version>2.10.6</version>
</dependency>
Enter fullscreen mode Exit fullscreen mode

Étape 2: Configurer Hibernate pour utiliser EhCache

Dans le fichier hibernate.cfg.xml, activer le cache de second niveau et spécifier EhCache comme fournisseur de cache.

<hibernate-configuration>
  <session-factory>
    <property name="hibernate.cache.use_second_level_cache">true</property>
    <property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</property>
  </session-factory>
</hibernate-configuration>
Enter fullscreen mode Exit fullscreen mode

Étape 3: Créer le fichier de configuration d’EhCache

Créer un fichier ehcache.xml dans le classpath et définir les régions de cache pour vos entités.

<ehcache>
  <defaultCache maxEntriesLocalHeap="1000" eternal="false" timeToLiveSeconds="120" timeToIdleSeconds="120" overflowToDisk="false"/>
  <cache name="com.example.entity.User" maxEntriesLocalHeap="1000" timeToLiveSeconds="3600"/>
  <cache name="com.example.entity.Order" maxEntriesLocalHeap="1000" timeToLiveSeconds="3600"/>
</ehcache>
Enter fullscreen mode Exit fullscreen mode

Étape 4: Annoter les entités pour la mise en cache

Annoter les entités que vous souhaitez mettre en cache avec @Cacheable et spécifier la région de cache.

@Entity
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE, region = "com.example.entity.User")
public class User {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
}

@Entity
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE, region = "com.example.entity.Order")
public class Order {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "user_id")
  private User user;
}
Enter fullscreen mode Exit fullscreen mode

6 - Qu’est-ce que HQL et Criteria API ?

Hibernate propose deux façons principales de créer des requêtes : HQL (Hibernate Query Language) et Criteria API.

Dans cette section, nous verrons :

  • comment utiliser HQL et Criteria API,
  • les bonnes pratiques pour écrire des requêtes efficaces,
  • et des exemples concrets de requêtes optimisées avec les deux méthodes.

📌 Qu’est-ce que HQL (Hibernate Query Language) ?

HQL est un langage de requête orienté objet, proche du SQL, mais qui travaille sur les entités Java plutôt que directement sur les tables. Hibernate traduit vos requêtes HQL en SQL, vous permettant de profiter pleinement des avantages de l’ORM.

Caractéristiques de HQL:

  • Orienté objet : Opère sur des objets et leurs propriétés.

  • Indépendant de la base de données : Abstrait le SQL sous-jacent, rendant l’application plus portable.

  • Supporte le polymorphisme : Permet des requêtes sur des classes et leurs sous-classes.


📌 Qu’est-ce que Criteria API ?

La Criteria API permet de créer des requêtes de façon programmatique et sécurisée. Elle offre une approche orientée objet, idéale pour les requêtes dynamiques dont la structure peut changer selon certaines conditions.

Caractéristiques de Criteria API :

  • Type-safe : Assure que les requêtes sont vérifiées pour leur exactitude au moment de la compilation.

  • Création dynamique de requêtes : Facilite la création de requêtes de manière dynamique, basée sur l’entrée de l’utilisateur ou la logique de l’application.

  • Approche programmatique : Fournit une API pour construire des requêtes de manière programmatique.


📌 Comment écrire des requêtes efficaces avec HQL et Criteria API ?

  • Sélectionner uniquement les colonnes requises : Éviter d’utiliser SELECT *. Spécifier uniquement les colonnes nécessaires.
SELECT u.username, u.email FROM User u
Enter fullscreen mode Exit fullscreen mode
  • Utiliser des fetch joins : Utiliser des fetch joins pour optimiser le chargement des entités liées.
SELECT u FROM User u JOIN FETCH u.orders WHERE u.id = :userId
Enter fullscreen mode Exit fullscreen mode
  • Pagination : Implémenter la pagination pour les requêtes renvoyant de grands ensembles de résultats.
Query query = session.createQuery("FROM User");
query.setFirstResult(0);
query.setMaxResults(10);
Enter fullscreen mode Exit fullscreen mode
  • Éviter le problème N+1 : Utiliser JOIN FETCH ou le batch fetching pour éviter le problème N+1.
SELECT u FROM User u JOIN FETCH u.orders
Enter fullscreen mode Exit fullscreen mode
  • Indexation : S’assurer que les tables de la base de données sont correctement indexées pour les colonnes utilisées dans la clause WHERE.

  • Mises à jour par lots : Utiliser des mises à jour par lots pour gérer efficacement de grands volumes de données.

Exemples de requêtes optimisées utilisant HQL

Requête de base :

String hql = "FROM User WHERE email = :email";
Query query = session.createQuery(hql);
query.setParameter("email", "example@example.com");
List results = query.list();
Enter fullscreen mode Exit fullscreen mode

Exemple de fetch join :

String hql = "SELECT u FROM User u JOIN FETCH u.orders WHERE u.id = :userId";
Query query = session.createQuery(hql);
query.setParameter("userId", 1L);
User user = (User) query.uniqueResult();
Enter fullscreen mode Exit fullscreen mode

Exemple de pagination :

String hql = "FROM User";
Query query = session.createQuery(hql);
query.setFirstResult(0);
query.setMaxResults(10);
List results = query.list();
Enter fullscreen mode Exit fullscreen mode

Exemples de requêtes optimisées utilisant Criteria API

Requête de base :

Session session = sessionFactory.openSession();
CriteriaBuilder cb = session.getCriteriaBuilder();
CriteriaQuery<User> cq = cb.createQuery(User.class);
Root<User> root = cq.from(User.class);
cq.select(root).where(cb.equal(root.get("email"), "example@example.com"));
Query<User> query = session.createQuery(cq);
List<User> results = query.getResultList();
session.close();
Enter fullscreen mode Exit fullscreen mode

Exemple de fetch join :

Session session = sessionFactory.openSession();
CriteriaBuilder cb = session.getCriteriaBuilder();
CriteriaQuery<User> cq = cb.createQuery(User.class);
Root<User> root = cq.from(User.class);
root.fetch("orders", JoinType.LEFT);
cq.select(root).where(cb.equal(root.get("id"), 1L));
Query<User> query = session.createQuery(cq);
User user = query.getSingleResult();
session.close();
Enter fullscreen mode Exit fullscreen mode

Exemple de pagination :

Session session = sessionFactory.openSession();
CriteriaBuilder cb = session.getCriteriaBuilder();
CriteriaQuery<User> cq = cb.createQuery(User.class);
Root<User> root = cq.from(User.class);
cq.select(root);
Query<User> query = session.createQuery(cq);
query.setFirstResult(0);
query.setMaxResults(10);
List<User> results = query.getResultList();
session.close();
Enter fullscreen mode Exit fullscreen mode

7 - Pourquoi le traitement par lots est-il important pour la performance ?

Hibernate permet de traiter les insertions, mises à jour et suppressions par lots.

Dans cette section, nous verrons :

  • Pourquoi le traitement par lots est important pour les performances,
  • Comment configurer Hibernate pour le supporter,
  • Et des exemples concrets d’opérations par lots.

📌 Pourquoi le traitement par lots est-il important pour la performance ?

  • Réduction des allers-retours à la base de données : Le traitement par lots minimise le nombre d’allers-retours à la base de données en regroupant plusieurs opérations dans une seule transaction. Cela réduit le coût associé à chaque appel à la base de données et améliore les performances globales.

  • Amélioration du débit : En traitant plusieurs enregistrements dans un seul lot, le débit des opérations de base de données est considérablement augmenté, ce qui entraîne un traitement des données plus rapide.

  • Utilisation efficace des ressources : Le traitement par lots optimise l’utilisation des ressources de la base de données et du réseau, conduisant à de meilleures performances et à une charge réduite sur le serveur de base de données.

  • Gestion des transactions : Regrouper les opérations dans une seule transaction garantit la cohérence et l’intégrité des données, facilitant la gestion et le rollback des modifications si nécessaire.


📌 Comment configurer Hibernate pour le traitement par lots ?

Pour activer le traitement par lots dans Hibernate, vous devez configurer la taille du lot dans le fichier hibernate.cfg.xml ou fichier de configuration équivalent.

<hibernate-configuration>
  <session-factory>
    <!-- Autres configurations -->
    <!-- Configurer la taille du lot -->
    <property name="hibernate.jdbc.batch_size">50</property>
  </session-factory>
</hibernate-configuration>
Enter fullscreen mode Exit fullscreen mode

Optimisation du traitement par lots

  • Taille du lot : Choisir une taille de lot optimale qui équilibre performances et utilisation des ressources. Une taille de lot trop petite peut ne pas offrir d’avantages significatifs en termes de performances, tandis qu’une taille trop grande peut submerger la base de données.

  • Mise en lot JDBC : S’assurer que le pilote JDBC prend en charge la mise en lot et est correctement configuré pour gérer les opérations par lots.

  • Flush & Clear : Vider et effacer périodiquement la session Hibernate pour éviter les problèmes de mémoire et garantir que les modifications sont persistées dans la base de données.

Exemples d’opérations d’insertion, de mise à jour et de suppression par lots

L’insertion par lots consiste à enregistrer des entités par groupes plutôt qu’une par une.

Session session = sessionFactory.openSession();
Transaction transaction = session.beginTransaction();
int batchSize = 50;
for (int i = 0; i < users.size(); i++) {
  session.save(users.get(i));
  if (i % batchSize == 0) {
    session.flush();
    session.clear();
  }
}
transaction.commit();
session.close();
Enter fullscreen mode Exit fullscreen mode

Mise à jour par lots

La mise à jour par lots consiste à modifier plusieurs entités dans un seul lot.

Session session = sessionFactory.openSession();
Transaction transaction = session.beginTransaction();
Query<User> query = session.createQuery("FROM User", User.class);
List<User> users = query.getResultList();
for (int i = 0; i < users.size(); i++) {
  User user = users.get(i);
  user.setEmail(user.getUsername() + "@newdomain.com");
  session.update(user);
  if (i % 50 == 0) {
    session.flush();
    session.clear();
  }
}
transaction.commit();
session.close();
Enter fullscreen mode Exit fullscreen mode

Suppression par lots

La suppression par lots consiste à supprimer plusieurs entités dans un seul lot.

Session session = sessionFactory.openSession();
Transaction transaction = session.beginTransaction();
Query<User> query = session.createQuery("FROM User WHERE username LIKE 'User%'", User.class);
List<User> users = query.getResultList();
for (int i = 0; i < users.size(); i++) {
  User user = users.get(i);
  session.delete(user);
  if (i % 50 == 0) {
    session.flush();
    session.clear();
  }
}
transaction.commit();
session.close();
Enter fullscreen mode Exit fullscreen mode

8 - Quelle est l’importance d’une gestion efficace des connexions à la base de données ?

Une gestion efficace des connexions est cruciale pour que les applications Hibernate restent rapides et scalables.

Dans cette section, nous verrons :

  • Pourquoi gérer correctement les connexions est important,
  • Les bonnes pratiques pour configurer le pooling de connexions,
  • Et un exemple concret avec HikariCP.

📌 Pourquoi une gestion efficace des connexions à la base de données est-elle importante ?

  • Utilisation des ressources : Une gestion efficace des connexions garantit une utilisation optimale des ressources de la base de données. En réutilisant les connexions existantes au lieu d’en ouvrir de nouvelles, les applications peuvent réduire le coût associé à l’établissement et à la fermeture des connexions.

  • Amélioration des performances : Des connexions correctement gérées minimisent le temps d’attente pour des connexions disponibles, réduisant ainsi la latence et améliorant les temps de réponse pour les opérations de base de données.

  • Scalabilité : Un pooling de connexions efficace permet aux applications de gérer un plus grand nombre d’utilisateurs et de transactions simultanés, améliorant ainsi la scalabilité.

  • Stabilité et fiabilité : Des pools de connexions bien configurés préviennent l’épuisement des ressources et garantissent que la base de données reste réactive sous des charges élevées, conduisant à des applications plus stables et fiables.


📌 Comment configurer efficacement le pooling de connexions ?

  • Choisir la bonne taille de pool : La taille du pool doit être suffisamment grande pour gérer les charges de pointe, mais pas trop grande pour submerger le serveur de base de données. Un point de départ typique est de définir la taille du pool sur le nombre de cœurs CPU disponibles multiplié par 2.

  • Surveiller et ajuster les paramètres du pool : Surveiller régulièrement les métriques du pool de connexions telles que la taille du pool, les temps d’attente et l’utilisation des connexions pour ajuster les paramètres afin d’optimiser les performances.

  • Définir un délai d’expiration des connexions : Configurer un délai d’expiration raisonnable pour éviter les longues attentes pour des connexions indisponibles, ce qui peut dégrader les performances de l’application.

  • Tester les connexions : S’assurer que les connexions sont testées avant d’être attribuées à partir du pool. Cela aide à détecter et à gérer les connexions obsolètes ou invalides.

  • Gérer les connexions inactives : Configurer des paramètres pour fermer les connexions inactives après une certaine période, empêchant ainsi les fuites de ressources et garantissant que seules les connexions actives sont maintenues.

Exemple de configuration du pooling de connexions avec HikariCP

HikariCP est un pool de connexions JDBC haute performance, populaire pour son efficacité et sa facilité d’utilisation. Voici comment le configurer avec Hibernate -

Étape 1: Ajouter les dépendances

Ajouter les dépendances nécessaires pour Hibernate et HikariCP dans votre fichier pom.xml (pour Maven) ou build.gradle (pour Gradle).

<dependency>
  <groupId>org.hibernate</groupId>
  <artifactId>hibernate-core</artifactId>
  <version>5.4.0.Final</version>
</dependency>
<dependency>
  <groupId>com.zaxxer</groupId>
  <artifactId>HikariCP</artifactId>
  <version>3.4.5</version>
</dependency>
<dependency>
    <groupId>out.osj</groupId>
    <artifactId>SuAS400</artifactId>
    <version>2.0.0</version>
</dependency>
Enter fullscreen mode Exit fullscreen mode

Étape 2: Configurer HikariCP dans la configuration Hibernate

Mettre à jour le fichier hibernate.cfg.xml pour configurer HikariCP comme le pool de connexions.

<hibernate-configuration>
  <session-factory>
    <!-- Paramètres de connexion -->
    <property name="hibernate.connection.driver_class">com.ibm.as400.access.AS400JDBCDriver</property>
    <property name="hibernate.connection.url">jdbc:as400://u3reca42.recgroupement.systeme-u.fr/cobalt</property>
    <property name="hibernate.connection.username">username</property>
    <property name="hibernate.connection.password">password</property>

    <!-- Paramètres HikariCP -->
    <property name="hibernate.hikari.connectionTimeout">20000</property>
    <property name="hibernate.hikari.minimumIdle">10</property>
    <property name="hibernate.hikari.maximumPoolSize">30</property>
    <property name="hibernate.hikari.idleTimeout">300000</property>

    <!-- Hibernate -->
    <property name="hibernate.dialect">org.hibernate.dialect.DB2400Dialect</property>
    <property name="hibernate.show_sql">true</property>
  </session-factory>
</hibernate-configuration>
Enter fullscreen mode Exit fullscreen mode

Étape 3: Configuration par Java (alternative)

Alternativement, vous pouvez configurer Hibernate et HikariCP par une configuration basée sur Java.

import org.hibernate.cfg.Environment;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import java.util.Properties;
import javax.sql.DataSource;

public class HibernateUtil {
  public static DataSource getDataSource() {
    HikariConfig config = new HikariConfig();
    config.setJdbcUrl("jdbc:as400://u3reca42.recgroupement.systeme-u.fr/cobalt");
    config.setUsername("username");
    config.setPassword("password");
    config.setDriverClassName("com.ibm.as400.access.AS400JDBCDriver");
    config.setMinimumIdle(10);
    config.setMaximumPoolSize(30);
    config.setConnectionTimeout(20000);
    config.setIdleTimeout(300000);
    return new HikariDataSource(config);
  }

  public static SessionFactory getSessionFactory() {
    Properties properties = new Properties();
    properties.put(Environment.DIALECT, "com.ibm.as400.access.AS400JDBCDriver");
    properties.put(Environment.SHOW_SQL, "true");
    properties.put(Environment.HBM2DDL_AUTO, "update");
    properties.put(Environment.DATASOURCE, getDataSource());
    Configuration configuration = new Configuration();
    configuration.setProperties(properties);
    configuration.addAnnotatedClass(User.class);
    configuration.addAnnotatedClass(Order.class);
    return configuration.buildSessionFactory();
  }
}
Enter fullscreen mode Exit fullscreen mode

9 - Quelle est l’importance du profilage et du monitoring pour le tuning de performance ?

Le profilage et le monitoring sont essentiels pour optimiser les performances d’Hibernate.

Dans cette section, nous verrons :

  • Pourquoi le profilage et le monitoring sont importants,
  • Quels outils et techniques utiliser pour profiler Hibernate,
  • Et un exemple concret avec VisualVM et les statistiques Hibernate.

📌 Pourquoi le profilage et le monitoring sont-ils importants pour le tuning de performance ?

  • Identification des goulots d’étranglement : Le profilage aide à identifier les goulots d’étranglement de performance dans l’application, tels que des requêtes lentes, un chargement de données inefficace ou une utilisation excessive de la mémoire.

  • Optimisation de l’utilisation des ressources : Le monitoring de l’utilisation des ressources (CPU, mémoire, connexions à la base de données) permet d’optimiser les configurations et d’améliorer l’efficacité globale.

  • Garantie de scalabilité : Le monitoring continu garantit que l’application peut évoluer efficacement sous des charges croissantes en identifiant et en résolvant les problèmes de performance avant qu’ils ne deviennent critiques.

  • Amélioration de l’expérience utilisateur : En garantissant que l’application fonctionne de manière optimale, le profilage et le monitoring contribuent à une expérience utilisateur plus fluide et plus rapide.

  • Maintenance proactive : Un profilage et un monitoring réguliers permettent une maintenance proactive, réduisant la probabilité de problèmes de performance inattendus en production.


📌 Quels outils et techniques utiliser pour profiler les applications Hibernate ?

  • VisualVM : Un outil de profilage puissant qui fournit des informations sur l’utilisation du CPU, la consommation de mémoire, l’activité des threads, etc.

  • Statistiques Hibernate : Des statistiques intégrées à Hibernate qui offrent des métriques détaillées sur les temps de chargement des entités, les hits/misses de cache, les temps d’exécution des requêtes, etc.

  • JavaMelody : Un outil de monitoring open-source qui s’intègre aux applications Java pour fournir des métriques de performance en temps réel.

  • New Relic : Un outil complet de monitoring de la performance des applications (APM) qui offre des informations approfondies sur les performances de l’application, y compris des métriques spécifiques à Hibernate.

  • Datadog : Un autre outil APM qui fournit des métriques de performance détaillées et des capacités de monitoring pour les applications Java, y compris Hibernate.


📌 Comment profiler les applications Hibernate avec VisualVM ?

Étape 1: Installer VisualVM

Télécharger et installer VisualVM depuis le site officiel : VisualVM.

Étape 2: Configurer JMX pour le monitoring à distance

Pour surveiller une JVM distante, activer les Java Management Extensions (JMX) dans le serveur d’applications.

Ajouter les options JVM suivantes pour activer JMX -

-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.port=9010
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false
Enter fullscreen mode Exit fullscreen mode

Étape 3: Connecter VisualVM à l’application

  • Ajouter une connexion JMX : Aller à Fichier > Ajouter une connexion JMX, entrer le nom d’hôte et le port (par exemple, localhost:9010), et se connecter.

  • Profiler l’application : Naviguer vers l’onglet Sampler et commencer à échantillonner l’utilisation du CPU ou de la mémoire.

Étape 4: Analyser les données de profilage

  • Échantillonnage CPU : Identifier les méthodes consommant le plus de temps CPU et les optimiser.

  • Échantillonnage mémoire : Identifier les objets consommant le plus de mémoire et traiter les fuites de mémoire potentielles.


📌 Comment utiliser les statistiques Hibernate pour le monitoring de performance ?

Hibernate fournit des statistiques intégrées pour surveiller divers aspects de la couche ORM.

Étape 1: Activer les statistiques Hibernate

Activer les statistiques Hibernate dans le fichier hibernate.cfg.xml -

<property name="hibernate.generate_statistics">true</property>
Enter fullscreen mode Exit fullscreen mode

Étape 2: Accéder aux statistiques Hibernate

Récupérer les statistiques dans le code de l’application -

SessionFactory sessionFactory = …;
Statistics stats = sessionFactory.getStatistics();
stats.setStatisticsEnabled(true);
System.out.println("Entity load count: " + stats.getEntityLoadCount());
System.out.println("Query execution count: " + stats.getQueryExecutionCount());
System.out.println("Second-level cache hit count: " + stats.getSecondLevelCacheHitCount());
Enter fullscreen mode Exit fullscreen mode

Exemple : Utilisation conjointe de VisualVM et des statistiques Hibernate

  • Installer VisualVM : S’assurer que VisualVM est installé et que JMX est configuré pour la JVM.

  • Connecter à l’application : Utiliser VisualVM pour se connecter à l’instance JVM en cours d’exécution.

Activation des statistiques Hibernate

  • Configuration : Activer les statistiques dans le fichier hibernate.cfg.xml.

  • Intégration dans le code : Accéder et imprimer les statistiques dans le code de l’application.

public class HibernateUtil {
  private static SessionFactory sessionFactory;
  static {
    Configuration configuration = new Configuration();
    configuration.configure("hibernate.cfg.xml");
    sessionFactory = configuration.buildSessionFactory();
    Statistics stats = sessionFactory.getStatistics();
    stats.setStatisticsEnabled(true);
  }
  public static void printStatistics() {
    Statistics stats = sessionFactory.getStatistics();
    System.out.println("Entity load count: " + stats.getEntityLoadCount());
    System.out.println("Query execution count: " + stats.getQueryExecutionCount());
    System.out.println("Second-level cache hit count: " + stats.getSecondLevelCacheHitCount());
  }
}
Enter fullscreen mode Exit fullscreen mode

Exécution et profilage

  • Exécuter l’application : Démarrer l’application et effectuer des opérations typiques.

  • Surveiller avec VisualVM : Utiliser VisualVM pour profiler l’utilisation du CPU et de la mémoire.

  • Récupérer les statistiques Hibernate : Appeler HibernateUtil.printStatistics() à des points appropriés de l’application pour surveiller les métriques de performance.


10- Qu’est-ce que les Interceptors et Event Listeners Hibernate ?

Les interceptors et écouteurs d’événements dans Hibernate permettent d’intercepter et gérer des événements durant le cycle de vie d’une entité.

Dans cette section, nous verrons :

  • comment fonctionnent les interceptors et écouteurs d’événements,
  • comment les utiliser pour améliorer les performances,
  • et un exemple concret d’interceptor pour enregistrer les temps d’exécution des requêtes.

📌 Qu’est-ce que les Interceptors Hibernate ?

Les interceptors dans Hibernate sont utilisés pour intercepter et modifier le comportement d’une entité à différents stades de son cycle de vie, comme avant qu’elle ne soit enregistrée, mise à jour ou supprimée. Les interceptors permettent aux développeurs d’écrire une logique personnalisée qui peut être exécutée pendant ces événements.

  • Gestion du cycle de vie : Les interceptors peuvent gérer les événements du cycle de vie des entités tels que onLoad, onSave, onUpdate et onDelete.

  • Exécution de logique personnalisée : Permet l’exécution d’une logique personnalisée avant ou après que certaines opérations soient effectuées sur une entité.

  • Contrôle centralisé : Fournit un mécanisme centralisé pour gérer des préoccupations transversales telles que l’enregistrement, l’audit et la validation.


📌 Qu’est-ce que les Event Listeners Hibernate ?

Les écouteurs d’événements dans Hibernate fournissent un moyen plus flexible et précis de gérer les événements du cycle de vie des entités par rapport aux interceptors. Hibernate propose une variété d’événements qui peuvent être écoutés, tels que pré-insertion, post-insertion, pré-mise à jour, post-mise à jour, pré-suppression et post-suppression.

  • Contrôle granulaire : Offre un meilleur contrôle sur les événements du cycle de vie des entités avec une large gamme de types d’événements.

  • Extensibilité : Permet l’ajout d’écouteurs d’événements personnalisés pour gérer des événements spécifiques selon les besoins.

  • Séparation des préoccupations : Aide à séparer la logique métier de la gestion des événements du cycle de vie, rendant le code plus maintenable.


📌 Comment les Interceptors et Event Listeners peuvent-ils être utilisés pour le tuning de performance ?

  • Enregistrement et surveillance : Les interceptors et les écouteurs d’événements peuvent enregistrer les temps d’exécution des requêtes, les hits/misses de cache et d’autres métriques de performance pour aider à identifier les goulots d’étranglement.

  • Audit : Suivre les modifications apportées aux entités pour comprendre comment les modifications de données impactent les performances.

  • Validation : Garantir l’intégrité et la cohérence des données en validant les entités avant qu’elles ne soient persistées ou mises à jour.

  • Gestion du cache : Implémenter une logique personnalisée pour gérer les entrées du cache, comme l’invalidation ou la mise à jour du cache lorsque les entités sont modifiées.

Exemple de mise en œuvre d’un interceptor pour enregistrer les temps d’exécution des requêtes

Étape 1: Créer un interceptor personnalisé

Implémenter un interceptor personnalisé en étendant la classe EmptyInterceptor fournie par Hibernate.

import org.hibernate.EmptyInterceptor;
import org.hibernate.type.Type;
import java.io.Serializable;
import java.util.Iterator;

public class PerformanceInterceptor extends EmptyInterceptor {
  @Override
  public String onPrepareStatement(String sql) {
    long startTime = System.currentTimeMillis();
    String result = super.onPrepareStatement(sql);
    long endTime = System.currentTimeMillis();
    System.out.println("Query: " + sql + " executed in " + (endTime - startTime) + " ms");
    return result;
  }

  @Override
  public boolean onLoad(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
    System.out.println("Loading entity: " + entity.getClass().getSimpleName() + " with ID: " + id);
    return super.onLoad(entity, id, state, propertyNames, types);
  }

  @Override
  public boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
    System.out.println("Saving entity: " + entity.getClass().getSimpleName());
    return super.onSave(entity, id, state, propertyNames, types);
  }
}
Enter fullscreen mode Exit fullscreen mode

Étape 2: Enregistrer l’interceptor dans la configuration Hibernate

Enregistrer l’interceptor personnalisé dans le fichier de configuration Hibernate (hibernate.cfg.xml).

<hibernate-configuration>
  <session-factory>
    <!-- Autres configurations -->
    <!-- Enregistrer l’interceptor personnalisé -->
    <property name="hibernate.ejb.interceptor">com.example.interceptor.PerformanceInterceptor</property>
  </session-factory>
</hibernate-configuration>
Enter fullscreen mode Exit fullscreen mode

Étape 3: Utiliser l’interceptor dans une configuration basée sur Java

Alternativement, enregistrer l’interceptor par programme dans une configuration basée sur Java.

import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;

public class HibernateUtil {
  private static final SessionFactory sessionFactory;
  static {
    try {
      Configuration configuration = new Configuration();
      configuration.configure("hibernate.cfg.xml");
      configuration.setInterceptor(new PerformanceInterceptor());
      sessionFactory = configuration.buildSessionFactory();
    } catch (Throwable ex) {
      throw new ExceptionInInitializerError(ex);
    }
  }

  public static SessionFactory getSessionFactory() {
    return sessionFactory;
  }
}
Enter fullscreen mode Exit fullscreen mode

11 - Conclusion

En résumé, optimiser Hibernate revient à accorder une chaîne cohérente de leviers complémentaires : des mappings propres, des stratégies de fetch maîtrisées, des requêtes pensées pour l’usage réel, un cache utilisé avec discernement, et une observation continue pour éviter les dérives.

Mal configuré, Hibernate produit du N+1, de la latence et de la charge inutile.

L’idée centrale reste simple : comprendre ce que charge Hibernate, pourquoi il le charge, et comment cela se comporte en production.

Commencer avec des réglages clairs, instrumenter tôt, affiner régulièrement sur la base de données réelles — c’est ce qui transforme Hibernate d’un facteur de risque en véritable socle robuste pour la montée en charge.

À vous maintenant d’appliquer, mesurer, itérer.

Top comments (0)