Hibernate vs JDBC - Performances comparées
Le 16 juin 2008, à 10:13 par Ulhume...

Ce n'est pas un scoop même si cela mérite souvent d'être rappelé, la base de données est un élément crucial pour à peu prés toute application professionnelle. Cruciale car c'est elle qui héberge le système d’information et qui, à ce titre, a beaucoup plus de valeur que tous les applicatifs, aussi complexes soient-ils, qui gravitent autour. Cruciale aussi en terme de performance car une base mal choisie, mal paramétrée ou mal utilisée, détruira systématiquement les plus louables efforts d'optimisation. Un point à garder en tête lorsqu'en Java (ou ailleurs) arrive le moment d'écrire ou de lire dans une base et que se pose la Grande Question : persistance ou pas persistance...

Cadre de l’évaluation

Hibernate

Je suis parti sur l’exemple fournit dans le tutoriel d’Hibernate. Dans cet exemple nous avons une très simple table Event et l’objet qui lui est associé avec seulement trois champs. Pas de relation, pas d’exotisme. J’ai juste rajouté une méthode pour sauvegarder un objet modifié et encadré les fonctions histoire de répéter 1000 fois l’opération. La session Hibernate est créée à l'extérieur de la boucle et n’est pas prise en compte dans le calcul du temps passé :

  1.     session = HibernateUtil.getSessionFactory().openSession();
  2.  
  3.   // je regarde l’heure
  4.     for (int i = 0; i < parameter(); i++) {
  5.       session.beginTransaction();
  6.       session.save(new Event("My Event"));
  7.       session.getTransaction().commit();
  8.     }
  9.      // je regarde le temps passé ici

J’ai choisi délibérément de faire une transaction par itération car c’est ainsi que cela se passe dans la vraie vie. Je ferais la même chose avec JDBC en faisant un commit à chaque tour.

Je prends tout ce luxe de précaution d’introduction pour éviter toute envolée lyriquo-trollesque. En gros, si vous pensez que ce code n’est pas optimisé, il faut aller engueuler les gens qui ont écrit le tutorial. Moi je n’y suis pour rien, j’ai fais juste comme on m’a dit de faire.

Pour terminer sur le cadre, j'inclus ici le fichier de paramétrage (en version compactée) utilisé pour ce test. Au passage, merci à Stéphane pour son aide sur les caches. Si vous avez des remarques sur un mauvaise usage rendant Hibernate sous-optimum, merci de me le faire savoir. Le but est d'avoir une comparaison le plus juste possible.

  1. <?xml version='1.0' encoding='utf-8'?>
  2. <hibernate-configuration>
  3.   <session-factory>
  4.     <property name="connection.driver_class">org.postgresql.Driver</property>
  5.     <property name="connection.url">jdbc:postgresql://mon_serveur/ma_base</property>
  6.     <property name="connection.username">mon_user</property>
  7.     <property name="connection.password">mon_password</property>
  8.     <!-- <property name="connection.pool_size">1</property>  -->
  9.     <property name="dialect">org.hibernate.dialect.PostgreSQLDialect</property>
  10.     <property name="current_session_context_class">thread</property>
  11.     <property name="cache.provider_class">org.hibernate.cache.NoCacheProvider</property>
  12.     <property name="c3p0.acquire_increment">1</property>
  13.     <property name="c3p0.idle_test_period">100</property>
  14.     <property name="c3p0.max_size">100</property>
  15.     <property name="c3p0.max_statements">20</property>
  16.     <property name="c3p0.min_size">10</property>
  17.     <property name="c3p0.timeout">100</property>
  18.     <property name="show_sql">false</property>
  19.     <property name="hbm2ddl.auto">create</property>
  20.     <mapping resource="net/karmaLab/benchmark/hibernate/Event.hbm.xml" />
  21.   </session-factory>
  22. </hibernate-configuration>

JDBC

Pour la version "à la main", je travaille sur la même base qu'hibernate, avec le mêmes pilote JDBC. L’équivalent de la méthode donnée plus haut est donc :

  1.     Class.forName("org.postgresql.Driver");
  2.     connection = DriverManager.getConnection("jdbc:postgresql://mon_serveur/tests", "mon_user", "mon_pass");
  3.  
  4.    // Je regarde l’heure ici
  5.  
  6.     PreparedStatement statement = connection
  7.         .prepareStatement("insert into EVENTS (EVENT_DATE, title, EVENT_ID) values (?, ?, ?)");
  8.     Event event = new Event("My Event");
  9.     for (int i = 0; i < parameter(); i++) {
  10.  
  11.       statement.setDate(1, new Date(event.getDate().getTime()));
  12.       statement.setString(2, event.getTitle());
  13.       statement.setLong(3, id++);
  14.       statement.execute();
  15.       connection.commit();
  16.     }
  17. // je regarde le temps passé ici

