TopiaQuery

author : Florian Desbois
contact : topia-devel@list.nuiton.org ou topia-users@list.nuiton.org
revision : $Revision$
date : $Date$

L'objet TopiaQuery permet de créer plus simplement des requêtes HQL pour éviter les concaténations complexes lors de l'utilisation de la méthode find du TopiaContext.

Chacune des parties de la requête sont indépendantes et seront concaténés au moment de l'exécution. Il est donc possible d'ajouter des éléments à la requête à n'importe quel moment de sa construction (from, select, where, order, group, ...).

La TopiaQuery peut donc être construite à un endroit et exécutée à un autre. Une même requête peut également être réutilisée pour être passée en sous-requête ou exécutée plusieurs fois à la suite avec certains légers changement de paramètres.

Modèle exemple

Ci-dessous un modèle simple utilisé pour les exemples de cette documentation.

Instantiation

La TopiaQuery nécessite obligatoirement une entité référence pour être exécutée. Cette entité correspondra à l'élément principal du FROM de la requête et si nécessaire sera ajouté automatiquement au SELECT.

Il y a plusieurs façons d'instancier la TopiaQuery :

Directement depuis un DAO

TopiaContext transaction = rootContext.beginTransaction();
BoatDAO dao = ModelDAOHelper.getBoatDAO(transaction);
TopiaQuery query = dao.createQuery();

ou depuis un topiaContext

TopiaContext transaction = rootContext.beginTransaction();
TopiaQuery query = transaction.createQuery(Boat.class, "B");

L'intérêt de passer par un DAO est de pouvoir par la suite executer la requête avec ce même DAO. Il est également possible d'utiliser des alias pour l'élément principal de la requête pour pouvoir plus facilement gérer les cas de jointure. Il suffit de préciser l'alias au moment de l'instanciation

TopiaContext transaction = rootContext.beginTransaction();
BoatDAO dao = ModelDAOHelper.getBoatDAO(transaction);
TopiaQuery query = dao.createQuery("E");

Ajout d'éléments au WHERE

Les méthodes de base nécessaires concernent l'ajout d'élements dans le WHERE de la requête. Plusieurs méthodes sont disponibles suivant les besoins pour ajouter simplement un élément au where

TopiaQuery query = boatDAO.createQuery();

// Recherche sur l'immatriculation du navire : immatriculation = 142154
query.addEquals("immatriculation", 142154);

// Recherche toutes les dates de construction < 2006
query.addWhere("buildYear", Op.LT, 2006);

// Recherche des navires ayant un nom
query.addNotNull("name");

// depuis 2.3.4
// Recherche des navires n'ayant pas de nom
query.addNull("name");

// Recherche des navires ayant une date de construction 2003, 2004 ou 2006
query.addEquals("buildYear", 2003, 2004, 2006);

// depuis 2.3.4
// Recherche entre deux dates
TopiaQuery queryContact = contactDAO.createQuery();
Calendar dateBegin = new GregorianCalendar(2010,2,3);
Calendar dateEnd = new GregorianCalendar(2010,5,6);
queryContact.addBetween("creationDate", dateBegin.getTime(), dateEnd.getTime());

// depuis 2.3.4
// Utilisation d'une sous-requête (les paramètres de la sous-requête seront
// ajoutés automatiquement à la requête principale en gérant les doublons
// (sur la valeur et la clé)).
// Le ? correspond à l'endroit ou sera injecté la requête, attention aux
// parenthèses.
queryContact.addSubQuery("boat IN elements(?)", query);

Il est fortement conseillé d'utiliser les constantes des entités pour les noms de leurs propriétés

query.addEquals(Boat.IMMATRICULATION, 142154);
query.addNotNull(Boat.NAME);
...

La TopiaQuery peut être chaînée, les méthodes permettant l'ajout d'éléments renvoient toutes la même TopiaQuery avec l'élément ajouté

TopiaContext transaction = rootContext.beginTransaction();
BoatDAO dao = ModelDAOHelper.getBoatDAO(transaction);
dao.createQuery().addEquals(Boat.IMMATRICULATION, 142154).addNotNull(Boat.NAME);

Opérateurs

