Hibernate.orgCommunity Documentation

Chapitre 15. Traitement par lot

15.1. Insertions en lot
15.2. Mise à jour des lots
15.3. L'interface StatelessSession
15.4. Opérations de style DML

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

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 que Hibernate cache toutes les instances de Customer nouvellement insérées dans le cache de second niveau. Dans ce chapitre, nous allons vous montrer comment éviter ce problème.

Dans ce chapitre nous montrerons comment éviter ce problème. Mais tout d'abord, si vous faites des traitements par lot, il est absolument indispensable d'activer l'utilisation des lots JDBC, pour obtenir des performances raisonnables. Configurez la taille du lot JDBC à un nombre raisonnable (disons, 10-50) :

hibernate.jdbc.batch_size 20

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

Vous désirez peut-être effectuer ce genre de tâche dans un traitement où l'interaction avec le cache de second niveau est complètement désactivée :

hibernate.cache.use_second_level_cache false

Toutefois ce n'est pas absolument nécessaire puisque nous pouvons configurer le CacheMode de façon à désactiver l'interaction avec le cache de second niveau.

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, same as the JDBC batch size
        //flush a batch of inserts and release memory:
        session.flush();
        session.clear();
    }
}
   
tx.commit();
session.close();

Pour récupérer et mettre à jour des données les mêmes idées s'appliquent. De plus, vous devez 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 a batch of updates and release memory:
        session.flush();
        session.clear();
    }
}
   
tx.commit();
session.close();

Alternativement, Hibernate fournit une API orientée commande qui peut être utilisée avec des flux de données vers 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 cycle 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 (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 outrepassent le modèle d'événements de Hibernate et les intercepteurs. Les sessions sans état sont vulnérables aux effets de réplication 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 instances 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 respectivement. Ainsi, elles ont des sémantiques très différentes des opérations save(), saveOrUpdate() et delete() définies par l'interface Session.

As already discussed, automatic and transparent object/relational mapping is concerned with the management of the object state. The object state is available in memory. This means that manipulating data directly in the database (using the SQL Data Manipulation Language (DML) the statements: INSERT, UPDATE, DELETE) will not affect in-memory state. However, Hibernate provides methods for bulk SQL-style DML statement execution that is performed through the Hibernate Query Language (HQL).

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

Certains points à 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 utilisant un alias ; si le nom de l'entité n'a pas d'alias, il sera illégal pour n'importe quelle référence de propriété d'être qualifiée.

  • No joins, either implicit or explicit, can be specified in a bulk HQL query. Sub-queries can be used in the where-clause, where the subqueries themselves may contain joins.

  • La clause where- est aussi optionnelle.

Par exemple, pour exécuter un HQL UPDATE, utilisez la méthode Query.executeUpdate() (la méthode est données pour ceux qui connaissent 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";
// or 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();

In keeping with the EJB3 specification, HQL UPDATE statements, by default, do not effect the version or the timestamp property values for the affected entities. However, you can force Hibernate to reset the version or timestamp property values through the use of a versioned update. This is achieved by adding the VERSIONED keyword after the UPDATE keyword.

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 permis en conjonction avec la déclaration update versioned.

Pour exécuter un HQL DELETE, utilisez la même méthode Query.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 non, corréler le nombre de lignes affectées 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 (join-subclass), par exemple. Le nombre retourné indique le nombre d'entités réelles affectées par l'expression. Si on revient à 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 à laquelle la classe fille est mappée, mais également 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 column speficiation dans la déclaration 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 termes, les expressions INSERT sont 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, cela est vérifié durant la compilation de la requête plutôt que de reléguer la vérification à la base de données. Notez cependant que cela pourrait poser des problèmes entre les Types de Hibernate qui sont équivalents contrairement à é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, bien que la base de données ne fasse pas de distinction ou ne soit pas capable de gérer la conversion.

  • Pour la propriété 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 si vous utilisez 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 comme 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, qui ne peut pas être utilisée parce qu'elle ne propose pas de moyen d'obtenir 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 seed value définie par le org.hibernate.type.VersionType est utilisée).

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

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();