Alors oui, le code JDBC est bien moins joli que le code Hibernate, mais voyons ce que cela donne côté performances.

Résultats

Les tests suivants ont été effectués avec le cache des PreparedStatement activé (c3p0.max_statements=20). Chaque test a été réitéré 10 fois de suite et seul le meilleur temps est gardé. Enfin le cache disque est vidé à chaque lancé et la base de données PostgreSQL est posée sur une machine distante sur un réseau local Gigabit.

Inserts

Selects

Updates

Conclusion

Bon, j'ai comme l'impression que tout cela se passe de commentaire ou presque : Hibernate est littéralement écrasé.

Bon, c'est pas un scoop non plus, mais cela a le mérite de tordre un peu le coup à la légende urbaine laissant croire jusqu'à un gain de performance avec Hibernate. Je veux bien accepter l'idée que dans des cas plus complexes de tables liées les unes aux autres Hibernate puisse obtenir des performances comparativement moins désastreuses mais pour des choses simples, le résultat est là.

Une couche de persistance, quelle qu'elle soit, a un coût. Et plus la volumétrie augmente, plus ce coup devient prépondérant. Un coût qui ne s'évalue pas seulement comme le pensent certains à la qualité et au nombre des requêtes émises, mais aussi aux ressources utilisées pour les fabriquer, et c'est bien là que le bât blesse.

Maintenant utiliser ou pas Hibernate ou équivalent (comme le système des EJB 2.0) est une question de choix d'architecture et de besoin. L'idée n'est pas ici de jeter le bébé avec l'eau du bain mais bien de prendre conscience que chaque "facilité" a une contrepartie et que dans certains cas cette dernière peut être létale à un projet.

Commentaires

Stephane , le 12 June, 2008 - 20:29

Bonjour,

Pour que la comparaison soit juste, il me semble que le cache de prepared statement du pool de connexions utilisée par hibernate devrait être activé.

Vous ne précisez pas cette information, votre évaluation avait elle été faite avec ce cache activé ? Cela peut fondamentalement changer les performances et c'est le mode normal d'utilisation d'hibernate en production, sinon hibernate passe son temps à ouvrir et fermer des prepared statement.

Ulhume, le 14 June, 2008 - 19:59

Bonne remarque merci. Je vais activer cela et si cela change quelque chose je rendrais à césar ce qui lui revoient Wink

Pour information et comme je le disais dans l'article, j'ai utilisé pour hibernate, strictement rien de plus que ce qui se trouve dans leur tutorial, fichiers de paramétrage compris.

Ulhume, le 16 June, 2008 - 02:06

Bon, j'ai activé le cache (org.hibernate.cache.EhCacheProvide) comme indiqué dans la doc, c'est effectivement mieux, mais cela reste bien moins efficace qu'un appel JDBC simple. J'ai utilisé le mauvais provider de cache ? Un paramètre en plus ?

Gronono , le 16 June, 2008 - 11:02

