Chapitre 9. Manipuler les données persistantes

9.1. Création d'un objet persistant

Un objet (une instance entité) est transiant ou persistant pour une Session donnée. Les objets nouvellement instanciés sont bien sûr transiants. La session offre les services de sauvegarde (de persistence) des instances transiantes :

DomesticCat fritz = new DomesticCat();
fritz.setColor(Color.GINGER);
fritz.setSex('M');
fritz.setName("Fritz");
Long generatedId = (Long) sess.save(fritz);
DomesticCat pk = new DomesticCat();
pk.setColor(Color.TABBY);
pk.setSex('F');
pk.setName("PK");
pk.setKittens( new HashSet() );
pk.addKitten(fritz);
sess.save( pk, new Long(1234) );

save() avec un seul argument, génère et assigne un identifiant unique à fritz. La même méthode avec deux arguments essaie de persister pk en utilisant l'identifiant donné. Généralement, nous vous déconseillons l'utilisation de la version à deux arguments puisqu'elle pourrait être utilisée pour créer des clés primaires avec une signification métier. Elle est plus efficace, dans certaines situations, comme l'utilisation d'Hibernate pour la persistance d'un Entity Bean BMP.

Les objets associés peuvent être persistés dans l'ordre que vous voulez du moment que vous n'avez pas de contrainte NOT NULL sur une clé étrangère. Il n'y a aucun risque de violation de contrainte de clé étrangère. Cependant, vous pourriez violer une contrainte NOT NULL si vous invoquiez save() sur des objets dans le mauvais ordre.

9.2. Chargement d'un objet

La méthode load() offerte par la Session vous permet de récupérer une instance persistante si vous connaissez son identifiant. Une des versions prend comme argument un objet class et charge l'état dans un objet nouvellement instancié. La seconde version permet d'alimenter une instance dans laquelle l'état sera chargé. La version qui prend comme argument une instance est particulèrement utile si vous pensez utiliser Hibernate avec des Entity Bean BMP, elle est fournie dans ce but. Vous découvrirez d'autres cas où l'utiliser (Pooling d'instance maison, etc.)

Cat fritz = (Cat) sess.load(Cat.class, generatedId);
// il est nécessaire de transformer les identifiants primitifs
long pkId = 1234;
DomesticCat pk = (DomesticCat) sess.load( Cat.class, new Long(pkId) );
Cat cat = new DomesticCat();
// charge l'état de pk dans cat
sess.load( cat, new Long(pkId) );
Set kittens = cat.getKittens();

Il est à noter que load() lèvera une exception irréversible s'il ne trouve pas d'enregistrement correspondant en base données. Si la classe est mappée avec un proxy, load() retourne un objet qui est un proxy non initialisé et n'interrogera la base de données qu'à la première invocation d'une méthode de l'objet. Ce comportement est très utile si vous voulez créer une association vers un objet sans réellement le charger depuis la base de données.

Si vous n'êtes par certain que l'enregistrement correspondant existe, vous devriez utiliser la méthode get(), qui interroge immédiatement la base de données et retourne null s'il n'y a aucun enregistrement correspondant.

Cat cat = (Cat) sess.get(Cat.class, id);
if (cat==null) {
    cat = new Cat();
    sess.save(cat, id);
}
return cat;

Vous pouvez aussi charger un objet en utilisant un ordre SQL de type SELECT ... FOR UPDATE. Référez vous à la section suivante pour une présentation des LockModes d'Hibernate.

Cat cat = (Cat) sess.get(Cat.class, id, LockMode.UPGRADE);

Notez que les instances associées ou collections contenues ne sont pas selectionnées en utilisant "FOR UPDATE".

Il est possible de recharger un objet et toutes ses collections à n'importe quel moment en utilisant la méthode refresh(). Ceci est utile quand les triggers d'une base de données sont utilisés pour initialiser certaines propriétés de l'objet.

sess.save(cat);
sess.flush(); //force l'ordre SQL INSERT
sess.refresh(cat); //recharge l'état (après exécution des triggers)

9.3. Requêtage

Si vous ne connaissez pas le(s) identifiant(s) de l'objet (ou des objets) que vous recherchez, utlisez la méthode find() offerte par la Session. Hibernate s'appuie sur un langage d'interrogation, orienté objet, simple mais puissant.

List cats = sess.find(
    "from Cat as cat where cat.birthdate = ?",
    date,
    Hibernate.DATE
);

