Chapitre 15. Requêtes par critères

Hibernate offre une API d'interrogation par critères intuitive et extensible.

15.1. Créer une instance de Criteria

L'interface net.sf.hibernate.Criteria représente une requête sur une classe persistente donnée. La Session fournit les instances de Criteria.

Criteria crit = sess.createCriteria(Cat.class);
crit.setMaxResults(50);
List cats = crit.list();

15.2. Restriction du résultat

Un criterion (critère de recherche) est une instance de l'interface org.hibernate.criterion.Criterion. La classe org.hibernate.criterion.Restrictions définit des méthodes pour obtenir des types de Criterion pré-définis.

List cats = sess.createCriteria(Cat.class)
    .add( Restrictions.like("name", "Fritz%") )
    .add( Restrictions.between("weight", minWeight, maxWeight) )
    .list();

Les restrictions peuvent être goupées de manière logique.

List cats = sess.createCriteria(Cat.class)
    .add( Restrictions.like("name", "Fritz%") )
    .add( Restrictions.or(
        Restrictions.eq( "age", new Integer(0) ),
        Restrictions.isNull("age")
    ) )
    .list();
List cats = sess.createCriteria(Cat.class)
    .add( Restrictions.in( "name", new String[] { "Fritz", "Izi", "Pk" } ) )
    .add( Restrictions.disjunction()
        .add( Restrictions.isNull("age") )
        .add( Restrictions.eq("age", new Integer(0) ) )
        .add( Restrictions.eq("age", new Integer(1) ) )
        .add( Restrictions.eq("age", new Integer(2) ) )
    ) )
    .list();

Il y a plusieurs types de criterion pré-définis (sous classes de Restriction), mais l'une d'entre elle particulièrement utile vous permet de spécifier directement du SQL.

List cats = sess.createCriteria(Cat.class)
    .add( Restrictions.sql("lower({alias}.name) like lower(?)", "Fritz%", Hibernate.STRING) )
    .list();

La zone {alias} sera remplacée par l'alias de colonne de l'entité que l'on souhaite intérroger.

Une autre approche pour obtenir un criterion est de le récupérer d'une instance de Property. Vous pouvez créer une Property en appelant Property.forName().

Property age = Property.forName("age");
List cats = sess.createCriteria(Cat.class)
    .add( Restrictions.disjunction()
        .add( age.isNull() )
        .add( age.eq( new Integer(0) ) )
        .add( age.eq( new Integer(1) ) )
        .add( age.eq( new Integer(2) ) )
    ) )
    .add( Property.forName("name").in( new String[] { "Fritz", "Izi", "Pk" } ) )
    .list();

15.3. Trier les résultats

Vous pouvez trier les résultats en utilisant org.hibernate.criterion.Order.

