Chapitre 13. Traitement par paquet

Une approche naïve pour insérer 100 000 lignes dans la base de données en utilisant Hibernate pourrait ressembler à ça :

Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
for ( int i=0; i<100000; i++ ) {
    Customer customer = new Customer(.....);
    session.save(customer);
}
tx.commit();
session.close();

Ceci devrait s'écrouler avec une OutOfMemoryException quelque part aux alentours de la 50 000ème ligne. C'est parce qu'Hibernate cache toutes les instances de Customer nouvellement insérées dans le cache de second niveau.

Dans ce chapitre nous montrerons comment éviter ce problème. D'abord, cependant, si vous faites des traitements par batch, il est absolument critique que vous activiez l'utilisation ds paquet JDBC (NdT : JDBC batching), si vous avez l'intention d'obtenir des performances raisonnables. Configurez la taille du paquet JDBC avec un nombre raisonnable (disons, 10-50) :

hibernate.jdbc.batch_size 20

Notez qu'Hibernate désactive, de manière transparente, l'insertion par paquet au niveau JDBC si vous utilisez un générateur d'identifiant de type identity.

Vous pourriez aussi vouloir faire cette sorte de travail dans un traitement où l'interaction avec le cache de second niveau est complètement désactivé :

hibernate.cache.use_second_level_cache false

13.1. Insertions en paquet

Lorsque vous rendez des nouveaux objets persistants, vous devez régulièrement appeler flush() et puis clear() sur la session, pour contrôler la taille du cache de premier niveau.

Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();

for ( int i=0; i<100000; i++ ) {
    Customer customer = new Customer(.....);
    session.save(customer);
    if ( i % 20 == 0 ) { //20, même taille que la taille du paquet JDBC
        //flush un paquet d'insertions et libère la mémoire :
        session.flush();
        session.clear();
    }
}

tx.commit();
session.close();

13.2. Paquet de mises à jour

Pour récupérer et mettre à jour des données les mêmes idées s'appliquent. En plus, vous avez besoin d'utiliser scroll() pour tirer partie des curseurs côté serveur pour les requêtes qui retournent beaucoup de lignes de données.

Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();

ScrollableResults customers = session.getNamedQuery("GetCustomers")
    .setCacheMode(CacheMode.IGNORE)
    .scroll(ScrollMode.FORWARD_ONLY);
int count=0;
while ( customers.next() ) {
    Customer customer = (Customer) customers.get(0);
    customer.updateStuff(...);
    if ( ++count % 20 == 0 ) {
        //flush un paquet de mises à jour et libère la mémoire :
        session.flush();
        session.clear();
    }
}

tx.commit();
session.close();

13.3. L'interface StatelessSession

Alternativement, Hibernate fournit une API orientée commande qui peut être utilisée avec des flux de données pour et en provenance de la base de données sous la forme d'objets détachés. Une StatelessSession n'a pas de contexte de persistance associé et ne fournit pas beaucoup de sémantique de durée de vie de haut niveau. En particulier, une session sans état n'implémente pas de cache de premier niveau et n'interagit pas non plus avec un cache de seconde niveau ou un cache de requêtes. Elle n'implémente pas les transactions ou la vérification sale automatique (NdT : automatic dirty checking). Les opérations réalisées avec une session sans état ne sont jamais répercutées en cascade sur les instances associées. Les collections sont ignorées par une session sans état. Les opérations exécutées via une session sans état outrepasse le modèle d'événements d'Hibernate et les intercepteurs. Les sessions sans état sont vulnérables aux effets de modification des données, ceci est dû au manque de cache de premier niveau. Une session sans état est une abstraction bas niveau, plus proche de la couche JDBC sous-jacente.

StatelessSession session = sessionFactory.openStatelessSession();
Transaction tx = session.beginTransaction();

ScrollableResults customers = session.getNamedQuery("GetCustomers")
    .scroll(ScrollMode.FORWARD_ONLY);