List mates = sess.find(
    "select mate from Cat as cat join cat.mate as mate " +
    "where cat.name = ?",
    name,
    Hibernate.STRING
);

List cats = sess.find( "from Cat as cat where cat.mate.bithdate is null" );

List moreCats = sess.find(
    "from Cat as cat where " + 
    "cat.name = 'Fritz' or cat.id = ? or cat.id = ?",
    new Object[] { id1, id2 },
    new Type[] { Hibernate.LONG, Hibernate.LONG }
);

List mates = sess.find(
    "from Cat as cat where cat.mate = ?",
    izi,
    Hibernate.entity(Cat.class)
);

List problems = sess.find(
    "from GoldFish as fish " +
    "where fish.birthday > fish.deceased or fish.birthday is null"
);

Le second argument de find() est un objet ou un tableau d'objets. Le troisième argument est un type Hibernate ou un tableau de types Hibernate. Ces types passés en argument sont utilisés pour lier les objets passés en argument au ? de la requête (ce qui correspond aux IN parameters d'un PreparedStatement JDBC). Comme en JDBC, il est préférable d'utiliser ce mécanisme de liaison (binding) plutôt que la manipulation de chaîne de caractères.

La classe Hibernate définit un certain nombre de méthodes statiques et de constantes, proposant l'accès à la plupart des types utilisés, comme les instances de net.sf.hibernate.type.Type.

Si vous pensez que votre requête retournera un très grand nombre d'objets, mais que vous n'avez pas l'intention de tous les utliser, vous pourriez améliorer les performances en utilisant la méthode iterate(), qui retourne un java.util.Iterator. L'itérateur chargera les objets à la demande en utilisant les identifiants retounés par la requête SQL initiale (ce qui fait un total de n+1 selects).

// itération sur les ids
Iterator iter = sess.iterate("from eg.Qux q order by q.likeliness"); 
while ( iter.hasNext() ) {
    Qux qux = (Qux) iter.next();  // récupération de l'objet
    // condition non définissable dans la requête
    if ( qux.calculateComplicatedAlgorithm() ) {
        // effacez l'instance en cours
        iter.remove();
        // n'est plus nécessaire pour faire le reste du process
        break;
    }
}

Malheureusement, java.util.Iterator ne déclare aucune exception, donc les exceptions SQL ou Hibernate qui seront soulevées seront transformées en LazyInitializationException (une classe fille de RuntimeException).

La méthode iterate() est également plus performante si vous prévoyez que beaucoup d'objets soient déjà chargés et donc disponibles via la session, ou si le résultat de la requête retourne très souvent les mêmes objets (quand les données ne sont pas en cache et ne sont pas dupliqués dans le résultat, find() est presque toujours plus rapide). Voici un exemple de requête qui devrait être appelée via la méthode iterate() :

Iterator iter = sess.iterate(
    "select customer, product " + 
    "from Customer customer, " +
    "Product product " +
    "join customer.purchases purchase " +
    "where product = purchase.product"
);

Invoquer la requête précédente avec find() retournerait un ResultSet JDBC très volumineux et contenant plusieurs fois les mêmes données.

Les requêtes Hibernate retournent parfois des tuples d'objets, dans ce cas chaque tuple est retourné sous forme de tableau (d'objets) :

Iterator foosAndBars = sess.iterate(
    "select foo, bar from Foo foo, Bar bar " +
    "where bar.date = foo.date"
);
while ( foosAndBars.hasNext() ) {
    Object[] tuple = (Object[]) foosAndBars.next();
    Foo foo = (Foo) tuple[0]; Bar bar = (Bar) tuple[1];
    ....
}

9.3.1. Requêtes scalaires

Les requêtes peuvent spécifier une propriété d'une classe dans la clause select. Elles peuvent même appeler les fonctions SQL d'aggrégation. Ces propriétés ou aggrégations sont considérées comme des résultats "scalaires".

Iterator results = sess.iterate(
        "select cat.color, min(cat.birthdate), count(cat) from Cat cat " +
        "group by cat.color"
);
while ( results.hasNext() ) {
    Object[] row = results.next();
    Color type = (Color) row[0];
    Date oldest = (Date) row[1];
    Integer count = (Integer) row[2];
    .....
}
Iterator iter = sess.iterate(
    "select cat.type, cat.birthdate, cat.name from DomesticCat cat"
);
List list = sess.find(
    "select cat, cat.mate.name from DomesticCat cat"
);

