11.3. Contrôle de consurrence optimiste

La gestion optimiste des accès concurrents avec versionnage est la seule approche pouvant garantir l'extensibilité des applications à haut niveau de charge. Le système de versionnage utilise des numéros de version ou l'horodatage pour détecter les mises à jour causant des conflits avec d'autres actualisations antérieures. Hibernate propose trois approches pour l'écriture de code applicatif utilisant la gestion optimiste d'accès concurrents. Le cas d'utilisation décrit plus bas fait mention de conversation, mais le versionnage peut également améliorer la qualité d'une application en prévenant la perte de mises à jour.

11.3.1. Gestion du versionnage au niveau applicatif

Dans cet exemple d'implémentation utilisant peu les fonctionnalités d'Hibernate, chaque interaction avec la base de données se fait en utilisant une nouvelle Session et le développeur doit recharger les données persistantes à partir de la BD avant de les manipuler. Cette implémentation force l'application à vérifier la version des objets afin de maintenir l'isolation transactionnelle. Cette approche, semblable à celle retrouvée pour les EJB, est la moins efficace de celles présentées dans ce chapitre.

// foo is an instance loaded by a previous Session
session = factory.openSession();
Transaction t = session.beginTransaction();

int oldVersion = foo.getVersion();
session.load( foo, foo.getKey() ); // load the current state
if ( oldVersion != foo.getVersion() ) throw new StaleObjectStateException();
foo.setProperty("bar");

t.commit();
session.close();

Le mapping de la propriété version est fait via <version> et Hibernate l'incrémentera automatiquement à chaque flush() si l'entité doit être mise à jour.

