| author : | Florian Desbois |
|---|---|
| contact : | topia-devel@list.nuiton.org ou topia-users@list.nuiton.org |
| revision : | 2261 |
| date : | 2011-04-16 18:41:18 +0200 (sam., 16 avril 2011) |
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.
Ci-dessous un modèle simple utilisé pour les exemples de cette documentation.

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");
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);
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 !=
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' ::
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.
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);
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.
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);
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");
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);
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é.
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.
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 ..."
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.
L'ordre définit par défaut est celui de création par ordre décroissant : topiaCreateDate desc.
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);
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];
}
Les aggrégats renvoient principalement un type Long et non Integer.
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 :
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.
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.
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.
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.
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.