9.3.2. L'interface de requêtage Query

Si vous avez besoin de définir des limites sur le résultat d'une requête (nombre maximum d'enregistrements et / ou l'indice du premier résultat que vous souhaitez récupérer), utilisez une instance de net.sf.hibernate.Query :

Query q = sess.createQuery("from DomesticCat cat");
q.setFirstResult(20);
q.setMaxResults(10);
List cats = q.list();

Vous pouvez même définir une requête nommée dans le document de mapping. N'oubliez pas qu'il faut utiliser une section CDATA si votre requête contient des caractères qui pourraient être interprétés comme un marqueur XML.

<query name="eg.DomesticCat.by.name.and.minimum.weight"><![CDATA[
    from eg.DomesticCat as cat
        where cat.name = ?
        and cat.weight > ?
] ]></query>
Query q = sess.getNamedQuery("eg.DomesticCat.by.name.and.minimum.weight");
q.setString(0, name);
q.setInt(1, minWeight);
List cats = q.list();

L'interface d'interrogation supporte l'utilisation de paramètres nommés. Les paramètres nommés sont des variables de la forme :name que l'on peut retrouver dans la requête. Query dispose de méthodes pour lier des valeurs à ces paramètres nommés ou aux paramètres ? du style JDBC. Contrairement à JDBC, l'indice des paramètres Hibernate démarre de zéro. Les avantages des paramètres nommés sont :

  • les paramètres nommés sont indépendants de l'ordre dans lequel ils apparaissent dans la requête

  • ils peuvent être présents plusieurs fois dans une même requête

  • ils sont auto-documentés (par leur nom)

//paramètre nommé (préféré)
Query q = sess.createQuery("from DomesticCat cat where cat.name = :name");
q.setString("name", "Fritz");
Iterator cats = q.iterate();
//paramètre positionné
Query q = sess.createQuery("from DomesticCat cat where cat.name = ?");
q.setString(0, "Izi");
Iterator cats = q.iterate();
//paramètre nommé liste
List names = new ArrayList();
names.add("Izi");
names.add("Fritz");
Query q = sess.createQuery("from DomesticCat cat where cat.name in (:namesList)");
q.setParameterList("namesList", names);
List cats = q.list();

9.3.3. Iteration scrollable

Si votre driver JDBC supporte les ResultSets scrollables, l'interface Query peut être utilisée pour obtenir des ScrollableResults qui permettent une navigation plus flexible sur les résultats.

Query q = sess.createQuery("select cat.name, cat from DomesticCat cat " +
                            "order by cat.name");
ScrollableResults cats = q.scroll();
if ( cats.first() ) {

    // cherche le premier 'name' de chaque page pour une liste de 'cats' triée par 'name'
    firstNamesOfPages = new ArrayList();
    do {
        String name = cats.getString(0);
        firstNamesOfPages.add(name);
    }
    while ( cats.scroll(PAGE_SIZE) );

    // Retourne la première page de 'cats'
    pageOfCats = new ArrayList();
    cats.beforeFirst();
    int i=0;
    while( ( PAGE_SIZE > i++ ) && cats.next() ) pageOfCats.add( cats.get(1) );

}

Le comportement de scroll() est similaire à celui d'iterate(), à la différence près que les objets peuvent être initialisés de manière sélective avec get(int), au lieu d'une initialisation complète d'une ligne de resultset.

9.3.4. Filtrer les collections

Un filtre (filter) de collection est un type spécial de requête qui peut être appliqué à une collection ou un tableau persistant. La requête peut faire référence à this, ce qui signifie "l'élément de la collection courante".

Collection blackKittens = session.filter(
    pk.getKittens(), "where this.color = ?", Color.BLACK, Hibernate.enum(Color.class)
);

La collection retournée est considérée comme un bag.

Remarquez que les filtres n'ont pas besoin de clause from (bien qu'ils puissent en avoir une si nécessaire). Les filtres ne sont pas limités à retourner des éléments de la collection qu'ils filtrent.

Collection blackKittenMates = session.filter(
    pk.getKittens(), "select this.mate where this.color = eg.Color.BLACK"
);

9.3.5. Les requêtes par critères

HQL est extrêmement puissant mais certaines personnnes préfèreront construire leurs requêtes dynamiquement, en utilisant une API orientée objet, plutôt qu'une chaîne de caractères dans leur code JAVA. Pour ces personnes, Hibernate fournit Criteria : une API d'interrogation intuitive.