List cats = sess.createCriteria(Cat.class)
    .add( Restrictions.like("name", "F%")
    .addOrder( Order.asc("name") )
    .addOrder( Order.desc("age") )
    .setMaxResults(50)
    .list();
List cats = sess.createCriteria(Cat.class)
    .add( Property.forName("name").like("F%") )
    .addOrder( Property.forName("name").asc() )
    .addOrder( Property.forName("age").desc() )
    .setMaxResults(50)
    .list();

15.4. Associations

Vous pouvez facilement spécifier des contraintes sur des entités liées, par des associations en utilisant createCriteria().

List cats = sess.createCriteria(Cat.class)
    .add( Restrictions.like("name", "F%")
    .createCriteria("kittens")
        .add( Restrictions.like("name", "F%")
    .list();

Notez que la seconde createCriteria() retourne une nouvelle instance de Criteria, qui se rapporte aux éléments de la collection kittens.

La forme alternative suivante est utile dans certains cas.

List cats = sess.createCriteria(Cat.class)
    .createAlias("kittens", "kt")
    .createAlias("mate", "mt")
    .add( Restrictions.eqProperty("kt.name", "mt.name") )
    .list();

(createAlias() ne crée pas de nouvelle instance de Criteria.)

Notez que les collections kittens contenues dans les instances de Cat retournées par les deux précédentes requêtes ne sont pas pré-filtrées par les critères ! Si vous souhaitez récupérer uniquement les kittens qui correspondent à la criteria, vous devez utiliser ResultTransformer.

List cats = sess.createCriteria(Cat.class)
    .createCriteria("kittens", "kt")
        .add( Restrictions.eq("name", "F%") )
    .setResultTransformer(Criteria.ALIAS_TO_ENTITY_MAP)
    .list();
Iterator iter = cats.iterator();
while ( iter.hasNext() ) {
    Map map = (Map) iter.next();
    Cat cat = (Cat) map.get(Criteria.ROOT_ALIAS);
    Cat kitten = (Cat) map.get("kt");
}

15.5. Peuplement d'associations de manière dynamique

Vous pouvez spéficier au moment de l'exécution le peuplement d'une association en utilisant setFetchMode() (c'est-à-dire le chargement de celle-ci). Cela permet de surcharger les valeurs "lazy" et "outer-join" du mapping.

List cats = sess.createCriteria(Cat.class)
    .add( Restrictions.like("name", "Fritz%") )
    .setFetchMode("mate", FetchMode.EAGER)
    .setFetchMode("kittens", FetchMode.EAGER)
    .list();

Cette requête recherchera mate et kittens via les jointures externes. Voir Section 19.1, « Stratégies de chargement » pour plus d'informations.

15.6. Requêtes par l'exemple

La classe org.hibernate.criterion.Example vous permet de construire un critère suivant une instance d'objet donnée.

Cat cat = new Cat();
cat.setSex('F');
cat.setColor(Color.BLACK);
List results = session.createCriteria(Cat.class)
    .add( Example.create(cat) )
    .list();

Les propriétés de type version, identifiant et association sont ignorées. Par défaut, les valeurs null sont exclues.

Vous pouvez ajuster la stratégie d'utilisation de valeurs de l'Exemple.

Example example = Example.create(cat)
    .excludeZeroes()           //exclude zero valued properties
    .excludeProperty("color")  //exclude the property named "color"
    .ignoreCase()              //perform case insensitive string comparisons
    .enableLike();             //use like for string comparisons
List results = session.createCriteria(Cat.class)
    .add(example)
    .list();

Vous pouvez utiliser les "exemples" pour des critères sur les objets associés.

List results = session.createCriteria(Cat.class)
    .add( Example.create(cat) )
    .createCriteria("mate")
        .add( Example.create( cat.getMate() ) )
    .list();

15.7. Projections, agrégation et regroupement

La classe org.hibernate.criterion.Projections est une fabrique d'instances de Projection. Nous appliquons une projection sur une requête en appelant setProjection().

List results = session.createCriteria(Cat.class)
    .setProjection( Projections.rowCount() )
    .add( Restrictions.eq("color", Color.BLACK) )
    .list();
List results = session.createCriteria(Cat.class)
    .setProjection( Projections.projectionList()
        .add( Projections.rowCount() )
        .add( Projections.avg("weight") )
        .add( Projections.max("weight") )
        .add( Projections.groupProperty("color") )
    )
    .list();

Il n'y a pas besoin de "group by" explicite dans une requête par critère. Certains types de projection sont définis pour être des projections de regroupement, lesquels apparaissent aussi dans la clause group by SQL.

Un alias peut optionnellement être assigné à une projection, ainsi la valeur projetée peut être référencée dans des restrictions ou des tris. Voici deux façons différentes de faire ça :

List results = session.createCriteria(Cat.class)
    .setProjection( Projections.alias( Projections.groupProperty("color"), "colr" ) )
    .addOrder( Order.asc("colr") )
    .list();
List results = session.createCriteria(Cat.class)
    .setProjection( Projections.groupProperty("color").as("colr") )
    .addOrder( Order.asc("colr") )
    .list();

Les méthodes alias() et as() enveloppe simplement une instance de projection dans une autre instance (aliasée) de Projection. Comme un raccourci, vous pouvez assignez un alias lorsque vous ajoutez la projection à la liste de projections :

List results = session.createCriteria(Cat.class)
    .setProjection( Projections.projectionList()
        .add( Projections.rowCount(), "catCountByColor" )
        .add( Projections.avg("weight"), "avgWeight" )
        .add( Projections.max("weight"), "maxWeight" )
        .add( Projections.groupProperty("color"), "color" )
    )
    .addOrder( Order.desc("catCountByColor") )
    .addOrder( Order.desc("avgWeight") )
    .list();
List results = session.createCriteria(Domestic.class, "cat")
    .createAlias("kittens", "kit")
    .setProjection( Projections.projectionList()
        .add( Projections.property("cat.name"), "catName" )
        .add( Projections.property("kit.name"), "kitName" )
    )
    .addOrder( Order.asc("catName") )
    .addOrder( Order.asc("kitName") )
    .list();

Vous pouvez aussi utiliser Property.forName() pour formuler des projections :

List results = session.createCriteria(Cat.class)
    .setProjection( Property.forName("name") )
    .add( Property.forName("color").eq(Color.BLACK) )
    .list();
List results = session.createCriteria(Cat.class)
    .setProjection( Projections.projectionList()
        .add( Projections.rowCount().as("catCountByColor") )
        .add( Property.forName("weight").avg().as("avgWeight") )
        .add( Property.forName("weight").max().as("maxWeight") )
        .add( Property.forName("color").group().as("color" )
    )
    .addOrder( Order.desc("catCountByColor") )
    .addOrder( Order.desc("avgWeight") )
    .list();

15.8. Requêtes et sous-requêtes détachées

La classe DetachedCriteria vous laisse créer une requête en dehors de la portée de la session, et puis l'exécuter plus tard en utilisant n'importe quelle Session arbitraire.

DetachedCriteria query = DetachedCriteria.forClass(Cat.class)
    .add( Property.forName("sex").eq('F') );

Session session = ....;
Transaction txn = session.beginTransaction();
List results = query.getExecutableCriteria(session).setMaxResults(100).list();
txn.commit();
session.close();

Une DetachedCriteria peut aussi être utilisée pour exprimer une sous-requête. Des instances de criterion impliquant des sous-requêtes peuvent être obtenues via Subqueries ou Property.

DetachedCriteria avgWeight = DetachedCriteria.forClass(Cat.class)
    .setProjection( Property.forName("weight").avg() );
session.createCriteria(Cat.class)
    .add( Property.forName("weight).gt(avgWeight) )
    .list();
DetachedCriteria weights = DetachedCriteria.forClass(Cat.class)
    .setProjection( Property.forName("weight") );
session.createCriteria(Cat.class)
    .add( Subqueries.geAll("weight", weights) )
    .list();

Même des requêtes corrélées sont possibles :

DetachedCriteria avgWeightForSex = DetachedCriteria.forClass(Cat.class, "cat2")
    .setProjection( Property.forName("weight").avg() )
    .add( Property.forName("cat2.sex").eqProperty("cat.sex") );
session.createCriteria(Cat.class, "cat")
    .add( Property.forName("weight).gt(avgWeightForSex) )
    .list();

15.9. Requêtes par identifiant naturel

Pour la plupart des requêtes, incluant les requêtes par critère, le cache de requêtes n'est pas très efficace, parce que l'invalidation du cache de requêtes arrive trop souvent. Cependant, il y a une sorte spéciale de requête où nous pouvons optimiser l'algorithme d'invalidation du cache : les recherches sur une clef naturelle constante. Dans certaines applications, cette sorte de requête se produit fréquemment. L'API de critère fournit une provision spéciale pour ce cas d'utilisation.

D'abord vous devriez mapper la clef naturelle de votre entité en utilisant <natural-id>, et activer l'utilisation du cache de second niveau.

<class name="User">
    <cache usage="read-write"/>
    <id name="id">
        <generator class="increment"/>
    </id>
    <natural-id>
        <property name="name"/>
        <property name="org"/>
    </natural-id>
    <property name="password"/>
</class>

Notez que cette fonctionnalité n'est pas prévue pour l'utilisation avec des entités avec des clefs naturelles mutables.

Ensuite, activez le cache de requête d'Hibernate.

Maintenant Restrictions.naturalId() nous permet de rendre l'utilisation de l'algorithme de cache plus efficace.

session.createCriteria(User.class)
    .add( Restrictions.naturalId()
        .set("name", "gavin")
        .set("org", "hb")
    ).setCacheable(true)
    .uniqueResult();