lundi 26 novembre 2012

Java Serveur App 15 : champ de recherche


Champ de recherche

On voit maintenant très souvent des applications intégrant des possibilités de recherche. Il existe plusieurs possibilités:
  • Effectuer des recherches via des requêtes directes en base de données.
  • Utiliser un serveur de recherche (indexation et requête texte) dédié comme solr ou elasticsearch.
La solution que nous montrerons ici est basé sur des requêtes directes. Les étapes seront: 
  • Écrire les requêtes de recherche.
  • Accéder à une liste filtrée par un terme de recherche.
L'opérateur LIKE et les motifs

Sans indexation, un champ de recherche se limite à une requête qui peut être plus ou moins complexe. L'opérateur qui nous servira le plus, c'est LIKE qui nous permet une forme de motifs (une version très allégée d'expressions régulières). On a accès à deux caractères spéciaux _ et %, le premier désigne un caractère quelconque, le second une suite de caractères quelconque.

Quelques exemples:

  • "%tion%" désignera toutes les valeurs qui contiennent la syllabe "tion"
  • "%mot%" désignera toutes les chaînes contenant le mot, ce qui nous servira pour nos recherches pour un mot clé.
  • "#FF__FF" désignera toutes les chaînes de caractères ressemblant à des couleurs RGB avec le rouge et le bleu au maximum, peu importe le vert.
  • "_t%" désignera toutes les chaînes dont la 2ème lettre est un "t"


Requête statique, un mot

Nous sommes dans la situation où on cherche un mot, soit dans le nom du restaurant soit dans le nom d'un plat, et on souhaite récupérer les restaurants. La requête fonctionne avec un seul mot clé.


public static List<Restaurant> search(String search) {
 EntityManagerFactory emf = Persistence.createEntityManagerFactory("RestoPresto");
 EntityManager em = emf.createEntityManager();
 String word = search.toLowerCase();
 String searchQuery = 
 "select distinct r from Restaurant r, IN(r.foodItems) f"+
 " where lower(trim(r.name)) like lower(trim(:word))"+
 " or lower(trim(f.name)) like lower(trim(:word)) ";
 System.out.println(searchQuery);
 Query q = em.createQuery(searchQuery, Restaurant.class);
 q.setParameter("word", "%" + word + "%");
 return q.getResultList();
}


Requête dynamique, plusieurs mots

Sans savoir combien de mots existent dans la chaîne de recherche, il est impossible d'écrire une requête statique. Nous devons donc écrire une requête dynamique qui contiendra autant de prédicats que de mots. Pour illustrer le fonctionnement nous prendrons le OU (disjonction) de ces prédicats, autrement dit un restaurant sortira s'il contient un mot ou un autre.


public static List<Restaurant> searchMulti(String search) {
 EntityManagerFactory emf = Persistence.createEntityManagerFactory("RestoPresto");
 EntityManager em = emf.createEntityManager();
 // get all search key words
 String[] words  = search.trim().split(" ");
 CriteriaBuilder cb = em.getCriteriaBuilder();
 CriteriaQuery<Restaurant> query = cb.createQuery(Restaurant.class);
 Root<Restaurant> root = query.from(Restaurant.class);
 List<Predicate> conditions = new ArrayList<Predicate>();
 Path<String> name = root.<String>get("name");
 // for each word in string
 for (String word: words){
  conditions.add(cb.like(cb.lower(name),"%" + word.toLowerCase() + "%"));
 }
 Predicate or = cb.or(conditions.toArray(new Predicate[conditions.size()]));
 query.where(or);
 TypedQuery<Restaurant> tq = em.createQuery(query);
 List<Restaurant> restos = tq.getResultList();
 return restos;
}

Fichiers du projet

Le projet avec nos deux options de recherche se trouve ici. La recherche se fait à partir de la liste de restaurants.


Plus loin sur la recherche

Nos requêtes ne permettent pas de renvoyer des résultats proches ("restaurant" pour "retaurant" etc.). Elles doivent être écrite à la main etc. et peuvent poser des problèmes de performance. 

C'est la raison pour laquelle des librairies comme Lucene et des serveurs comme Solr ou elasticsearch existent. Ces composants permettent d'indexer efficacement des informations et d'avoir une réelle souplesse dans la recherche de données.




Java Serveur App 14 : validation


Validation

Un des problèmes fréquents de la programmation web est que les formulaires envoient uniquement des paramètres de type String. En web, tout est String et il faut donc convertir les informations d'un formulaire avant d'en faire un objet et/ou de l'écrire en BD.

La conversion inclut nécessairement une étape de validation puisque rien ne garantit qu'une chaîne de caractères représente un nombre, une date ou encore une adresse de courriel.

La validation peut s'effectuer de plusieurs manières:
  • Côté client, dans le navigateur et donc nécessairement en javascript. Récemment JQuery et ses plugins de validation ont montré leur puissance sur ce terrain.
  • Côté serveur avec l'ensemble des conversions et manipulations qui précèdent la création d'un objet correct.
  • AJAX : le client appelle le serveur pour une vérification sans recharger la page.
Nous allons donc voir quand et comment utiliser ces différentes techniques.

Validation côté client

La validation côté client a les caractéristiques suivantes:

  • Elle peut se faire sans accéder au serveur. Cela évite de charger le serveur et permet plus d'interactivité en pointant directement l'élément d'un formulaire qui pose problème.
  • Elle n'a pas accès directement à la base de données et ne peut donc pas vérifier si une valeur est présente au absente de la BD.
  • Elle concerne principalement les formulaires mais pas forcément les autres accès au serveur.

La librairie que nous utiliserons est JQuery Validation.
<form id="client" action="client" method="post">
 <!-- le titre devient le message d erreur -->
 <p>Nom: <input type="text" name="nom" class="required" minlength="2" maxlength="25" title="Ce champ est requis"> </p>
 <p>Carte de crédit: <input type="text" name="credit" class="creditcard"></p>
 <p>Courriel: <input type="text" name="courriel" class="required email"></p>
 <p>Age (entre 18 et 100 ans): <input type="text" name="age" id="age" title="Entre 18 et 100 ans"></p>
 <p><input type="submit" value="Envoyer"></p>
</form>


Le code Javascript décrit une nouvelle fonction de validation et permet de changer les messages d'erreur.


$.validator.addMethod("verifAge", function() {
 return $("#age").val() < 100 && $("#age").val() > 18;
});
$("#client").validate({
 rules : {
  age : {
   verifAge : true
  }
 // ajout de la règle au champ dont le id est age
 },
 messages : {
  credit : "Entrez une carte de crédit valide", // autre maniere de mettre le message d erreur (pas title)
  courriel : "Entrez un courriel valide"
 },
 onkeyup : false
});


Validation côté serveur

La validation côté serveur reste le seul et unique moyen de s'assurer qu'on créer des objets corrects. Un ensemble de librairies permettent de s'assurer que les valeurs de paramètres remplissent un ensemble de contraintes pour pouvoir les écrire sereinement en BD ou s'assurer qu'on pourra les convertir sans risque.


Nous utiliserons la librairie Apache Commons Validator. Ce code de validation va s'exécuter naturellement au niveau du contrôleur dans une architecture MVC. C'est la seule validation sûre avant d'écrire en BD.

Par contre le retour utilisateur est souvent plus compliqué. Ici nous utilisons un attribut contenant des messages à afficher dans la page.


if (action.equals("/server")){
 // server side validation
 System.out.println("Validation SERVER en cours");
 // creation des validateurs
 CreditCardValidator credit = new CreditCardValidator(CreditCardValidator.AMEX + CreditCardValidator.VISA);
 EmailValidator email = EmailValidator.getInstance();
 IntegerValidator age = IntegerValidator.getInstance();
 // validation
 boolean prenomOk = StringUtils.isAlpha(request.getParameter("prenom"));
 boolean courrielOk = email.isValid(request.getParameter("courriel"));
 boolean creditOk = credit.isValid(request.getParameter("credit"));
 // age est un peu plus complexe
 boolean ageOk = false;
 if (request.getParameter("age") != null && !request.getParameter("age").equals("")){
  ageOk  = age.isInRange(
    Integer.parseInt(request.getParameter("age")), 18, 100);
 }
 // messages pour l'utilisateur
 List<String> messages = new ArrayList<String>();
 if (!prenomOk) messages.add("Veuillez entrer un prénom avec des lettres");
 if (!courrielOk) messages.add("Veuillez entrer une adresse courriel valide");
 if (!creditOk) messages.add("Veuillez entrer une carte de crédit valide");
 if (!ageOk) messages.add("Veuillez entrer un age entre 18 et 100 ans");
 request.setAttribute("erreurs", messages);
 request.getRequestDispatcher("index.jsp").forward(request, response);
 System.out.println("Validation SERVER effectuée");
 return;
}



Validation AJAX

La validation AJAX est une tentative pour obtenir le meilleur des deux mondes:

  • L'interactivité côté client.
  • L'accès aux librairies et aux données côté serveur.
Il s'agit souvent d'une solution intéressante mais elle ne garantit pas que les données finalement envoyées seront correctes et ne dispense donc pas de validation côté serveur.

Dans le cas de JQuery Validation, il suffit de mettre en place une règle "remote". Javascript attend donc une réponse "true" ou "false" selon que le champ est bon ou mauvais.

// definit une regle qui demande au serveur si c bon (true) ou mauvais (false)
$("#ajax").validate({
 rules : {
  fajax : {
   required : true,
   remote : {
    url : 'ajaxValidate',
    type : "post"
   }
  }
 }
});
Dans ce cas, le code contrôleur est plutôt simple et doit juste répondre true ou false.
String[] conjonctions = {"mais","ou","est","donc","or","ni","car"};
String candidat = request.getParameter("fajax");
if (Arrays.asList(conjonctions).contains(candidat)){
 // on repond true si la valeur est bonne
 response.getWriter().print("true");
}
else{
 // on repond false si la valeur est à refuser
 response.getWriter().print("false");
}
Fichiers du projet

Vous trouverez le projet contenant les exemples de validation ici.

Java Serveur App 13 : AJAX


AJAX

AJAX signifie:
  • Asynchronous : il s'agit d'appels asynchrones vers le serveur. La page ne se recharge pas.
  • Javascript : les appels sont initiés par Javascript ce qui permet de modifier la page dans la navigateur avec le résultat de la requête.
  • And
  • XML : les échanges entre le client et le serveur se font avec du XML. Cependant XML est de moins en moins utilisé, des formats comme JSON ou YAML sont de plus en plus utilisés.
Dans notre application il va falloir:
  • Faire l'appel au serveur depuis Javascript, nous verrons que JQuery propose une alternative très simple et souple.
  • Effectuer l'aller retour au serveur et voir comment répondre à une requête avec une chaine de caractères et pas une page JSP complète.
  • Récupérer le résultat dans Javascript et effectuer les modifications nécessaires dans la page
Exemple de la suppression d'un élément d'une liste

Notre exemple est une liste chargée dans le navigateur. Dans l'ordre, la transaction AJAX va comporter les étapes suivantes:

  1. Le clic sur le bouton de suppression déclenche une fonction Javascript qui lance un appel au serveur sur l'URL "ajaxSupprimer" avec un paramètre id pour l'objet.
  2. La serveur reçoit cet identifiant, supprime l'objet correspondant et répond "done" au client s'il a réussi à supprimer, "error" sinon.
  3. Le client effectue la suppression de la ligne en Javascript.
Le code client est le suivant


<%@page import="java.util.List"%>
<%@page import="com.deguet.Controller"%>
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>Demo AJAX</title>
 <script src="http://code.jquery.com/jquery-latest.js"></script>
        <script>
            function supprimer(element){
                console.log("Suppression AJAX de "+element);
                $.ajax({
                   url: 'ajaxSupprimer?id='+element,
                   success: function(data) {
                    $("#"+element).remove();
                   }
                });
            }
        </script>
    </head>
    <body>
        <h1>Exemple AJAX</h1>
        <table>
        <%
            Controller.initElements(request);
            List<String> elements = (List<String>) session.getAttribute("elements");
            System.out.println("La liste vaut " + elements);
            for (String element : elements){
                %>
                 <tr id="<%= element%>">
                  <td><%= element%></td>
                  <td> <button  onclick="supprimer('<%= element%>');" >X</button>  </td>
                 </tr>
                <%
            }
        %>
        </table>
    </body>
</html>


Quelques explications:

  • Dans la boucle for, on crée chaque ligne de notre tableau avec un id dans le DOM pour pouvoir facilement la supprimer plus tard. La fonction supprimer est appelé sur le onclick avec cet id.
  • Plus haut, on voit la fonction supprimer responsable de l'appel AJAX ($.ajax() de JQuery) auquel on fournit l'URL ("ajaxSupprimer?id=5" si l'id est 5) ainsi que la fonction à exécuter en cas de succès (puisque l'appel est asynchrone). Le paramètre "data" contient alors la réponse du serveur.
  • La fonction de "success" va simplement supprimer le tr (ligne dans notre tableau) avec l'id correspondant.


Maintenant côté serveur, notre contrôleur va prendre en charge le motif URL ajaxSupprimer:


if (action.equals("/ajaxSupprimer")){
 String id = request.getParameter("id");
 System.out.println("Suppression AJAX " + id);
 List<String> elements = (List<String>) request.getSession().getAttribute("elements");
 if (elements != null && elements.contains(id)) {
  elements.remove(id);
  response.getWriter().println("done");
 } else {
  response.getWriter().println("error");
 }
 return;
}

Les différentes étapes sont :

  • Récupérer les objets nécessaires: ici l'identifiant et la liste à modifier.
  • Déterminer si l'opération est possible, c'est le sens du "if (elements != null .... )"
  • Effectuer l'opération quand c'est possible.
  • Renvoyer une réponse appropriée à travers l'écriture dans la réponse (response.getWriter().print()).

Ce code peut être testé sans le javascript en inscrivant directement une URL comme:

http://localhost:7654/ajaxSupprimer?id=pipo

Le cas pipo devrait donner une erreur. Ce type d'URL permet de tester facilement le code contrôleur sans avoir à effectuer un appel via Javascript.

AVERTISSEMENT: l'exemple fournit ne tient absolument pas compte des cas d'erreur.

Impacts sur l'approche MVC

En résumé, l'approche MVC que nous avons appliquée à notre application se décrit comme:

  1. une action de l'utilisateur est une URL avec des paramètres texte
  2. le contrôleur réagit à cette action en effectuant les traitements nécessaires, les accès à la BD etc.
  3. le contrôleur passe uniquement les objets nécessaires à la création de la vue.
Ce qui reste vrai : une action est toujours une url avec des paramètres; le contrôleur est responsable de la validation, des accès à la BD en réaction à une action.

Ce qui change : le résultat est un texte envoyé au navigateur, la vue initiale est modifiée par javascript. Le débogage de la vue ne peut plus se faire sur le code serveur Java, il faut travailler en javascript. 

Fichiers 

Vous trouverez l'exemple AJAX dans le projet ici.

dimanche 25 novembre 2012

Java Serveur App 12 : envoi de courriel


Envoi de courriels

La plupart des applications web ont besoin de pouvoir envoyer des courriels à leurs utilisateurs. Nous allons voir comment faire dans le cadre d'une application web Java. Les étapes sont les suivantes:
  • Intégrer JavaMail.
  • Configurer les propriétés pour un serveur d'envoi.
  • Créer un message et l'envoyer.
JavaMail et le pom.xml

La librairie la plus courante pour envoyer des courriels en Java s'appelle ... JavaMail.

Encore une fois, une simple dépendance dans notre pom.xml va nous permettre d'utiliser la librairie.


<dependency>
 <groupId>javax.mail</groupId>
 <artifactId>mail</artifactId>
 <version>1.4</version>
</dependency>

Utiliser Gmail et son serveur d'envoi

Sans se plonger dans les détails, l'envoi d'un courriel nécessite un serveur SMTP qui va s'occuper de transférer le courriel jusqu'au serveur de réception.


La question est donc : où trouver un serveur SMTP accessible?

  • Il n'existe pas de serveur SMTP complètement ouvert car il servirait de relai pour tous les spammers de la planète.
  • Le fournisseur d'accès Internet fournit normalement un serveur SMTP que vous pouvez utiliser avec parfois un besoin d'autentification par nom d'utilisateur et mot de passe.
  • Gmail et son service de messagerie en ligne vous permet d'accéder à son serveur de messagerie si vous avez un compte Gmail.

Nous utiliserons cette possibilité (ATTENTION : vous devez lire les conditions d'utilisation de Gmail pour vous assurer que vous ne violez aucune règle énoncée par Google) juste pour tester un envoi de courriel pour votre application.

Il est fortement recommandé de créer un compte Google spécialement pour votre application car vous devrez écrire votre mot de passe dans le code.
// A remplacer par votre login Gmail pour l'envoi de courriel
final String gmailLogin = "moncompte@gmail.com";
// A remplacer par le mot de passe du compte Gmail
final String gmailPass  = "monmotdepasse";
// A remplacer par votre adresse courriel
final String destination = "destination@domaine.com";

Properties props = new Properties();
props.put("mail.smtp.auth", "true");
props.put("mail.smtp.starttls.enable", "true");
props.put("mail.smtp.host", "smtp.gmail.com");
props.put("mail.smtp.port", "587");

Session session = Session.getInstance(props,new javax.mail.Authenticator() {
 protected PasswordAuthentication getPasswordAuthentication() {
  return new PasswordAuthentication(gmailLogin,gmailPass);
 }
}); 

Dans le détail:

  • Les 3 variables initiales sont à remplacer par les valeurs que vous souhaitez utiliser. Gmail requiert un compte pour utiliser son serveur, et il faudra une adresse de destination pour votre courriel.
  • La configuration du serveur pour JavaMail se fait à travers l'objet Properties.
  • Ensuite, nous créons une Session qui est l'objet nécessaire pour pouvoir créer nos messages plus tard.


Créer un message et l'envoyer

Reste maintenant à créer et envoyer un message. La création du message se fait via un MimeMessage. Une fois le message créé, nous allons utiliser la classe Transport de JavaMail pour envoyer le message.


Address sender = new InternetAddress(gmailLogin);
MimeMessage msg = new MimeMessage(session);
msg.setFrom(sender);
msg.setRecipients(Message.RecipientType.TO,destination);
msg.setSubject("Test de courriel de mon application "+id);
msg.setSentDate(new Date());
msg.setText("Bonjour!\nTest de courriel avec une URL et un paramètre \nhttp://localhost:7070/voirCommande?id="+id);
Transport.send(msg);

ATTENTION: il y a de fortes chances que le courriel envoyé soit classé dans les spams de votre boîte de réception. Vérifier le dossier spam de votre courriel avant de jeter votre code.

Fichiers du projet

Vous trouverez le projet intégrant les éléments permettant d'envoyer des courriels ici. La classe la plus importante de ce projet est Courriel.java qui contient le code ci-dessus. L'application part sur le port 7654 à l'URL :

http://localhost:7654/



Java Serveur App 11 : fichiers en base de données


Fichiers en base de données

Nous allons voir comment gérer des fichiers en base de données dans une application Java Web. Les différentes étapes sont:

  • Créer un modèle de données permettant de stocker des données binaires, sous forme d'un tableau d'octets.
  • Permettre d'écrire des fichiers dans ce modèle grâce à un téléversement de fichier.
  • Permettre de servir les fichiers stockés afin de les afficher, de les télécharger etc.



Modèle de données

La première partie consiste à mettre en place une entité permettant de stocker un fichier. Voici le code d'une entité classique permettant de stocker un fichier:


package com.deguet;

import javax.persistence.*;

@Entity
public class File {

 @Id
 @GeneratedValue(strategy=GenerationType.IDENTITY)
 private int id;

 @Lob // inferred as binary because it is a byte[]
 private byte[] content;

 private String type;

 public int getId() {return id;}
 public void setId(int id) {this.id = id;}

 public byte[] getContent() {return content;}
 public void setContent(byte[] content) {this.content = content;}

 public String getType() {return type;}
 public void setType(String type) {this.type = type;}
 
}

Nous utilisons 2 champs de données : le premier est le contenu, la succession d'octets constituant le fichier, le second est le type de fichier.

File upload, téléversement de fichiers

Pour gérer le téléversement, la programmation web impose d'utiliser un formulaire envoyant ses paramètres selon la méthode POST (la méthode GET ne permet pas d'envoyer suffisamment de données). L'encodage des fichiers nécessite la déclaration de multipart dans le formulaire:



<form enctype="multipart/form-data" action="up" method="POST">
 Choose a file to upload: <input name="uploadedfile" type="file" /><br />
 <input type="submit" value="Upload" />
</form>

Pour la réception côté serveur du fichier, nous allons utiliser les utilitaires de la fondation Apache. Nous aurons besoin des deux dépendances suivantes:

<dependency>
 <groupId>commons-io</groupId>
 <artifactId>commons-io</artifactId>
 <version>2.4</version>
</dependency>

<dependency>
 <groupId>commons-fileupload</groupId>
 <artifactId>commons-fileupload</artifactId>
 <version>1.2.2</version>
</dependency>

La réception va se faire à travers un motif URL dans notre contrôleur.
if ( ServletFileUpload.isMultipartContent( request ))
{
 System.out.println("MultiPart might contain a file ");
 List<FileItem> fileItems;
 try {
  fileItems = new ServletFileUpload( new DiskFileItemFactory()).parseRequest( request );
  for ( FileItem item : fileItems )
  {
   String fieldName = item.getFieldName();
   if ( item.isFormField()) { 
    item.getString();      
    System.out.println("Param post multi not file " + fieldName+" = "+item.getString());
   } // Form's input field
   else { 
    System.out.println("Param post multi file " + fieldName+" = "+item.getContentType());
    File i = new File();
    // get the type
    i.setType(item.getContentType());
    // get the bytes
    i.setContent(item.get());
    // save it in database
    facade.create(i);
    response.sendRedirect("index.jsp");
    return;
   }
  }
 } catch (FileUploadException e) {
  e.printStackTrace();
 } catch (Throwable e) {
  e.printStackTrace();
 }
}
Quelques points sur le code précédent:
  • Il s'applique sur une requête MultiPart.
  • Apache FileUpload fonctionne avec des FileItem qui représentent les fichiers contenus dans la requête.
  • Les items peuvent être des "form fields", ce sont les champs classiques de formulaires qui se retrouvent sous la forme de chaîne de caractères.
  • Dans le cas contraire, c'est un fichier, autrement dit une succession d'octets.
  • La fonction get() de l'item permet de récupérer les octets du fichier, getContentType permet d'obtenir le type MIME.
  • Finalement, notre façade nous permet de créer le fichier récupéré.

Service d'accès à un fichier

Pour servir le fichier stocké en base de données, nous allons utiliser un motif URL qui va directement envoyer le fichier dans sa réponse HTTP. 
if (action.equals("/down")){
 System.out.println("Got a request for a file ");
 // get the image from the database
 String id = request.getParameter("id");
 Integer i = Integer.parseInt(id);
 File img = facade.find(i);
 // set the file type in headers
 response.setContentType(img.getType());
 // write the bytes directly to the response
 ServletOutputStream out = response.getOutputStream();
 byte[] bytes = img.getContent();
 out.write(bytes);
 return;
}


Fichiers du projet

Vous trouverez ici un projet illustrant le stockage de fichiers binaires dans une application web Java.




lundi 19 novembre 2012

Java Serveur App 10 : écriture et transaction JPA


Écrire en base de données

Écrire en base de données est crucial pour n'importe quelle application puisque c'est à ce moment que votre application se souvient, stocke de l'information et c'est cette information qui donne sa valeur à votre application. 

Pour écrire en base de données, l'outil principal fourni par la spécification JPA est l'EntityManager qui fournit deux éléments très importants:

  • Les méthodes de base que sont persist() et merge().
  • La possibilité d'ouvrir et de soumettre une transaction.



Transaction avec erreur

Une transaction permet d'encapsuler plusieurs petites opérations en un bloc indivisible. Le but est de pouvoir défaire l'ensemble si une opération échoue et ainsi éviter d'avoir des objets persistés dans un état incohérent. 

Notre exemple est le suivant: pour chaque plat du système, on veut créer une variante "XL" qui coutera 50 cents de plus et aura les mêmes ingrédients.

public static void transactionAvecErreur(){
 // recuperer l'EntityManager
 EntityManagerFactory emf = Persistence.createEntityManagerFactory("RestoPresto");
 EntityManager em = emf.createEntityManager();
 EntityTransaction transaction  = em.getTransaction();
 // transaction avec attrapage des erreurs
 try{
  transaction.begin();
  // aller chercher tous les plats
  String query = "SELECT f FROM FoodItem f";
  Query q = em.createQuery(query);
  List<FoodItem> dishes = q.getResultList();
  // pour chaque plat
  for (FoodItem dish : dishes){
   // dupliquer chaque plat
   FoodItem xl = new FoodItem();
   // ajouter 50 cents au prix du duplicata
   xl.setPrice(dish.getPrice()+0.5);
   // ajouter " XL " à la description du plat dupliqué
   xl.setName(dish.getName()+" XL");
   // stocker en BD
   em.persist(xl);
  }
  transaction.commit();
 }catch(Throwable thrown){// si exception, le rollback est implicite
  thrown.printStackTrace();
 }
}

Le problème vient ici du champ "description" de l'objet FoodItem qui ne peut pas être null en base de données. En effet, une des causes d'échec d'une écriture en BD est le plus souvent le non respect des contraintes. (ici le champ description a reçu l'annotation @Column(nullable = false) qui indique que la valeur null n'est pas acceptée)

Transaction sans erreur

Nous montrons ici la même transaction avec la correction de l'erreur, ici la description est bien là. Nous voyons aussi comment ajouter le plat à la liste de plats du restaurant.

public static void transactionSansErreur(){
 // recuperer l'EntityManager
 EntityManagerFactory emf = Persistence.createEntityManagerFactory("RestoPresto");
 EntityManager em = emf.createEntityManager();
 EntityTransaction transaction  = em.getTransaction();
 // transaction avec attrapage des erreurs
 try{
  transaction.begin();
  // aller chercher tous les plats
  String query = "SELECT f FROM FoodItem f";
  Query q = em.createQuery(query);
  List<FoodItem> dishes = q.getResultList();
  // pour chaque plat
  for (FoodItem dish : dishes){
   // dupliquer chaque plat
   FoodItem xl = new FoodItem();
   // ajouter 50 cents au prix du duplication
   xl.setPrice(dish.getPrice()+0.5);
   // ajouter " XL " à la description du plat dupliqué
   xl.setName(dish.getName()+" XL");
   // ATTENTION NE PAS OUBLIER LA DESCRIPTION !!!!!!!!!!!
   xl.setDescription(dish.getDescription());
   // mettre à jour les relations
   Restaurant r = dish.getRestaurant();
   xl.setRestaurant(r);
   r.getFoodItems().add(xl);
  }
  transaction.commit();
 }catch(Throwable thrown){
  thrown.printStackTrace();
 }
}

Écriture d'une commande

Pour notre application, une des transactions nécessaires est l'inscription d'une commande. Cette inscription nécessite d'écrire la commande mais également de rajouter cette commande au champ "baskets" de chaque plat. Nous devons également inscrire la date de création de la commande au moment de son inscription en base de données.


public static void inscrireUneCommande(Basket b){
 // recuperer l'EntityManager
 EntityManagerFactory emf = Persistence.createEntityManagerFactory("RestoPresto");
 EntityManager em = emf.createEntityManager();
 EntityTransaction transaction  = em.getTransaction();
 try{
  transaction.begin();
  b.setCreationDate(new Date());
  em.persist(b);
  em.flush();
  // mise à jour de la relation pour chaque plat
  for (FoodItem item : b.getFoodItems()){
   // ajout de l'élément dans la liste des paniers auxquels appartient le plat
   item.getBaskets().add(b);
   // prise en compte des changements dans l'objet Java.
   em.merge(item);
  }
  transaction.commit();
 }catch(Throwable thrown){
  thrown.printStackTrace();
 }
}

EntityManager, flush et clear

Les objets géré par un EntityManager et la BD ne sont pas toujours exactement synchronisés suite à une écriture en BD. Nous allons voir les principales causes et les solutions:

  • Exécution retardée: quand on demande un ensemble d'opérations de persistence, elles ne sont pas toujours exécutées immédiatement ou de manière synchrone. Autrement dit, quand on crée un objet, notre EntityManager va souvent continuer à effectuer d'autres opérations pour ne pas attendre la BD. Parfois, vous aurez besoin de forcer votre EntityManager à effectuer une opération pour que la suite puisse se faire. Dans ce cas, la méthode flush() de l'EntityManager permet de forcer l'exécution des opérations en attente.
  • Objet non relâché: un EntityManager gère des objets en lien avec la BD. Il arrive qu'un EntityManager X rapporte un objet A en mémoire. Si un autre EntityManager Y effectue des modifications sur l'objet A, il se peut que X ne les voient pas même si vous effectuez un nouveau find. La raison est que X maintient un cache et ne recherche pas en BD. Ici deux solutions: i) utiliser la méthode clear() sur X afin de vider tous les objets gérés ce qui forcera X à accéder à la BD à nouveau ii) utiliser un EntityManager neuf pour chaque opération.

Une des questions importante est le nombre d'EntityManager et leur possible réutilisation. La réutilisation d'un EntityManager est possible mais elle peut mener à des situations difficiles à démêler pour un programmeur débutant.

Mon conseil pour apprendre est d'acquérir un nouvel EntityManager pour chaque transaction/opération que vous effectuez en BD. (Vous trouverez dans le projet une nouvelle facade qui illustre cette approche)


Gestion des exceptions commit et rollback implicite

JPA nous fournit une solution simple pour gérer les rollbacks. Si une exception est levée pendant l'exécution d'une transaction, il va effectuer un rollback implicite. Il reste possible d'intercepter l'exception avec un bloc try catch Java pour réagir en conséquence.


Fichiers du projet

Vous trouverez le projet avec les deux exemples de transaction et la transaction permettant d'écrire une commande ici. Plusieurs changements ont été faits dans plusieurs fichiers non décrits dans le blog. Les transactions se trouvent dans une classe appelée Transactions.


Lectures d'approfondissement

Il est intéressant de regarder plus en profondeur les problèmes de désynchronisation entre les objets en mémoire et les objets qui persistent, pour cela deux références ici et ici.

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.