while ( customers.next() ) {
    Customer customer = (Customer) customers.get(0);
    customer.updateStuff(...);
    session.update(customer);
}

tx.commit();
session.close();

Notez que dans le code de l'exemple, les intances de Customer retournées par la requête sont immédiatement détachées. Elles ne sont jamais associées à un contexte de persistance.

Les opérations insert(), update() et delete() définies par l'interface StatelessSession sont considérées comme des opérations d'accès direct aux lignes de la base de données, ce qui résulte en une exécution immédiate du SQL INSERT, UPDATE ou DELETE respectif. De là, elles ont des sémantiques tres différentes des opérations save(), saveOrUpdate() et delete() définies par l'interface Session.

13.4. Opérations de style DML

Comme déjà discuté avant, le mapping objet/relationnel automatique et transparent est intéressé par la gestion de l'état de l'objet. Ceci implique que l'état de l'objet est disponible en mémoire, d'où manipuler (en utilisant des expressions du langage de manipulation de données - Data Manipulation Language (DML) - SQL) les données directement dans la base n'affectera pas l'état en mémoire. Pourtant, Hibernate fournit des méthodes pour l'exécution d'expression DML de style SQL lesquelles sont réalisées à travers le langage de requête d'Hibernate (Chapitre 14, HQL: Langage de requêtage d'Hibernate).

La pseudo-syntaxe pour les expressions UPDATE et DELETE est : ( UPDATE | DELETE ) FROM? EntityName (WHERE where_conditions)?. Certains points sont à noter :

  • Dans la clause from, le mot-clef FROM est optionnel

  • Il ne peut y avoir qu'une seule entité nommée dans la clause from ; elle peut optionnellement avoir un alias. Si le nom de l'entité a un alias, alors n'importe quelle référence de propriété doit être qualifiée en ayant un alias ; si le nom de l'entité n'a pas d'alias, alors il est illégal pour n'importe quelle référence de propriété d'être qualifiée.

  • Aucune jointure (implicite ou explicite) ne peut être spécifiée dans une requête HQL. Les sous-requêtes peuvent être utilisées dans la clause where ; les sous-requêtes, elles-mêmes, peuvent contenir des jointures.

  • La clause where est aussi optionnelle.

Par exemple, pour exécuter un UPDATE HQL, utilisez la méthode Query.executeUpdate() (la méthode est données pour ceux qui sont familiers avec PreparedStatement.executeUpdate() de JDBC) :

Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();

String hqlUpdate = "update Customer c set c.name = :newName where c.name = :oldName";
// ou String hqlUpdate = "update Customer set name = :newName where name = :oldName";
int updatedEntities = s.createQuery( hqlUpdate )
        .setString( "newName", newName )
        .setString( "oldName", oldName )
        .executeUpdate();
tx.commit();
session.close();

Par défaut, les statements HQL UPDATE, n'affectent pas la valeur des propriétés Section 5.1.7, « version (optionnel) » ou Section 5.1.8, « timestamp (optionnel) » pour les entités affectées; ceci est compatible avec la spec EJB3. Toutefois, vous pouvez forcer Hibernate à mettre à jour les valeurs des propriétés version ou timestamp en utilisant le versioned update. Pour se faire, ajoutez le mot clé VERSIONED après le mot clé UPDATE.

Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
String hqlVersionedUpdate = "update versioned Customer set name = :newName where name = :oldName";
int updatedEntities = s.createQuery( hqlUpdate )
        .setString( "newName", newName )
        .setString( "oldName", oldName )
        .executeUpdate();
tx.commit();
session.close();

Notez que les types personnalisés (org.hibernate.usertype.UserVersionType) ne sont pas supportés en conjonction avec le statement update versioned statement.

Pour exécuter un HQL DELETE, utilisez la même méthodeQuery.executeUpdate():

Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();

String hqlDelete = "delete Customer c where c.name = :oldName";
// or String hqlDelete = "delete Customer where name = :oldName";
int deletedEntities = s.createQuery( hqlDelete )
        .setString( "oldName", oldName )
        .executeUpdate();
tx.commit();
session.close();

La valeur du int retourné par la méthode Query.executeUpdate() indique le nombre d'entités affectées par l'opération. Considérez que cela peut ou pas corréler le nombre de lignes affectés dans la base de données. Une opération HQL pourrait entraîner l'exécution de multiples expressions SQL réelles, pour des classes filles mappées par jointure (NdT: join-subclass), par exemple. Le nombre retourné indique le nombre d'entités réelles affectées par l'expression. Retour à l'exemple de la classe fille mappée par jointure, un effacement d'une des classes filles peut réellement entraîner des suppressions pas seulement dans la table qui mappe la classe fille, mais aussi dans la table "racine" et potentillement dans les tables des classes filles plus bas dans la hiérarchie d'héritage.

La pseudo-syntaxe pour l'expression INSERT est : INSERT INTO EntityName properties_list select_statement. Quelques points sont à noter :

  • Seule la forme INSERT INTO ... SELECT ... est supportée ; pas la forme INSERT INTO ... VALUES ... .

    La properties_list est analogue à la spécification de la colonne The properties_list is analogous to the column speficiation dans l'expression SQL INSERT. Pour les entités impliquées dans un héritage mappé, seules les propriétés directement définies à ce niveau de classe donné peuvent être utilisées dans properties_list. Les propriétés de la classe mère ne sont pas permises ; et les propriétés des classes filles n'ont pas de sens. En d'autres mots, les expressions INSERT par nature non polymorphiques.

  • select_statement peut être n'importe quelle requête de sélection HQl valide, avec l'avertissement que les types de retour doivent correspondre aux types attendus par l'insertion. Actuellement, c'est vérifié durant la compilation de la requête plutôt que la vérification soit reléguée à la base de données. Notez cependant que cela pourrait poser des problèmes entre les Types d'Hibernate qui sont équivalents opposé à égaux. Cela pourrait poser des problèmes avec des disparités entre une propriété définie comme un org.hibernate.type.DateType et une propriété définie comme un org.hibernate.type.TimestampType, même si la base de données ne ferait pas de distinction ou ne serait pas capable de gérer la conversion.

  • Pour la propriéte id, l'expression d'insertion vous donne deux options. Vous pouvez soit spécifier explicitement la propriété id dans properties_list (auquel cas sa valeur est extraite de l'expression de sélection correspondante), soit l'omettre de properties_list (auquel cas une valeur générée est utilisée). Cette dernière option est seulement disponible en utilisant le générateur d'identifiant qui opère dans la base de données ; tenter d'utiliser cette option avec n'importe quel type de générateur "en mémoire" causera une exception durant l'analyse. Notez que pour les buts de cette discussion, les générateurs "en base" sont considérés être org.hibernate.id.SequenceGenerator (et ses classes filles) et n'importe quelles implémentations de org.hibernate.id.PostInsertIdentifierGenerator. L'exception la plus notable ici est org.hibernate.id.TableHiLoGenerator, qu ne peut pas être utilisée parce qu'il ne propose pas un moyen de d'exposer ses valeurs par un select.

  • Pour des propriétés mappées comme version ou timestamp, l'expression d'insertion vous donne deux options. Vous pouvez soit spécifier la propriété dans properties_list (auquel cas sa valeur est extraite des expressions select correspondantes), soit l'omettre de properties_list (auquel cas la valeur de graine (NdT : seed value) définie par le org.hibernate.type.VersionType est utilisée).

Un exemple d'exécution d'une expression INSERT HQL :

Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();

String hqlInsert = "insert into DelinquentAccount (id, name) select c.id, c.name from Customer c where ...";
int createdEntities = s.createQuery( hqlInsert )
        .executeUpdate();
tx.commit();
session.close();