Hibernate n'est pas une base de données en lui même. C'est un outil léger de mapping objet relationnel. La gestion des transactions est déléguée à la connexion à base de données sous-jacente. Si la connexion est enregistrée dans JTA, les opérations effectuées pas la Session sont des parties atomiques de la transaction JTA. On peut voir Hibernate comme une fine surcouche de JDBC qui lui ajouterait les sémantiques objet.
Une SessionFactory est un objet threadsafe, couteux à créer, prévu pour être partagé par tous les threads de l'application. Une Session est un objet non threadsafe, non coûteux qui ne doit être utilisé qu'une fois, pour un process métier donné, puis détruit. Par exemple, lorsque vous utilisez Hibernate dans une application à base de servlets, les servlets peuvent obtenir une SessionFactory en utilisant
SessionFactory sf = (SessionFactory)getServletContext().getAttribute("my.session.factory");
Chaque appel de service créé une nouvelle Session, la flush(), commit() sa connexion, la close() et finalement la libère (La SessionFactory peut aussi être référencée dans le JNDI ou dans une variable statique Singleton).
Dans un bean session sans état, une approche similaire peut être utilisée. Le bean obtiendra une SessionFactory dans setSessionContext(). Ensuite, chaque méthode métier créera une Session, appelera flush() puis close(). Ben sûr, l'application n'a pas à appeler commit() sur la connexion.(Laissez cela à JTA, la connexion à la base de données participe automatiquement aux transactions gérées par le container).
Nous utilisons l'API Transaction d'Hibernate comme décrit précédemment. Un simple commit() de la Transaction Hibernate "flush" l'état et committe chaque connexion à la base de données associée (en gérant de manière particulière les transactions JTA).
Assurez vous de bien comprendre le sens de flush(). L'opération Flush() permet de synchroniser la source de données persistante avec les modifications en mémoire mais pas l'inverse. Notez que pour toutes les connexions/transactions JDBC utilisées par Hibernate, le niveau d'isolation de transaction pour ces connexions s'applique à toutes les opérations effectuées par Hibernate !
Les sections suivantes traitent d'approches alternatives qui utilisent le versioning pour garantir l'atomicité de la transaction. Elles sont considérées comme des techniques "avancées", et donc à utiliser en sachant ce que l'on fait.
Vous devez respecter les règles suivantes lorsque vous créez des Sessions Hibernate :
Ne jamais créer plus d'une instance concurrente de Session ou Transaction par connexion à la base de données.
Soyez extrêmement rigoureux lorsque vous créez plus d'une Session par base de données par transaction. La Session traçant elle-même les modifications faites sur les objets chargés, une autre Session pourrait voir des données corrompues.
La Session n'est pas threadsafe ! Deux thread concurrents ne doivent jamais accéder à la même Session . Généralement, la Session doit être considérée comme une unité de travail unitaire !
L'application peut accéder de manière concurrente à la même entité persistente via deux unités de travail différentes. Cependant, une instance de classe persistante n'est jamais partagée par deux instances Session. Il y a donc deux notions d'identité différentes.
foo.getId().equals( bar.getId() )
foo==bar
Pour les objets rattachés à une Session donnée, les deux notions sont identiques. Cependant, puisque l'application peut accéder de manière concurrente au "même" (identité persistante - dans la base de données) objet métier par deux sessions différentes, les deux instances seront en fait "différentes" (identité dans JVM).
Cette approche laisse la gestion de la concurrence à Hibernate et à la base de données. L'application n'aura jamais besoin de synchroniser un objet métier tant qu'elle s'en tient à un thread par Session ou à l'identité d'un objet (dans une Session, l'application peut utiliser sans risque == pour comparer deux objets).
Beaucoup de traitements métiers nécessitent une série d'interactions avec l'utilisateur entrecoupées d'accès à la base de données. Dans les applications web et les applications d'entreprise, il n'est pas acceptable qu'une transaction de base de données se déroule le temps de plusieurs interactions avec l'utilisateur.
La couche applicative prend dont en partie la responsabilité de maintenir l'isolation des traitements métier. C'est pourquoi, nous appelons ce processus une transaction applicative. Une transaction applicative pourra s'étendre sur plusieurs transactions à la base de données. Elle sera atomique si seule la dernière des transactions à la base de données enregistre les données mises à jour, les autres ne faisant que des accès en lecture.
La seule statégie remplissant les critères de concurrence et scalabitité élevées est le contrôle optimiste de la concurrence en appliquant des versions aux données : on utilisera par la suite le néologisme versionnage. Hibernate fournit trois approches pour écrire des applications basées sur la concurrence optimiste.
Une seule instance de Session et ses instances persistantes sont utilisées pour toute la transaction d'application.
La Session utilise le vérouillage optimiste pour s'assurer que plusieurs transactions à la base de données ne soient vues par l'application que comme une seule transaction logique (transaction applicative). La Session est déconnectée de sa connexion JDBC lorsqu'elle est en attente d'interaction avec l'utilisateur. Cette approche est la plus efficace en terme d'accès à la base de données. L'application n'a pas à ce soucier de la vérification de version ou du réattachement d'instaces détachées.
// foo est une instance chargée plus tôt par la Session session.reconnect(); foo.setProperty("bar"); session.flush(); session.connection().commit(); session.disconnect();
L'objet foo sait par quelle Session il a été chargé. Dès que la Session obtient une connexion JDBC, un commit sera fait sur les modifications apportées à l'objet.
Ce pattern est problématique si la Session est trop volumineuse pour être stockées pendant le temps de réflexion de l'utilisateur, par exemple il est souhaitable qu'une HttpSession reste aussi petite que possible. Comme la Session est aussi le premier niveau de cache et contient tous les objets chargés, il n'est probablement possible de n'utiliser cette stratégie que pour des cycles contenant peu de requêtes/réponses. C'est, en fait, recommandé puisque la Session risquerait très vite de contenir des données obsolètes.
Chaque interaction avec la base de données se fait dans une nouvelle Session. Cependant, les mêmes instances persistantes sont réutilisées pour chaque interaction à la base de données. L'application manipule l'état des instances détachées, chargées à l'initialement par une autre Session, puis "réassociées" en utilisant Session.update() ou Session.saveOrUpdate().
// foo est une instance chargée plus tôt par une autre Session foo.setProperty("bar"); session = factory.openSession(); session.saveOrUpdate(foo); session.flush(); session.connection().commit(); session.close();
Vous pouvez aussi appeler lock() au lieu de update() et utiliser LockMode.READ (effectuant un contrôle de version en court circuitant tous les caches) si vous êtes sûrs que l'objet n'a pas été modifié.
Chaque interaction avec la base de données se fait dans une nouvelle Session qui recharge toutes les instances persistantes depuis la base de données avant de les manipuler. Cette approche force l'application à assurer son propre contrôle de version pour garantir l'isolation de la transaction d'application (bien sur, Hibernate continuera de mettre à jour les numéros de version pour vous). Cette approche est la moins performante en terme d'accès à la base de données. Elle est ressemble plus à celle utilisée par les EJBs entités.
// foo est une instance chargée plus tôt par une autre Session session = factory.openSession(); int oldVersion = foo.getVersion(); session.load( foo, foo.getKey() ); if ( oldVersion!=foo.getVersion ) throw new StaleObjectStateException(); foo.setProperty("bar"); session.flush(); session.connection().commit(); session.close();
Evidemment, si vous vous trouvez dans un environnement avec peu de concurrence sur les données et que vous n'avez pas besoin de contrôle de version, vous pouvez utiliser cette méthode en retirant simplement le contrôle de version.
La première approche décrite ci dessus consiste à maintenir une seule Session pour tout un process métier qui englobe plusieurs interactions avec l'utilisateur (par exemple, une servlet peut stocker une Session dans l'HttpSession de l'utilisateur). Pour des raisons de performance, il est préférable
d'effectuer un commit de la Transaction (ou de la connexion JDBC) puis
déconnecter la Session de la connexion JDBC
avant d'attendre l'activité de l'utilisateur. La méthode Session.disconnect() déconnectera la session de la connexion JDBC et la retournera au pool (à moins que vous ne fournissiez la connexion).
Session.reconnect() obtient une nouvelle connexion (ou vous devez en fournir une) et redémarre la session. Après reconnexion, pour forcer le contrôle de version sur les données que vous ne modifiez pas, vous pouvez appeler Session.lock() sur les objets susceptibles d'avoir été modifiés par une autre transaction. Vous n'avez pas besoin de vérouiller (lock) les données que vous êtes en train de modifier.
Voici un exemple :
SessionFactory sessions; List fooList; Bar bar; .... Session s = sessions.openSession(); Transaction tx = null; try { tx = s.beginTransaction(); fooList = s.find( "select foo from eg.Foo foo where foo.Date = current date" //utilisation de la fonction date de DB2 ); bar = (Bar) s.save(Bar.class); tx.commit(); } catch (Exception e) { if (tx!=null) tx.rollback(); s.close(); throw e; } s.disconnect();
Puis :
s.reconnect(); try { tx = s.beginTransaction(); bar.setFooTable( new HashMap() ); Iterator iter = fooList.iterator(); while ( iter.hasNext() ) { Foo foo = (Foo) iter.next(); s.lock(foo, LockMode.READ); //vérifie que foo n'est pas obsolète bar.getFooTable().put( foo.getName(), foo ); } tx.commit(); } catch (Exception e) { if (tx!=null) tx.rollback(); throw e; } finally { s.close(); }
Vous pouvez voir que la relation entre les Transactions et les Sessions est de type plusieurs-vers-une. Une Session représente une conversation entre l'application et la base de données. La Transaction divise cette conversation en plusieurs unités atomiques de travail au niveau de la base de données.
Il n'est pas prévu que les utilisateurs passent beaucoup de temps à se soucier des stratégies de verrou. Il est généralement suffisant de spécifier le niveau d'isolation pour les connexions JDBC puis de laisser la base de données faire le travail. Cependant, les utilisateurs avancés veulent parfois obtenir des verrous pessimistes exclusifs, ou réobtenir les verrous au début d'une nouvelle transaction.
Hibernate utilisera toujours les mécanismes de vérouillage de la base de données, il ne vérouillera jamais les objets en mémoire !
La classe LockMode définit les niveaux de verrou qui peuvent être obtenus par Hibernate. Un verrou est obtenu pas les mécanismes suivant ;
LockMode.WRITE est obtenu automatiquement lorsqu'Hibernate insère ou modifie un enregistrement.
LockMode.UPGRADE peut être obtenu à la demande explicite de l'utilsateur en utilisant la syntaxe SELECT ... FOR UPDATE sur les bases de données qui la supportent.
LockMode.UPGRADE_NOWAIT peut être obtenu à la demande explicite de l'utilsateur grâce à la syntaxte SELECT ... FOR UPDATE NOWAIT sous Oracle.
LockMode.READ est obtenu automatiquement lorsqu'Hibernate consulte des données avec des niveaux d'isolation de type lectures reproductibles (repeatable read) ou de type sérialisable (serializable). Peut être réobtenu à la demande explicite de l'utilsateur
LockMode.NONE représente l'absence de verrou. Tous les objets basculent à ce verrou à la fin d'une Transaction. Les objets associés à la session via l'appel de update() ou saveOrUpdate() démarrent aussi sur ce mode de verrou.
La "demande explicite de l'utilsateur" se traduit par les moyens suivants :
un appel de Session.load(), spécifiant un mode de verrou (LockMode).
un appel de Session.lock().
un appel de Query.setLockMode().
Si Session.load() est appelée avec UPGRADE ou UPGRADE_NOWAIT, et que l'obet demandé n'a pas encore été chargé par la session, l'objet sera chargé en utilisant SELECT ... FOR UPDATE. Si load() est appelé et que l'objet a déja été chargé avec un mode moins restrictif, Hibernate appelle lock() pour cet objet.
Session.lock() effectue un contrôle de version si le mode de verrou spécifié est READ, UPGRADE ou UPGRADE_NOWAIT (Dans le cas de UPGRADE ou UPGRADE_NOWAIT, SELECT ... FOR UPDATE est utilisé).
Si la base de données ne supporte pas le mode de verrou demandé, Hibernate utilisera un mode approchant approprié (au lieu de lancer une exception). Ce qui garantit la portabilité des applications