Bonjour,
Effectivement, ce n'est pas une surprise de voire Hibernate moins performant que du JDBC simple.
Mais Hibernate apporte un gain important en terme de développement sur des moyens/gros projets (je dirais qu'à partir d'une vingtaine de table c'est un plus).
En effet, le mapping se fait simplement (plus ou moins suivant les associations entre les objets). Il est même allégé par le système d'annotations remplaçant le XML.
En utilisant Hibernate, on pense tout objet même les requêtes en utilisant les criterias. Les associations entre les objets (et les tables) se fait sans aucun effort de la part du développeur.
Le plus gros travail avec Hibernate est sa configuration (simplifié si on le couple avec Spring). Après c'est un jeu d'enfant.
Pense aussi à la gestion de transaction et d'accès concurrent, qu'il faut implémenter à la main en JDBC.

Bref, sur des projets dont le développement est couteux et destinés à durer, ils faut mieux choisir Hibernate dès le début pour gagner du temps par la suite.

Je rajouterai une dernière chose. Les performances d'une application ne sont pas le premier facteur à prendre en compte. Il faut mieux faire une application maintenable et qui donne des résultats correctes qu'une application rapide qui se trompe une fois sur deux et dont ne peut pas relire le code parqu'il y en a partout.

C'est un simple constat que j'ai fait en me basant sur mon expérience professionnel. Souvent Hibernate simplifie bien les choses.

Cordialement,
Gronono

Ulhume, le 16 June, 2008 - 11:15

@Gronono

Tout d'abord, pour JDBC, une grande simplification du processus peut être opéré conjointement par la génération automatique de code et l'utilisation de Spring JDBC Template pour les cas "non standards". La génération de code est une solution souvent, et à tord, écartée qui peut permettre d'extraire d'un schéma de base la DAO "basique" qu'Hibernate fabrique dynamiquement. Fonctionellement cela abouti au même résultat. Ensuite pour l'épineux problème des selects, Spring JDBCTemplate permet de garder un bon rapport performance/maintenabilité.

De manière générale, la facilité de maintenance d'un système de développement est souvent liée à la capacité de ceux qui en ont la charge à mettre en place des solutions efficace de rationalisation. A titre d'exemple, certains projets où j'ai eu à jouer les pompiers, avec leurs 200 fichiers .hbm, étaient bien loin d'être d'être optimum Wink A se stade, il aurait mieux fallu écrire les requêtes à la main.

Ceci dit, chaque approche apporte son lot d'avantages et d'inconvénients. J'ai aussi un longue expérience professionnelle qui me fait dire qu'en ce domaine il n'est jamais de règle absolue. Si j'ai à construire l'architecture d'un système de gestion dont le système d'information est remplis au grès d'étapes de saisie humaines (typique d'une application de gestion), la perte de performance induit par hibernate va clairement être compensé par la maintenabilité. En revanche lorsqu'il s'agit par exemple de stocker en base des données remontant en flux (données financières, issues de systèmes embarqués, etc), hibernate devient un problème et le gain de maintenabilité est complètement éclipsé par des performances désastreuses. De même s'il s'agit d'une application amené à faire des requêtes en simple lecture sur une base massive.

Tout cela pour dire que même si je suis un peu ironique dans mon introduction, ce benchmark n'a pour autre but que de faire prendre conscience que le choix hibernate a un coût et que ce coût ne peut être négligé qu'à partir du moment où l'on a clairement évalué et même testé cette solution sur les véritables volumétries que l'application aura à prendre en charge.

Donc pour reprendre (sans ironie) ta phrase

Bref, sur des projets dont le développement est coûteux et destinés à durer, ils faut mieux choisir Hibernate dès le début pour gagner du temps par la suite.

, je reformulerais en disant que pour choisir, il faut déjà être sur de l'endroit où l'on met les pieds, et ensuite seulement, Hibernate est un choix qui présente des avantages, tout comme JDBC.

En somme, tout choix d'architecture est raisonnable, s'il est raisonné, et basé sur les véritables informations, pas les mythes de performance relayés par plus les enthousiastes et/ou les plus intéressés Wink Car si ce n'est pas une surprise pour toi, un tel décalage de performance en a été une pour moi, surtout après avoir été de nombreuse fois "contré" sur certains choix d'architecture par un "Hibernate est aussi rapide, si ce n'est plus rapide, que du natif". Un rapport de 3 à 7 selon l'opération, sur un million d'enregistrement, c'est loin d'être une paille...

Dernier point avant d'arrêter mes jacasseries, le fait de tout penser objet comme tu l'évoques est aussi un facteur de perte de performances. Car le monde objet et le monde relationnels sont suffisamment éloignés pour que le fait d'éloigner cette contrainte des tâches quotidiennes d'un développeur, lui fasse produire des architecture intrinsèquement sous-optimales par rapport à ce que peu produire une base de donnée "moderne".

Ulhume, le 17 June, 2008 - 11:26

J'ai mis à jour le test en suivant les conseils de stéphane (c3p0.max_statements). Ceci dit, je n'ai pas ajouté C3p0 sur la version JDBC, ce qui "handicape" ce dernier.

J'ai aussi ajouté le vidage du cache disque entre chaque tests. Les résultats ne change pas des masses mais c'est sans aucun doute plus objectif.

Ulhume, le 18 June, 2008 - 09:32
Mise à jour
  • Ajout de la configuration d'hibernate.
  • Moins de bla-bla.

Poster un nouveau commentaire

Le contenu de ce champ est gardé secret et ne sera pas montré publiquement.
  • Allowed HTML tags: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • To highlight piece of code, just surround them with <code type="language"> Your code &tl;/code>>. Language can be java,c++,bash,etc... Everything Geshi support.
  • Les lignes et les paragraphes vont à la ligne automatiquement.
  • Textual smileys will be replaced with graphical ones.
  • Les adresses de pages web et de messagerie électronique sont transformées en liens automatiquement.

Plus d'informations sur les options de formatage

Connexion utilisateur
Les derniers bavardages...