lundi 12 novembre 2012

Java Serveur App 9 : requêtes JPQL


Java Persistence Query Language : JPQL

JPA nous permet de manipuler des données persistentes sous forme d'objets. Mais la programmation objet n'implique pas de langage de requête. 

JPQL se compare avec SQL de la manière suivante:
  • SQL renvoie des enregistrements, JPQL renvoie des objets.
  • Les paramètres des requêtes peuvent être des objets Java.
  • JPQL a une syntaxe particulière pour traverser des relations dans le modèle objet.
Au delà de ces différences, les deux sont des langages de requêtes (Query Language) avec un objectif initial identique, accéder à des données persistantes de manière ciblée.


Premier exemple complet

Nous cherchons à trouver les restaurants avec plus de 6 plats. Après avoir créé une classe appelée JPQLRequests, nous allons y ajouter la méthode suivante:
public static List<Restaurant> ex1(){
 // recuperer l'EntityManager
 EntityManagerFactory emf = Persistence.createEntityManagerFactory("RestoPresto");
 EntityManager em = emf.createEntityManager();
 // creer la requete
 String query = "SELECT r FROM Restaurant r WHERE SIZE(r.foodItems) >= 6";
 Query q = em.createQuery(query);
 // execution et recuperation de la liste de resultats
 List<Restaurant> result = q.getResultList();
 return result;
}

Il y a ici trois étapes:

  1. On récupère un EntityManager pour interagir avec l'unité de persistence (la plupart du temps, une base de données).
  2. On crée la requête, dans notre cas à partir d'une chaîne de caractères.
  3. On exécute et on récupère la liste d'objets.
Pour cette première requête, nous allons voir le détail:
  • "SELECT r FROM Restaurant r" constitue la base de la requête. Ici on nomme r pour pouvoir accéder à ses champs dans la suite.
  • "WHERE SIZE(r.foodItems) >= 6", ici SIZE est un opérateur permettant d'accéder à la taille d'une collection. On note qu'on accède au champ directement par son nom et pas par un getter. 
Autres exemples

Notre requête numéro 2 portera sur les plats dont le prix est supérieur à 5 dollars classés par prix. Cette requête nous permet de voir comment ordonner le résultat selon un champ persisté.

public static List<FoodItem> ex2(){
 EntityManagerFactory emf = Persistence.createEntityManagerFactory("RestoPresto");
 EntityManager em = emf.createEntityManager();
 String query = "SELECT f FROM FoodItem f WHERE f.price > 5 ORDER BY f.price DESC";
 Query q = em.createQuery(query);
 List<FoodItem> result = q.getResultList();
 return result;
}

Notre troisième exemple est une requête paginée. Elle montre comment fixer un certain nombre de résultats à récupérer dans la requête ce qui sert habituellement à afficher une liste d'objets sans rapporter l'ensemble de la base de données. Dans notre exemple, nous récupérons les 10 premiers objets.
public static List<FoodItem> ex3(){
 EntityManagerFactory emf = Persistence.createEntityManagerFactory("RestoPresto");
 EntityManager em = emf.createEntityManager();
 
 String query = "SELECT f FROM FoodItem f WHERE f.price > 5 ";
 Query q = em.createQuery(query);
 q.setFirstResult(0);
 q.setMaxResults(10);
 
 List<FoodItem> result = q.getResultList();
 return result;
}

Le 4ème exemple : les restaurants avec au moins un plat coutant moins de 5 dollars. Cet exemple de requête nous montre comment traverser une relation: nous allons récupérer des restaurants mais en les sélectionnant selon une propriété (le prix) d'un plat.
public static List<Restaurant> ex4(){
 EntityManagerFactory emf = Persistence.createEntityManagerFactory("RestoPresto");
 EntityManager em = emf.createEntityManager(); 
 String query = "SELECT r FROM Restaurant r, IN(r.foodItems) f WHERE f.price < 7";
 Query q = em.createQuery(query); 
 List<Restaurant> result = q.getResultList();
 return result;
}


Requête avec paramètre

Les exemples que nous avons vus jusqu'ici était des requêtes statiques. Voyons maintenant comment faire une requête qui dépend d'un paramètre. Reprenons l'exemple des plats dont le prix est compris entre un minimum et un maximum.
public static List<Restaurant> ex5(int min, int max){
 EntityManagerFactory emf = Persistence.createEntityManagerFactory("RestoPresto");
 EntityManager em = emf.createEntityManager();  
 String query = "SELECT r FROM Restaurant r WHERE SIZE(r.foodItems) BETWEEN :min and :max";
 Query q = em.createQuery(query);
 q.setParameter("min", min);
 q.setParameter("max", max); 
 List<Restaurant> result = q.getResultList();
 return result;
}
Ici on voit que la notation ":min" permet de déclarer un paramètre appelé min pour lequel on va passer la valeur du paramètre plus tard. 

