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'
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.