Une enum interne à la TopiaQuery permet de manipuler les opérateurs nécessaires aux ajouts dans le WHERE :

  • TopiaQuery.Op.EQ : Opérateur =
  • TopiaQuery.Op.GT : Opérateur >
  • TopiaQuery.Op.GE : Opérateur >=
  • TopiaQuery.Op.LIKE : Opérateur LIKE
  • TopiaQuery.Op.LT : Opérateur <
  • TopiaQuery.Op.LE : Opérateur <=
  • TopiaQuery.Op.NOT_NULL : Opérateur IS NOT NULL
  • TopiaQuery.Op.NULL : Opérateur IS NULL
  • TopiaQuery.Op.NEQ : Opérateur !=

Autres parties de la requête

Ajout d'élément au FROM

Il est souvent nécessaire d'ajouter une autre entité au FROM de la requête, pour ce faire, il existe deux méthodes :

  • addFrom(Class entityClass) : ajoute une entité au FROM (Ex : addFrom(Contact.class);)
  • addFrom(Class entityClass, String alias) : ajoute une entité au FROM avec un alias (Ex : addFrom(Contact.class, "C");)

C'est généralement la dernière méthode qui sera la plus utilisé, l'utilisation des alias facilitant grandement les liaisons entre les entités.

Depuis la 2.3.4, deux méthodes ont été rajoutés pour le cas des jointures :

  • addJoin(Class entityClass, String alias, boolean fetch) : utilise un inner join pour lier une entité à celle de la requête. Concrètement l'opérateur JOIN Hql sera utilisé : 'FROM Contact C JOIN C.boat'

    System Message: WARNING/2 (line 179)

    Literal block expected; none found.

    TopiaContext transaction = rootContext.beginTransaction(); ContactDAO dao = ModelDAOHelper.getBoatDAO(transaction); TopiaQuery query = dao.createQuery("C").addJoin("C.boat", null, false);

  • addLeftJoin(Class entityClass, String alias, boolean fetch) : même chose que le addJoin sauf que l'opérateur LEFT JOIN Hql sera utilisé.

Voir le Chargement des donnees pour l'utilisation du fetch.

Ajout d'élément au SELECT

Deux méthodes sont disponibles pour le cas du SELECT, une méthode addSelect qui se chargera d'ajouter une propriété au SELECT, et une méthode setSelect qui définira directement quel est le SELECT souhaité. Pour le cas du addSelect, la méthode gère automatiquement l'entité principale utilisé pour instancier la requête, il n'est donc pas nécessaire de l'ajouter manuellement.

TODO : find an example

Le setSelect quant à lui est utilisé pour les aggregations par exemple ou pour récupérer des parties précises du résultat

TopiaContext transaction = rootContext.beginTransaction();
BoatDAO dao = ModelDAOHelper.getBoatDAO(transaction);
// On souhaite connaître le nombre de résultats uniquement
TopiaQuery query = dao.createQuery().
                    addNotNull(Boat.NAME).
                    setSelect("COUNT(*)");

autre exemple

// On souhaite récupérer uniquement les noms des navires
TopiaQuery query = dao.createQuery().
                    addNotNull(Boat.NAME).
                    setSelect(Boat.NAME);
Note
Pour l'ajout d'une contrainte sur l'unicité des résultats, vous pouvez utiliser la méthode addDistinct() qui permet l'ajout du mot clé DISTINCT sur le SELECT de la requête.

Ajout d'élément au GROUP BY

Dans certains cas, il peut être nécessaire d'utiliser un GROUP BY pour les aggregations

TopiaContext transaction = rootContext.beginTransaction();
ContactDAO dao = ModelDAOHelper.getContactDAO(transaction);
// On souhaite connaître le nombre de contacts par navire
TopiaQuery query = dao.createQuery().
                    setSelect("COUNT(*)").
                    addGroup(Contact.BOAT);

Ajout d'élément au ORDER BY

Il est possible également d'ajouter un ordre aux résultats

TopiaContext transaction = rootContext.beginTransaction();
BoatDAO dao = ModelDAOHelper.getBoatDAO(transaction);
// Tous les noms des navires triés
TopiaQuery query = dao.createQuery().
                    addNotNull(Boat.NAME).
                    setSelect(Boat.NAME).
                    addOrder(Boat.NAME);

