| author : | Florian Desbois |
|---|---|
| contact : | topia-devel@list.nuiton.org ou topia-users@list.nuiton.org |
| revision : | 2589 |
| date : | 2012-06-15 |
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.
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);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);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.
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.
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];
}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.
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.