Criteria crit = session.createCriteria(Cat.class);
crit.add( Expression.eq("color", eg.Color.BLACK) );
crit.setMaxResults(10);
List cats = crit.list();

Si vous n'êtes pas à l'aise avec les syntaxes type SQL, c'est peut être la manière la plus simple de commencer avec Hibernate. Cette API est aussi plus extensible que le HQL. Les applications peuvent s'appuyer sur leur propre implémentation de l'interface Criterion.

9.3.6. Requêtes en SQL natif

Vous pouvez construire votre requête en SQL, en utilisant createSQLQuery(). Il est nécessaire de placer vos alias SQL entre accolades.

List cats = session.createSQLQuery(
    "SELECT {cat.*} FROM CAT AS {cat} WHERE ROWNUM<10", 
    "cat",
    Cat.class
).list();
List cats = session.createSQLQuery(
    "SELECT {cat}.ID AS {cat.id}, {cat}.SEX AS {cat.sex}, " +
           "{cat}.MATE AS {cat.mate}, {cat}.SUBCLASS AS {cat.class}, ... " +
    "FROM CAT AS {cat} WHERE ROWNUM<10", 
    "cat",
    Cat.class
).list()

Les requêtes SQL peuvent contenir des paramètres nommés et positionnés, comme dans les requêtes Hibernate.

9.4. Mise à jour des objets

9.4.1. Mise à jour dans la même session

Les instances transactionnelles persistantes (objets chargés, sauvegardés, créés ou résultats d'une recherche par la Session) peuvent être manipulées par l'application. Toute modification sur un état persistant sera sauvegardée (persistée) quand la Session sera flushée (ceci sera décrit plus tard dans ce chapitre). Le moyen le plus simple de modifier l'état d'un objet est donc de le charger (load()), et de le manipuler pendant que la Session est ouverte :

DomesticCat cat = (DomesticCat) sess.load( Cat.class, new Long(69) );
cat.setName("PK");
sess.flush();  // les modifications de 'cat' sont automatiquement détectées et sauvegardées

Il arrive que cette approche ne convienne pas puisqu'elle nécessite une même session pour exécuter les deux ordres SQL SELECT (pour charger l'objet) et UPDATE (pour sauvegarder son état mis à jour) dans la même session. Hibernate propose une méthode alternative.

9.4.2. Mise à jour d'objets détachés

Certaines applications ont besoin de récupérer un objet dans une transaction, de le passer ensuite à la couche de présentation pour modification, et enfin de le sauvegarder dans une nouvelle transaction (les applications suivant cette approche se trouvent dans un contexte d'accès aux données hautement concurrent, elles utilisent généralement des données versionnées pour assurer l'isolation des transactions). Cette approche nécessite un modèle de développement légèrement différent de celui décrit dans la section précedente. Hibernate supporte ce modèle en proposant la méthode Session.update().

// dans la première session
Cat cat = (Cat) firstSession.load(Cat.class, catId);
Cat potentialMate = new Cat();
firstSession.save(potentialMate);

// dans une couche supérieure de l'application
cat.setMate(potentialMate);

// plus tard, dans une nouvelle session
secondSession.update(cat);  // mise à jour de 'cat'
secondSession.update(mate); // mise à jour de  'mate'

Si Cat avec l'identifiant catId avait déja été chargé par secondSession au moment où l'application essaie de le mettre à jour, une exception aurait été soulevée.