Une autre méthode est disponible pour l'ordre descendant : addOrderDesc. Cependant rien ne vous interdit la syntaxe suivante

addOrder(Boat.NAME, Boat.BUILD_YEAR + " desc");

Requête complexe

L'intérêt majeur de la TopiaQuery est de pouvoir la manipuler à travers des méthodes tout en lui ajoutant des éléments suivant certaines conditions. Il peut être également utile d'utiliser une TopiaQuery comme sous-requête d'une autre. De plus certains mots clés comme EXISTS ou encore l'utilisation de méthode HQL ne possèdent pas leurs propres méthodes. Vous pouvez cependant utiliser la méthode de base addWhere(String str) qui permet l'ajout au WHERE directement (avec ajout automatique des parenthèses). Dans ce cas, il est souvent nécessaire d'ajouter des paramètres HQL à la requête (:monParam) qui devront être ajouté à la TopiaQuery en utilisant la méthode addParam(String name, Object value). Ex

TopiaContext transaction = rootContext.beginTransaction();
ContactDAO dao = ModelDAOHelper.getContactDAO(transaction);
Date beginDate = ...
Date endDate = ...
TopiaQuery query1 = dao.createQuery("C").
        addWhere("C." + Contact.VALIDATION + " IS NOT NULL OR " +
            "C." + Contact.CREATION_DATE + " BETWEEN :begin AND :end").
        addParam("begin", beginDate).addParam("end", endDate);

La méthode fullQuery() permettra de récupérer la requête sous forme de chaîne pour pouvoir la manipuler comme sous-requête. Il est possible également de récupérer les paramètres via la méthode getParams() pour les rajouter à la requête global

// suite du code précédent
// On souhaite la date de création la plus récente sur la requête précédente
query1.setSelect("MAX(C." + Contact.CREATION_DATE + ")");

// Préparation de la deuxième requête
TopiaQuery query2 = dao.createQuery("C2");

// sélection spécifique pour un navire
if (immatriculation != null) {
    query2.addEquals("C2." + Contact.BOAT + "." + Boat.IMMATRICULATION,
            immatriculation);

    // Ajout d'une condition sur le navire dans la première requête
    query1.addWhere("C." + Contact.BOAT + " = C2." + Contact.BOAT);
}

// Utilisation de la première requête comme sous-requête
query2.add("C2." + Contact.CREATION_DATE  + " = (" + query1.fullQuery() + ")");
// Ajout des paramètres nécessaires de la première requête dans la deuxième
query2.addParams(query1.getParams());

Depuis la 2.3.4, la méthode addSubQuery(String statement, TopiaQuery subquery) permet de faciliter l'injection d'une sous-requête notamment pour la gestion des paramètres automatiquement (avec prise en charge des doublons)

// Même chose que précédemment en utilisant la méthode addSubQuery
query2.addSubQuery("C2." + Contact.CREATION_DATE  + " = (?)", query1);

La requête sous forme HQL

SELECT C2 FROM Contact C2
WHERE C2.boat.immatriculation = :immatriculation
AND C2.creationDate = (SELECT MAX(C.creationDate) FROM Contact C
                        WHERE (C.validation IS NOT NULL OR
                                C.creationDate BETWEEN :begin AND :end)
                        AND C.boat = C2.boat);
Attention
Il ne faut pas utiliser la méthode addEquals(String str, Object value) pour une comparaison entre deux propriétés comme précédemment : query1.addWhere("C." + Contact.BOAT + " = C2." + Contact.BOAT). L'appel query1.addEquals("C." + Contact.BOAT, "C2." + Contact.BOAT) ne fonctionnera pas comme souhaité.

Résultats

Plusieurs méthodes sont disponibles pour récupérer les résultats de la requête. Pour chaque méthode, il est nécessaire de l'appeler avec le contexte topia. La méthode de base est la méthode execute() qui renvoie une liste non typé à l'instar de la méthode find(...) du TopiaContext. Il est cependant possible de récupérer directement un objet, un entier (pour un aggregat par exemple) ou une chaîne de caractères suivant le select de la requête. Pour le count très utile dans de nombreux cas, il est mis à disposition la méthode executeCount() qui se chargera de remplacer temporairement le select par un COUNT(*). L'avantage c'est que votre requête ne perd pas son SELECT d'origine pour pouvoir être par exemple executé de façon différente par la suite.