Bien sûr, si votre application ne fait pas face à beaucoup d'accès concurrents et ne nécessite pas l'utilisation du versionnage, cette approche peut également être utilisée, il n'y a qu'à ignorer le code relié au versionnage. Dans ce cas, la stratégie du last commit wins (littéralement: le dernier commit l'emporte) sera utilisée pour les conversations (longues transactions applicatives). Gardez à l'esprit que cette approche pourrait rendre perplexe les utilisateurs de l'application car ils pourraient perdre des données mises à jour sans qu'aucun message d'erreur ne leur soit présenté et sans avoir la possibilité de fusionner les données.

Clearly, manual version checking is only feasible in very trivial circumstances and not practical for most applications. Often not only single instances, but complete graphs of modified objects have to be checked. Hibernate offers automatic version checking with either an extended Session or detached instances as the design paradigm.

11.3.2. Les sessions longues et le versionnage automatique.

Dans ce scénario, une seule instance de Session et des objets persistants est utilisée pour toute l'application. Hibernate vérifie la version des objets persistants avant d'effectuer le flush() et lance une exception si une modification concurrente est détectée. Il appartient alors au développeur de gérer l'exception. Les traitements alternatifs généralement proposés sont alors de permettre à l'usager de faire la fusion des données ou de lui offrir de recommencer son travail à partie des données les plus récentes dans la BD.

Il est à noter que lorsqu'une application est en attente d'une action de la part de l?usager, La Session n'est pas connectée à la couche JDBC sous-jacente. C'est la manière la plus efficace de gérer les accès à la base de données. L'application ne devrait pas se préoccuper du versionnage des objets, de la réassociation des objets détachés, ni du rechargement de tous les objets à chaque transaction.

// foo is an instance loaded earlier by the old session
Transaction t = session.beginTransaction(); // Obtain a new JDBC connection, start transaction

foo.setProperty("bar");

session.flush();    // Only for last transaction in conversation
t.commit();         // Also return JDBC connection
session.close();    // Only for last transaction in conversation

L'objet foo sait quel objet Session l'a chargé. Session.reconnect() obtient une nouvelle connexion (celle-ci peut être également fournie) et permet à la session de continuer son travail. La méthode Session.disconnect() déconnecte la session de la connexion JDBC et retourne celle-ci au pool de connexion (à moins que vous ne lui ayez fourni vous même la connexion.) Après la reconnexion, afin de forcer la vérification du versionnage de certaines entités que vous ne cherchez pas à actualiser, vous pouvez faire un appel à Session.lock() en mode LockMode.READ pour tout objet ayant pu être modifié par une autre transaction. Il n'est pas nécessaire de verrouiller les données que vous désirez mettre à jour.

Si des appels implicites aux méthodes disconnect() et reconnect() sont trop coûteux, vous pouvez les éviter en utilisant hibernate.connection.release_mode .

Ce pattern peut présenter des problèmes si la Session est trop volumineuse pour être stockée entre les actions de l'usager. Plus spécifiquement, une session HttpSession se doit d'être la plus petite possible. Puisque la Session joue obligatoirement le rôle de mémoire cache de premier niveau et contient à ce titre tous les objets chargés, il est préférable de n'utiliser cette stratégie que pour quelques cycles de requêtes car les objets risquent d'y être rapidement périmés.

Notez que la Session déconnectée devrait être conservée près de la couche de persistance. Autrement dit, utilisez un EJB stateful pour conserver la Session et évitez de la sérialiser et de la transférer à la couche de présentation (i.e. Il est préférable de ne pas la conserver dans la session HttpSession .)

The extended session pattern, or session-per-conversation, is more difficult to implement with automatic current session context management. You need to supply your own implementation of the CurrentSessionContext for this, see the Hibernate Wiki for examples.

11.3.3. Les objets détachés et le versionnage automatique

Chaque interaction avec le système de persistance se fait via une nouvelle Session . Toutefois, les mêmes instances d'objets persistants sont réutilisées pour chacune de ces interactions. L'application doit pouvoir manipuler l'état des instances détachées ayant été chargées antérieurement via une autre session. Pour ce faire, ces objets persistants doivent être rattachés à la Session courante en utilisant Session.update() , Session.saveOrUpdate() , ou Session.merge() .

// foo is an instance loaded by a previous Session
foo.setProperty("bar");
session = factory.openSession();
Transaction t = session.beginTransaction();
session.saveOrUpdate(foo); // Use merge() if "foo" might have been loaded already
t.commit();
session.close();

Again, Hibernate will check instance versions during flush, throwing an exception if conflicting updates occurred.

Vous pouvez également utiliser lock() au lieu de update() et utiliser le mode LockMode.READ (qui lancera une vérification de version, en ignorant tous les niveaux de mémoire cache) si vous êtes certain que l'objet n'a pas été modifié.

11.3.4. Personnaliser le versionnage automatique

Vous pouvez désactiver l'incrémentation automatique du numéro de version de certains attributs et collections en mettant la valeur du paramètre de mapping optimistic-lock à false. Hibernate cessera ainsi d'incrémenter leur numéro de version s'ils sont mis à jour.

Legacy database schemas are often static and can't be modified. Or, other applications might also access the same database and don't know how to handle version numbers or even timestamps. In both cases, versioning can't rely on a particular column in a table. To force a version check without a version or timestamp property mapping, with a comparison of the state of all fields in a row, turn on optimistic-lock="all" in the <class> mapping. Note that this conceptually only works if Hibernate can compare the old and new state, i.e. if you use a single long Session and not session-per-request-with-detached-objects.

Il peut être souhaitable de permettre les modifications concurrentes lorsque des champs distincts sont modifiés. En mettant la propriété optimistic-lock="dirty" dans l'élément <class> , Hibernate ne fera la comparaison que des champs devant être actualisés lors du flush().

In both cases, with dedicated version/timestamp columns or with full/dirty field comparison, Hibernate uses a single UPDATE statement (with an appropriate WHERE clause) per entity to execute the version check and update the information. If you use transitive persistence to cascade reattachment to associated entities, Hibernate might execute unnecessary updates. This is usually not a problem, but on update triggers in the database might be executed even when no changes have been made to detached instances. You can customize this behavior by setting select-before-update="true" in the <class> mapping, forcing Hibernate to SELECT the instance to ensure that changes did actually occur, before updating the row.