L'application devrait unitairement mettre à jour (update()) les instances transiantes accessibles depuis l'instance transiante donnée si et seulement si elle souhaite que leur état soit aussi mis à jour (A l'exception des objets engagés dans un cycle de vie dont nous parlerons plus tard).

Les utilisateurs d'Hibernate ont émis le souhait de pouvoir soit sauvegarder une instance transiante en générant un nouvel identifiant, soit mettre à jour son état en utilisant son identifiant courant. La méthode saveOrUpdate() implémente cette fonctionnalité.

Hibernate distingue les "nouvelles" (non sauvegardées) instances, des instances existantes (sauvegardées ou chargées dans une session précédente) grâce à la valeur de leur propriété d'identifiant (ou de version, ou de timestamp). L'attribut unsaved-value du mapping <id> (ou <version>, ou <timestamp>) spécifie quelle valeur doit être interprétée comme représentant une "nouvelle" instance.

<id name="id" type="long" column="uid" unsaved-value="null">
    <generator class="hilo"/>
</id>

Les valeurs permises pour unsaved-value sont :

  • any - toujours sauvegarder (save)

  • none - toujours mettre à jour (update)

  • null - sauvegarder (save) quand l'identifiant est nul (valeur par défaut)

  • une valeur valide pour l'identifiant - sauvegarder (save) quand l'identifiant est nul ou égal à cette valeur

  • undefined - par défaut pour version ou timestamp, le contrôle sur l'identifiant est alors utilisé

// dans la première session
Cat cat = (Cat) firstSession.load(Cat.class, catID);

// dans une couche supérieure de l'application
Cat mate = new Cat();
cat.setMate(mate);

// plus tard, dans une nouvelle session
secondSession.saveOrUpdate(cat);   // mise à jour de l'état existant (cat a un id non null)
secondSession.saveOrUpdate(mate);  // sauvegarde d'une nouvelle instance (mate a un id null)

L'utilisation et la sémantique de saveOrUpdate() semble confuse pour les nouveaux utilisateurs. Tout d'abord, tant que vous n'essayez pas d'utiliser une instance d'une session dans une autre session, il est inutile d'utiliser update() ou saveOrUpdate(). De pan entiers d'applications n'utiliseront aucune de ces deux méthodes.

Généralement update() ou saveOrUpdate() sont utilisés dans les scénarii suivant:

  • l'application charge un objet dans une première session

  • l'objet est passé à la couche UI

  • l'objet subit quelques modificatons

  • l'objet redescend vers la couche métier

  • l'application persiste ces modifications en appelant update() dans une seconde session

saveOrUpdate() réalise ce qui suit :

  • si l'objet est déjà persistant dans la session en cours, ne fait rien

  • si l'objet n'a pas d'identifiant, elle le save()

  • si l'identifiant de l'objet corresponds au critère défini par unsaved-value, elle le save()

  • si l'objet est versionné (version ou timestamp), alors la version est vérifiée en priorité sur l'identifiant, sauf si unsaved-value="undefined" (valeur par défaut) est utilisé pour la version

  • si un autre objet associé à la session a le même identifiant, une exception est soulevée

9.4.3. Réassocier des objets détachés

La méthode lock() permet à l'application de réassocier un objet non modifié avec une nouvelle session.

//simple réassociation :
sess.lock(fritz, LockMode.NONE);
//vérifie la version, puis ré associe :
sess.lock(izi, LockMode.READ);
//vérifie la version en utilisant SELECT ... FOR UPDATE, puis réassocie :
sess.lock(pk, LockMode.UPGRADE);

9.5. Suppression d'objets persistants

Session.delete() supprimera l'état d'un objet de la base de données. Evidemment, votre application peut toujours contenir une référence à cet objet. La meilleure façon de l'apréhender est donc de se dire que delete() transforme une instance persistante en instance transiante.

sess.delete(cat);

Vous pouvez aussi effacer plusieurs objets en passant une requête Hibernante à la méthode delete().

Vous pouvez supprimer les objets dans l'ordre que vous souhaitez, sans risque de violer une contrainte de clé étrangère. Cependant, vous pourriez violer une contrainte NOT NULL si vous invoquiez delete() sur des objets dans le mauvais ordre.

9.6. Flush

La Session exécute parfois les ordres SQL nécessaires pour synchroniser l'état de la connexion JDBC avec l'état des objets contenus en mémoire. Ce processus, flush, se déclenche :

  • à certaines invocations de find() ou d'iterate()

  • à l'appel de net.sf.hibernate.Transaction.commit()

  • à l'appel de Session.flush()

Les ordres SQL sont exécutés dans cet ordre :

  1. toutes les insertions d'entités, dans le même ordre que celui utilisé pour la sauvegarde (Session.save()) des objets correpsondants

  2. toutes les mises à jour d'entités

  3. toutes les suppressions de collection

  4. toutes les suppressions, insertions, mises à jour d'éléments de collection

  5. toutes les insertions de collection

  6. toutes les suppressions d'entités, dans le même ordre que celui utilisé pour la suppression (Session.delete()) des objets correpsondants

(Une exception existe pour les objets utilisant les générations d'ID native puisqu'ils sont insérés quand ils sont sauvegardés).

A moins d'appeler explicitement flush(), il n'y a aucune garantie sur le moment la Session exécute les appels JDBC, seul l'ordre dans lequel ils sont appelés est garanti. Cependant, Hibernate garantit que les méthodes Session.find(..) ne retourneront jamais de données périmées ; ni de données érronées.

Il est possible de changer les comportements par défaut pour que le flush s'exécute moins fréquement. La classe FlushMode définit trois modeds différents. Ceci est utile pour des transactions en lecture seule où il peut être utlisé pour accroître (très) légèrement les performances.

sess = sf.openSession();
Transaction tx = sess.beginTransaction();
sess.setFlushMode(FlushMode.COMMIT); //autorise les requêtes à retourner des données corrompues
Cat izi = (Cat) sess.load(Cat.class, id);
izi.setName(iznizi);
// exécute quelques requêtes....
sess.find("from Cat as cat left outer join cat.kittens kitten");
//les modifications de 'izi' ne sont sont pas flushées!
...
tx.commit(); //flush s'exécute

9.7. Terminer une Session

La fin d'une session implique quatre phases :

  • flush de la session

  • commit de la transaction

  • fermeture de la session

  • traitement des exceptions

9.7.1. Flusher la Session

Si vous utilisez l'API Transaction, vous n'avez pas à vous soucier de cette étape. Elle sera automatiquement réalisée à l'appel du commit de la transaction. Autrement, vous devez invoquer Session.flush() pour vous assurer que les changements sont synchronisés avec la base de données.

9.7.2. Commit de la transaction de la base de données

Si vous utilisez l'API Transaction d'Hibernate, cela donne :

tx.commit(); // flush la Session et commit la transaction

Si vous gérez vous-même les transactions JDBC, vous devez manuellement appeler la méthode commit() de la connexion JDBC.

sess.flush();
sess.connection().commit();  // pas nécessaire pour une datasource JTA

Si vous décidez de ne pas committer vos modifications :

tx.rollback();  // rollback la transaction

ou :

// pas nécessaire pour une datasource JTA mais important dans le cas contraire 
sess.connection().rollback();

Si vous faites un rollback d'une transaction vous devriez immédiatement la fermer et arrêter d'utiliser la session courante, ceci pour assurer l'intégrité de l'état interne d'Hibernate.

9.7.3. Fermeture de la Session

Un appel de Session.close() marque la fin d'une session. La conséquence principale de close() est que la connexion JDBC est relachée par la session.

tx.commit();
sess.close();
sess.flush();
sess.connection().commit();  // pas nécessaire pour une datasource JTA
sess.close();

Si vous gérez vous même votre connexion, close() retourne une référence à cette connexion, vous pouvez ainsi la fermer manuellement ou la rendre au pool. Si ce n'est pas le cas close() rend la connexion au pool.

9.8. Traitement des exceptions

Hibernate, au cours de son utilisation, peut lever des exceptions, généralement des hibernateExceptions. La cause éventuelle de l'exception qui peut être récupérée en utilisant getCause().

Si la Session lève une exception, vous devrez immédiatement effectuer un rollback de la transaction, appeler session.close() et ne plus utiliser l'instance courante de la Session. Certaines méthodes de Session ne laisseront pas la session dans un état consistant. Cela signifie que toutes les exceptions levées par Hibernate doivent être considérées comme fatales, vous pourriez donc envisager de convertir l'exception non runtime HibernateException en RuntimeException (la solution la plus simple est de changer la clause extends dans HibernateException.java et de recompiler le tout). Notez que le fait que l'exception ne soit pas runtime est dû à une erreur des premiers ages d'Hibernate et sera corrigée dans la prochaine version majeure.

Hibernate essaiera de convertir les SQLExceptions levées lors des interactions avec la base de données en des sous-classes de JDBCException. La SQLException sous-jacente est accessible en appelant JDBCException.getCause(). Hibernate convertit la SQLException en une sous-classe appropriée de JDBCException en s'appuyant sur le SQLExceptionConverter attaché à la SessionFactory. Par défaut, le SQLExceptionConverter utilisé est celui défini par le dialecte ; il est cependant possible d'attacher une implémentation spécifique (voir la javadoc de SQLExceptionConverterFactory pour plus de détails). Les sous-types standards de JDBCException sont :

  • JDBCConnectionException - indique une erreur lors de la communication JDBC sous-jacente.

  • SQLGrammarException - indique une erreur de grammaire ou de syntaxe du SQL envoyé.

  • ConstraintViolationException - indique une forme de violation de contrainte d'intégrité.

  • LockAcquisitionException - indique une erreur lors de l'acquisition d'un niveau de verrou requis pour exécuter l'opération demandée.

  • GenericJDBCException - indique une exception générique dont la cause ne tombe pas dans les catégories précédentes.

Comme toujours, toutes les exceptions sont considérées comme fatales à la Session et à la transaction courante. Le fait qu'Hibernate sache maintenant mieux distinguer les différents types de SQLException n'implique en aucune manière que les exceptions soient récupérables du point de vue de la Session. La hiérarchie typée des exceptions permet à l'application de mieux réagir en catégorisant la cause de l'exception plus simplement si besoin.

Il est recommandé d'effectuer le traitement des exceptions comme suit :

Session sess = factory.openSession();
Transaction tx = null;
try {
    tx = sess.beginTransaction();
    // faire qqch
    ...
    tx.commit();
}
catch (Exception e) {
    if (tx!=null) tx.rollback();
    throw e;
}
finally {
    sess.close();
}

Ou, si vous gérez les transactions JDBC manuellement :

Session sess = factory.openSession();
try {
    // faire qqch
    ...
    sess.flush();
    sess.connection().commit();
}
catch (Exception e) {
    sess.connection().rollback();
    throw e;
}
finally {
    sess.close();
}

Ou, si vous utilisez une datasource couplée avec JTA :

UserTransaction ut = .... ;
Session sess = factory.openSession();
try {
    // faire qqch
    ...
    sess.flush();
}
catch (Exception e) {
    ut.setRollbackOnly();
    throw e;
}
finally {
    sess.close();
}

N'oubliez pas qu'un serveur d'applications (dans un environnement géré par JTA) ne rollback les transactions automatiquement que pour les exceptions java.lang.RuntimeExceptions. Si une exception applicative est levée (à savoir une exception non runtime HibernateException), vous devez appeler la méthode setRollbackOnly() d'EJBContext vous-même ou, comme montré dans l'exemple précédent, l'encapsuler dans une RuntimeException (par exemple EJBException pour un rollback automatique.

9.9. Cycles de vie et graphes d'objets

Pour sauvegarder ou mettre à jour tous les objets contenus dans un graphe d'objets associés, vous pouvez soit

  • invoquer individuellement save(), saveOrUpdate() ou update() sur chaque objet OU

  • lier des objets en utilisant cascade="all" ou cascade="save-update".

De même, pour supprimer tous les objets d'un graphe, vous pouvez soit

  • invoquer individuellement delete() sur chaque objet OU

  • lier des objets en utilisant cascade="all", cascade="all-delete-orphan" or cascade="delete".

Recommandation :

  • Si la durée de vie de l'objet fils est liée à celle de l'objet père, faîtes en un objet lié au cycle de vie en spécifiant cascade="all".

  • Autrement, invoquez explicitement save() et delete() dans le code de l'application. Si vous souhaitez vraiment éviter de taper du code supplémentaire, utilisez cascade="save-update" et appeler explicitement delete().

Mapper une assocation (plusieurs-vers-une, ou une collection) avec cascade="all" définit l'association comme une relation de type parent/fils où la sauvegarde/mise à jour/suppression du parent engendre la sauvegarde/mise à jour/suppression du ou des fils. Par ailleurs, la simple référence à un fils depuis un parent persistant engendrera une sauvegarde/mise à jour de l'enfant. La métaphore est cependant incomplète. Un fils qui n'est plus référencé par son père n'est pas automatiquement supprimé, sauf dans le cas d'une association <one-to-many> mappée avec cascade="all-delete-orphan". Les définitions précises des opérations en cascade sont les suivantes :

  • Si un parent est sauvegardé, tous ces fils sont passés à saveOrUpdate()

  • Si un parent est passé à update() ou à saveOrUpdate(), tous ces fils sont passés à saveOrUpdate()

  • Si un fils transiant devient référencé par un parent persistant, il est passé à saveOrUpdate()

  • Si le parent est supprimé, tous ces enfants sont passés à delete()

  • Si un enfant transiant est déréférencé par un parent persistant, rien ne se passe (l'application devra explicitement supprimer l'enfant si nécessaire) sauf si cascade="all-delete-orphan" est positionné, dans ce cas le fils "orphelin" est supprimé

Hibernate n'implémente pas complètement la persistence par atteignabilité, ce qui aurait pour conséquence (inefficace) la garbage collection des objets persistants. Cependant, en raison de la demande, Hibernate supporte la notion de persistance d'entités lorsqu'elles sont référencées par un autre objet persistant. Les associations définies avec cascade="save-update" ont ce comportement. Si vous souhaitez utliser cette approche dans toute votre application, il est plus facile de spécifier l'attribut default-cascade de l'élément <hibernate-mapping>.

9.10. Intercepteurs

L'interface Interceptor fournit des "callbacks" de la session vers l'application permettant à l'application de consulter et / ou manipuler des propriétés d'un objet persistant avant qu'il soit sauvegardé, mis à jour, supprimé ou chargé. Une utilisation possible de cette fonctionnalité est de tracer l'accès à l'information. Par exemple, l'Interceptor qui suit va automatiquement positionner le createTimestamp quand un Auditable est créé et mettre à jour la propriété lastUpdateTimestamp quand un Auditable est modifié.

package net.sf.hibernate.test;

import java.io.Serializable;
import java.util.Date;
import java.util.Iterator;

import net.sf.hibernate.Interceptor;
import net.sf.hibernate.type.Type;

public class AuditInterceptor implements Interceptor, Serializable {

    private int updates;
    private int creates;

    public void onDelete(Object entity,
                         Serializable id,
                         Object[] state,
                         String[] propertyNames,
                         Type[] types) {
        // ne rien faire
    }

    public boolean onFlushDirty(Object entity, 
                                Serializable id, 
                                Object[] currentState,
                                Object[] previousState,
                                String[] propertyNames,
                                Type[] types) {

        if ( entity instanceof Auditable ) {
            updates++;
            for ( int i=0; i < propertyNames.length; i++ ) {
                if ( "lastUpdateTimestamp".equals( propertyNames[i] ) ) {
                    currentState[i] = new Date();
                    return true;
                }
            }
        }
        return false;
    }

    public boolean onLoad(Object entity, 
                          Serializable id,
                          Object[] state,
                          String[] propertyNames,
                          Type[] types) {
        return false;
    }

    public boolean onSave(Object entity,
                          Serializable id,
                          Object[] state,
                          String[] propertyNames,
                          Type[] types) {
        
        if ( entity instanceof Auditable ) {
            creates++;
            for ( int i=0; i<propertyNames.length; i++ ) {
                if ( "createTimestamp".equals( propertyNames[i] ) ) {
                    state[i] = new Date();
                    return true;
                }
            }
        }
        return false;
    }

    public void postFlush(Iterator entities) {
        System.out.println("Creations: " + creates + ", Updates: " + updates);
    }

    public void preFlush(Iterator entities) {
        updates=0;
        creates=0;
    }
    
    ......
    ......
    
}

L'intercepteur doit être spécifié quand la session est créée.

Session session = sf.openSession( new AuditInterceptor() );

Vous pouvez aussi activer un intercepteur pour toutes les sessions d'une SessionFactory, en utilisant la Configuration :

new Configuration().setInterceptor( new AuditInterceptor() );

9.11. API d'accès aux métadonnées

Hibernate a besoin d'un meta-modèle très riche de toutes les entités et types de valeurs. Parfois, ce modèle est très utile à l'application elle même. Par exemple, l'application peut utiliser les métadonnées d'Hibernate pour implémenter un algorithme "intelligent" de copie qui comprend quels objets doivent être copiés (valeurs de types muables) et quels objets ne peuvent l'être (valeurs de types imuables, et éventuellement les entités associées).

Hibernate expose les métadonnées au travers des interfaces ClassMetadata et CollectionMetadata et la hiérarchie de Type. Les instances des interfaces de métadonnées peuvent être obtenues depuis la SessionFactory.

Cat fritz = ......;
Long id = (Long) catMeta.getIdentifier(fritz);
ClassMetadata catMeta = sessionfactory.getClassMetadata(Cat.class);
Object[] propertyValues = catMeta.getPropertyValues(fritz);
String[] propertyNames = catMeta.getPropertyNames();
Type[] propertyTypes = catMeta.getPropertyTypes();
// retourne une Map de toutes les propriétés qui ne sont pas des collections ou des associations
// TODO: what about components?
Map namedValues = new HashMap();
for ( int i=0; i<propertyNames.length; i++ ) {
    if ( !propertyTypes[i].isEntityType() && !propertyTypes[i].isCollectionType() ) {
        namedValues.put( propertyNames[i], propertyValues[i] );
    }
}