Un des intérêts est que le paramètre passé à la requête peut très bien être un objet. Par exemple, la requête suivante permet de récupérer le restaurant qui sert un plat en particulier:
public static List<Restaurant> ex6(FoodItem f){
 EntityManagerFactory emf = Persistence.createEntityManagerFactory("RestoPresto");
 EntityManager em = emf.createEntityManager(); 
 String query = "SELECT r FROM Restaurant r WHERE :f MEMBER OF r.foodItems ";
 Query q = em.createQuery(query);
 q.setParameter("f", f);
 List<Restaurant> result = q.getResultList();
 return result;
}



API de création de requêtes

La création de requête JPQL sous forme d'une chaîne de caractères impose des requêtes statiques auxquelles on ne peut pas ajouter de clause dynamiquement. Pour parer à ce problème, JPA propose une API de création de requête. Revoyons l'exemple initial dans ce format:


public static List<Restaurant> ex1api(){
 EntityManagerFactory emf = Persistence.createEntityManagerFactory("RestoPresto");
 EntityManager em = emf.createEntityManager();
 // creer la requete
 CriteriaBuilder cb = em.getCriteriaBuilder();
 // equivalent "SELECT r FROM RESTAURANT r"
 CriteriaQuery<Restaurant> q = cb.createQuery(Restaurant.class);
 Root<Restaurant> c = q.from(Restaurant.class);
 q.select(c);
 // equivalent "WHERE SIZE(r.foodItems) >= 6"
 Expression<Integer> size = cb.size(c.<List<FoodItem>>get("foodItems"));
 Predicate eq = cb.ge(size, 6);
 q.select(c).where(eq);
 TypedQuery<Restaurant> query = em.createQuery(q);
 List<Restaurant> result = query.getResultList();
 return result;
}


On voit que la création d'une requête simple est plus complexe en utilisant l'API. Cependant, il faut garder en tête que certaines requêtes ne sont programmables qu'avec l'API. Par exemple, si vous souhaitez récupérer tous les plats dont le nom est contenu dans un tableau de String qui n'est connu qu'à l'exécution.

Requêtes nommées

Une autre possibilité pour la requête 1 est d'utiliser une requête nommée. Une requête nommée se déclare via une annotation à la classe entité que la requête renvoie. On les place entre l'annotation d'entité et la ligne de déclaration de la classe comme suit:
@Entity
@NamedQuery(name="Restaurant.ex1", query="SELECT r FROM Restaurant r WHERE SIZE(r.foodItems) >= 6")
public class Restaurant implements Serializable {

La requête est exactement la même que pour l'exemple 1. Ensuite, pour appeler la requête on utilisera le code suivant:

public static List<Restaurant> ex1named(){
 EntityManagerFactory emf = Persistence.createEntityManagerFactory("RestoPresto");
 EntityManager em = emf.createEntityManager();
 TypedQuery<Restaurant> query = em.createNamedQuery("Restaurant.ex1", Restaurant.class);
 List<Restaurant> result = query.getResultList();
 return result;
}

Les requêtes nommées ont l'intérêt d'être déclarée dans la classe de l'entité directement. Ainsi, elle facilite leur maintenance puisqu'un programmeur qui modifie l'entité aura directement accès aux requêtes pour les modifier en conséquence.

Test non significatif de performance

Pour la requête 1, nous allons tester 100 fois la requête standard, nommée et dynamique (créé via API) pour observer la performance sur une requête simple. Ma machine de test est un MacBook Pro avec un Core 2Duo à 2,4GHz et 8Go de mémoire vive. La base de données est la version embarquée de Apache Derby.

requete 1 standard performance for 100 req is 1465  ms
requete 1 standard performance for 100 req is 1574  ms
requete 1 standard performance for 100 req is 1493  ms
requete 1 standard performance for 100 req is 1465  ms
requete 1 api performance for 100 req is 1638  ms
requete 1 api performance for 100 req is 1551  ms
requete 1 api performance for 100 req is 1541  ms
requete 1 api performance for 100 req is 1612  ms
requete 1 named performance for 100 req is 1398  ms
requete 1 named performance for 100 req is 1395  ms
requete 1 named performance for 100 req is 1395  ms
requete 1 named performance for 100 req is 1405  ms

On obtient le classement suivant:

  1. Requête nommée est la plus rapide.
  2. Juste après la requête construite avec une chaîne de caractères.
  3. En dernier, la requête construire avec l'API.
Cela correspond à un compromis à faire entre le caractère dynamique d'une requête et sa performance.

ATTENTION: ces résultats dépendent de très nombreux paramètres et ne sont donnés qu'à titre indicatif.



Lecture de référence

Le point d'entrée de référence principal pour JPQL est sans doute le tutoriel 
et en particulier les exemples.

Vous trouverez des exemples additionnels de requêtes traversant des relations ici

Et finalement, des suggestions concernant l'optimisation ici.


Fichiers du projets et des requêtes

Vous trouverez le projet avec les requêtes décrites dans le post ici.







Aucun commentaire:

Enregistrer un commentaire