Note
Si la requête contient un DISTINCT (via la méthode addDistinct()), le executeCount() gèrera automatiquement la contrainte sur la requête : "SELECT COUNT(DISTINCT B) FROM Boat B ..."

Limitation des résultats

Il est possible de limiter le nombre de résultats lors de l'exécution pour optimiser la requête dans le cas d'une pagination par exemple. Pour ce faire il faut utiliser les méthodes setLimit(int start, int end) et/ou setMaxResults(int max)

// 18 premiers résultats
query.setLimit(0, 17);
// équivalent à
query.setMaxResults(18);
// résultats du 50ème au 60ème
query.setLimit(49, 59);

// depuis 2.3.4

Pour éviter d'embarquer ces paramètres à chaque fois qu'ils sont nécessaires lors d'un filtrage paginée, un bean, EntityFilter est disponible. Il contient les attributs suivants :

  • startIndex : index de début des résultats.
  • endIndex : index de fin des résultats.
  • orderBy : propriétés à ordonner (l'ajout des mots clés 'asc' et 'desc' est possible).
  • referenceId : identifiant d'une référence utile à la requête.
  • referenceProperty : nom de la propriété correspondant à la valeur du referenceId.

Exemple : Nous souhaitons les contacts 30 à 60 triés par 'creationDate' décroissant et 'personName' croissant pour un navire donné par son topiaId

EntityFilter filter = new EntityFilter();
filter.setStartIndex(30);
filter.setEndIndex(60);
filter.setOrderBy("creationDate desc, personName");
filter.setReferenceId(boat.getTopiaId());
// ou filter.setReference(boat);
filter.setReferenceProperty("boat");

TopiaQuery query = contactDAO.createQuery().addFilter(filter);

L'intérêt de l'EntityFilter est de pouvoir l'instancier et le manipuler directement depuis votre interface (Swing, Web, ...) et de l'utiliser sur une requête métier. La méthode addFilter(EntityFilter filter) permettra d'injecter les paramètres s'ils possèdent une valeur.

Note
L'ordre définit par défaut est celui de création par ordre décroissant : topiaCreateDate desc.

Utilisation des DAO

Les DAO fournissent également quelques méthodes permettant de récupérer plus facilement les résultats avec le type souhaité :

  • countByQuery(TopiaQuery query) : compte le nombre de résultats de la requête.
  • existByQuery(TopiaQuery query) : renvoie vrai si la requête à retourner au moins 1 résultat.
  • findByQuery(TopiaQuery query) : renvoie une entité (un seul résultat)
  • findAllByQuery(TopiaQuery query) : renvoie une liste d'entités
  • findAllMappedByQuery(TopiaQuery query) : renvoie une map d'entités avec pour clé le topiaId de l'entité.
  • findAllMappedByQuery(TopiaQuery query, Class keyClass, String keyProperty) : renvoie une map d'entités avec pour clé la propriété passée en argument.

Exemple

TopiaContext transaction = rootContext.beginTransaction();
BoatDAO dao = ModelDAOHelper.getBoatDAO(transaction);
TopiaQuery query = dao.createQuery();
...
// pour vérifier l'existance de résultat
boolean hasResult = dao.existByQuery(query);

// pour savoir le nombre de résultats
int count = dao.countByQuery(query);

// pour récupérer les résultats
Map<String, Boat> boatMap = dao.findAllMappedByQuery(query);
// ou
List<Boat> boatList = dao.findAllByQuery(query);
// ou juste le premier résultat
Boat boat = dao.findByQuery(query);
// ou avec pour clé l'immatriculation du navire (unique)
Map<Integer, Boat> boatMapImma = dao.findAllMappedByQuery(query,
        Integer.class, Boat.IMMATRICULATION);

Résultats complexes

Certains cas de requête peuvent avoir des résultats plus complexes, notamment lorsqu'il s'agit de propriétés de différentes entités ou avec l'utilisation d' aggrégats (AVG, SUM, COUNT). Dans ce cas il faut utiliser la méthode de base findByQuery(TopiaQuery query) depuis une transaction qui renverra une liste non typée. Lorsqu'il y a plus d'un élément dans le select la liste renvoyée est une List<Object[]>, le tableau pour chaque ligne correspondant aux valeurs des résultats. Exemple

TopiaContext transaction = rootContext.beginTransaction();
ContactDAO dao = ModelDAOHelper.getContactDAO(transaction);
String boatImma = Contact.BOAT + "." + Boat.IMMATRICULATION;
// On souhaite le nombre de contacts par navire
TopiaQuery query = dao.createQuery().
                setSelect(boatImma, "COUNT(*)").addGroup(boatImma);

List<Object[]> results = transaction.findByQuery(query);
// Parcours des résultats
for (Object[] result : results) {
    Integer immatriculation = (Integer)result[0];
    Long count = (Long)result[1];
}
Note
Les aggrégats renvoient principalement un type Long et non Integer.

Chargement des donnees

Généralement une fois la requête exécutée, la transaction utilisée est directement fermée (topiaContext.closeContext()). Dans ce cas, il est souvent nécessaire de charger certaines entités pour éviter les malencontreuses LazyException d'Hibernate. Plusieurs possibilités s'offrent à vous :

Chargement automatique

Hibernate permet de déclarer explicitement que certaines relations doivent se charger dès la récupération des entités. Il faut pour cela utiliser le tagValue lazy dans le fichier de properties du modèle. Par exemple pour charger le navire associé à chaque contact récupéré via une requête, il faut préciser

myapp.entity.Contact.attribute.boat.tagvalue.lazy=false

(myapp.entity.Contact étant le nom qualifié de la classe Contact)

Ainsi chaque contact récupéré aura automatiquement son navire associé de chargé.

Attention cependant, il ne faut pas abuser du tagvalue lazy car sinon Hibernate risque de charger une bonne partie de votre base de données à chaque fois, ce qui peut s'avérer extrêment coûteux. Cette utilisation doit être limitée au cas d'une simple association comme c'est ici le cas, le Boat chargé étant indispensable à l'utilisation du Contact.

Chargement manuel

La TopiaQuery fournit deux méthodes intéressantes pour le chargement manuelle :

  • addLoad(String...) : permet de charger les relations une fois la requête exécutée.
  • addFetch(String...) : permet de charger les relations directement au moment de la requête en utilisant une jointure et le mot clé 'FETCH' Hql.

Le addLoad est pour le moment limité à des relations unitaires (autant que votre modèle vous le permet, ex : entityA.entityB.entityC) ou à une seule relation multiple directe ou indirecte (entityA.entitiesB). Si nous prenons le cas du modèle d'exemple, il est possible de charger le navire associé aux contacts en utilisant : queryContact.addLoad(Contact.BOAT);

Dans le cas du addLoad, plusieurs requêtes seront exécutées suivant le nombre de Contact résultats. Il est dans ce cas plus judicieux d'utiliser le addFetch qui chargera directement les Boat au moment de la récupération des Contact : queryContact.addFetch(Contact.BOAT); L'alias peut s'avérer indispensable pour l'utilisation du addFetch, voir la javadoc pour l'utilisation des arguments.

Note
Les méthodes de jointures permettent de faire un fetch directement : addJoin(String property, String alias, boolean fetch); Voir la partie de la documentation concernant le FROM de la requête.
Attention
Hibernate ne supporte pas plus de 3 ou 4 jointures suivant leurs complexités ! Il est donc important de limiter l'utilisation des fetch aux cas simples.

Que choisir ?

  • Relation unitaire (N-1) obligatoire : utiliser le tagValue lazy
  • Requête relativement simple (moins de deux jointures) : utiliser le addFetch
  • Requête complexe : utiliser le addLoad
  • Si addLoad non utilisable (trop de chargement de collections) : effectuer un chargement manuel en parcourant les résultats. Pour le chargement d'une entité simple, il suffit d'utiliser le getter correspondant pour la charger, tandis que pour une collection, l'appel à la méthode size associée permet de charger l'intégralité des éléments.