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.







lundi 29 octobre 2012

Java Serveur App 8 : JPA et les relations

Relations

Une entité peut contenir des champs entier, des chaines de caractères ou encore des dates. 

Maintenant, nous allons aussi vouloir permettre à une entité de faire référence à une autre entité. C'est là qu'interviennent les relations.

OneToMany et ManyToOne

Un restaurant a plusieurs plats. Chaque plat n'appartient qu'à ce restaurant. On a donc une relation de 1 restaurant vers plusieurs plats. Dans l'autre sens de cette relation, plusieurs plats sont reliés à un restaurant. Cela nous mène au code suivant:



package com.deguet.model;

import java.io.Serializable;
import java.util.List;

import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;


@Entity
public class Restaurant implements Serializable {
 private static final long serialVersionUID = 1L;

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

 private String name;

 //bi-directional many-to-one association to FoodItem
 @OneToMany(fetch = FetchType.LAZY, cascade={CascadeType.ALL}, orphanRemoval = true)
 private List<FoodItem> foodItems;

    public Restaurant() {
    }

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

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

 public String getName() {
  return this.name;
 }

 public void setName(String name) {
  this.name = name;
 }

 public List<FoodItem> getFoodItems() {
  return this.foodItems;
 }

 public void setFoodItems(List<FoodItem> foodItems) {
  this.foodItems = foodItems;
 }
 
}
Le champs qui représente la relation est foodItems qui contiendra la liste des plats du restaurant.

Le code d'un plat (FoodItem) contient également une référence vers le restaurant auquel ce plat appartient. C'est ce qu'on appellera une relation bidirectionnelle.

package com.deguet.model;

import java.io.Serializable;
import java.util.List;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;

import com.restooo.model.Basket;
import com.restooo.model.Restaurant;

@Entity
public class FoodItem implements Serializable {
 private static final long serialVersionUID = 1L;

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

 private String name;

 private String description;

 private Double price;

 //bi-directional many-to-many association to Basket
 @ManyToMany(mappedBy="foodItems")
 private List<Basket> baskets;

 //bi-directional many-to-one association to Restaurant
    @ManyToOne
 private Restaurant restaurant;

 public String getName() {
  return name;
 }

 public void setName(String name) {
  this.name = name;
 }

 public FoodItem() {
 }

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

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

 public String getDescription() {
  return this.description;
 }

 public void setDescription(String description) {
  this.description = description;
 }

 public Double getPrice() {
  return price;
 }

 public void setPrice(Double price) {
  this.price = price;
 }

 public List<Basket> getBaskets() {
  return baskets;
 }

 public void setBaskets(List<Basket> baskets) {
  this.baskets = baskets;
 }

 public Restaurant getRestaurant() {
  return restaurant;
 }

 public void setRestaurant(Restaurant restaurant) {
  this.restaurant = restaurant;
 }

}



ManyToMany

Une commande contient plusieurs plats en quantité différentes. Nous allons appeler notre commande Basket (Order est un mot réservé SQL). Nous y voyons le côté principal est celui de la commande à laquelle on ajoutera des plats se déclare par le champs foodItems. 


package com.deguet.model;

import java.io.Serializable;
import java.util.List;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;


/**
 * The persistent class for the Basket database table.
 * 
 */
@Entity
public class Basket implements Serializable {
 private static final long serialVersionUID = 1L;

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

 private String test;
 
 //bi-directional many-to-many association to FoodItem Owning SIDE
 @ManyToMany
    @JoinTable(
           name="basket_fooditem",
           joinColumns={@JoinColumn(name="BASKET_ID", referencedColumnName="id")},
           inverseJoinColumns={@JoinColumn(name="FOODITEM_ID", referencedColumnName="id")})
 private List<FoodItem> foodItems;

    public Basket() {
    }

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

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

 public List<FoodItem> getFoodItems() {
  return this.foodItems;
 }

 public void setFoodItems(List<FoodItem> foodItems) {
  this.foodItems = foodItems;
 }

 public String getTest() {
  return test;
 }

 public void setTest(String test) {
  this.test = test;
 }
 
}

Lazy fetch

Les relations et les méthodes getXXX correspondantes permettent une méthode de chargement retardée appelée Lazy Loading en anglais. L'objet relié n'est chargé que quand la méthode getXXX est appelée.


Exemple: un restaurant avec 350 plats est chargé en mémoire. La requête récupère juste le restaurant. Ensuite, si on exécute un getFoodItems() sur le champ contenant les plats, une autre requête est faite à la base de données.


Création d'un restaurant et de ses plats

Pour illustrer la création d'entité ainsi que d'autres entités liées, nous allons créer un restaurant avec plusieurs centaines de plats à travers notre code. Pour cela nous allons créer une classe InitialLoad avec le code suivant:

package com.deguet;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Random;

import com.deguet.model.FoodItem;
import com.deguet.model.Restaurant;

public class InitialLoad {

 
 private static Facade<Restaurant> rf = new Facade<Restaurant>(Restaurant.class);
 
 public static void load(){
  String[] types = {"Pizza ","Sushi ","Patate ","Tacos ","Pita ","Cassoulet ","Paela ","Quiche "};
  String[] noms  = {"Réjean ","Paulo ","Joris ","Claudette ","Jean ","Paul ","Pierre ","Pedro ","Bertha ","Johnny "};
  String[] plats = {"du jour ","fancy ","italiana ","québécoise ","pour trois","à la mode de chez nous",
    "espanola","qui tue ", "qui pique","pas cher","pour deux","pour toute la famille","du Colonel Sanders"};
  Random rand = new Random();
  for (String type : types){
   for (String nom : noms){
    Restaurant r = new Restaurant();
    List<FoodItem> dishes = new ArrayList<FoodItem>();
    r.setName(type+nom);
    r.setFoodItems(dishes);
    rf.create(r);
    Collections.shuffle(Arrays.asList(plats));
    for (int i = 0 ; i < 5 ; i++)
    {
     String plat = plats[i];
     FoodItem item = new FoodItem();
     item.setRestaurant(r);
     item.setDescription("Bla ingrédients ...");
     item.setName(type+plat);
     item.setPrice((rand.nextInt(100)+50)/10.0);
     dishes.add(item);
    }
    rf.edit(r);
   }
  }
 }

}


persistence.xml

Si vous oubliez de mentionner les nouvelles classes à JPA dans votre persistence.xml, vous aurez un message d'erreur "bli.bla.Blo  is not a known entity type".


Fichiers et lecture supplémentaire

Vous trouverez ici le zip du projet.

Une lecture supplémentaire fortement recommandée est le wikibook sur JPA.

mardi 23 octobre 2012

Java Serveur App 7 : JPA et CRUD

Modèle : JPA, les ORM

Le modèle correspond aux données de notre système. Il s’agit de savoir comment stocker l’information. Un Object Relationship Mapper s'assure de faire le lien entre un modèle objet et une base de données relationnelle. Le lien peut se faire dans 2 directions:


  • Créer automatiquement des classes objets à partir d'une base de données existante.
  • Créer des classes objets et demander la génération automatique d'une base de données relationnelle qui stockera ces objets.
C'est cette seconde possibilité que nous allons utiliser dans notre application.

pom.xml : EclipseLink

EclipseLink est l'implémentation de référence de la norme JPA. Il s'agit donc d'un des nombreux moteurs permettant de stocker des objets en base de données. Il existe d'autres implémentations, la plus connue étant certainement Hibernate. Afin d'inclure EclipseLink, il va nous falloir ajouter un repository dans notre pom.xml. Ajoutez la balise suivante juste avant la balise dependencies de votre pom.xml:

<repositories>
 <repository>
  <id>eclipselink</id>
  <url>http://www.eclipse.org/downloads/download.php?r=1&amp;nf=1&amp;file=/rt/eclipselink/maven.repo/</url>
 </repository>
</repositories>

Ensuite, ajoutez la dépendance suivante:
<dependency>
 <groupId>org.eclipse.persistence</groupId>
 <artifactId>eclipselink</artifactId>
 <version>2.4.0</version>
</dependency>

pom.xml : Derby (Apache)

Apache Derby est une base de données qui peut être embarquée. Autrement dit, elle se lance quand le serveur est lancé. Nous l'utiliserons ici pour simplifier puisqu'il n'y a pas besoin d'installer de serveur de base de données séparé.


<dependency>
 <groupId>org.apache.derby</groupId>
 <artifactId>derby</artifactId>
 <version>10.9.1.0</version>
</dependency>

<dependency>
 <groupId>org.apache.derby</groupId>
 <artifactId>derbyclient</artifactId>
 <version>10.9.1.0</version>
</dependency>
JPA, première entité

JPA fonctionne en déclarant des classes comme des entités. Une entité va être un classe Java pour laquelle nous allons déclarer un champ identifiant. Voici le code de notre première entité:


package com.deguet.model;

import java.io.Serializable;
import javax.persistence.*;

@Entity
public class Test implements Serializable {
 
 @Id
 @GeneratedValue(strategy=GenerationType.IDENTITY)
 private int id;

 private String content;
 
 public int getId(){return this.id;}
 public void setId(int idImages) {this.id = idImages;}
 
 public String getContent() {return content;}
 public void setContent(String content) {this.content = content;}
}

Cette classe est placée dans le package com.deguet.model. La différence la plus importante est ici la présence d'un identifiant id. En effet, la base de données a besoin d'un identifiant explicite. (ce n'est pas le cas en objet, l'objet étant identifié par sa référence, implicite).

persistence.xml

JPA se repose sur le fichier persistence.xml pour décrire la base de données à utiliser et les classe Java faisant partie du modèle. Le fichier persistence.xml doit se trouver dans WEB-INF/classes/META-INF/persistence.xml.


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0"
  xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
  <persistence-unit name="RestoPresto">
    <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
    <class>com.deguet.model.Test</class>
    <properties>
      <property name="javax.persistence.jdbc.password" value="app" />
      <property name="javax.persistence.jdbc.user" value="app" />
      <property name="javax.persistence.jdbc.driver" value="org.apache.derby.jdbc.ClientDriver" />
      <property name="javax.persistence.jdbc.url" value="jdbc:derby:databasederby/resto;create=true" />
      <property name="eclipselink.ddl-generation" value="drop-and-create-tables" />
      <property name="eclipselink.drop-ddl-jdbc-file-name" value="ddl.jdbc"/>
      <property name="eclipselink.ddl-generation.output-mode" value="both" />
      <property name="eclipselink.logging.level" value="INFO"/>
    </properties>
  </persistence-unit>
</persistence>

Nous avons défini une unité de persistence. Vous remarquerez les points suivants:

  • Chaque classe d'entité (ici Test) doit être déclarée dans le fichier.
  • l'URL pour la base de données derby est en fait un chemin vers le répertoire databasederby qui sera créé dans le répertoire de votre projet.
  • la propriété ddl-generation indique à EclipseLink qu'il doit détruire et recréer la base de données à chaque exécution (drop and create).
  • la propriété drop-ddl-jdbc-file-name permet de récupérer les scripts de création de la base (dans le fichier ddl.jdbc) afin de déboguer un éventuel problème à la génération de la base.


Facade CRUD pour notre entité


JPA se repose sur un EntityManager pour effectuer les opérations en base de données.

Les opérations CRUD (Create Retrieve Update Delete) nécessite d'accéder à un EntityManager afin d'exécuter les requêtes correspondantes. Afin de faciliter la compréhension voici une classe permettant d'effectuer les opérations de base (création, mise à jour, récupération ou suppression) pour une entité T:



package com.deguet;

import java.util.List;
import javax.persistence.*;

public class Facade<T> {
 
  Class<T> entityClass;

  @PersistenceContext(unitName = "RestoPresto")
  private static EntityManager em;
  protected EntityManager getEntityManager() {return em;}

  public Facade(Class<T> entityClass) {
    if (em == null){
      EntityManagerFactory emf = Persistence.createEntityManagerFactory("RestoPresto");
      em = emf.createEntityManager();
    }
    this.entityClass = entityClass;
  }

  public void create(T entity) {
    EntityManager em = getEntityManager();
    em.getTransaction().begin();
    if (!em.contains(entity)) {
      em.persist(entity);
      em.flush();
    }
    em.getTransaction().commit();
  }

  public void edit(T entity) {
    EntityManager em = getEntityManager();
    em.getTransaction().begin();
    em.merge(entity);
    em.flush();
    em.getTransaction().commit();
  }

  public void remove(T entity) {
    EntityManager em = getEntityManager();
    em.getTransaction().begin();
    T toDelete = em.merge(entity);
    em.remove(toDelete);
    em.flush();
    em.getTransaction().commit();
  }

  public T find(Object id) {
    return getEntityManager().find(entityClass, id);
  }

  public List<T> findAll() {
    javax.persistence.criteria.CriteriaQuery cq = getEntityManager().getCriteriaBuilder().createQuery();
    cq.select(cq.from(entityClass));
    return getEntityManager().createQuery(cq).getResultList();
  }

  public int count() {
    javax.persistence.criteria.CriteriaQuery cq = getEntityManager().getCriteriaBuilder().createQuery();
    javax.persistence.criteria.Root<t> rt = cq.from(entityClass);
    cq.select(getEntityManager().getCriteriaBuilder().count(rt));
    javax.persistence.Query q = getEntityManager().createQuery(cq);
    return ((Long) q.getSingleResult()).intValue();
  }
}

Test dans le contrôleur


Pour tester que tout fonctionne correctement, nous allons maintenant tester la création d'un objet puis la récupération de la liste d'objets dans un nouveau motif URL dans notre contrôleur.

Tout d'abord vous devez créer une facade pour notre entité avec la ligne suivante:


Facade<Test> facade = new Facade<Test>(Test.class);


Ensuite il faut créer le motif URL permettant de déclencher la création d'un nouvel objet, sa sauvegarde puis la récupération de la liste complète.

if (action.equals("/testCharge")){
System.out.println("Controler GET acheter ");
Test t = new Test();
t.setContent("bli blo " + System.currentTimeMillis());
facade.create(t);
List<Test> tests = facade.findAll();
for (Test test : tests){
System.out.println("Le test est " + test);
}
return;
}

En rechargeant plusieurs fois la page, http://localhost:7070/testCharge vous verrez la liste d'entités s'allonger.

Fichiers du projet avec une entité

Vous trouverez le projet avec l'écriture d'une entité en base de données ici.



dimanche 21 octobre 2012

Java Serveur App 6 : attributs de requête/session/contexte

Jusqu'ici, nous avons vu:
  • les pages JSP et leur balises
  • les motifs URL et les liens avec une Servlet contrôleur
  • les différents mode de redirection
Le problème est que nous n'avons pas de moyen de partager des objets Java entre différentes pages ou encore entre le contrôleur et une page. Nous allons voir ici comment mettre en place un tel partage.

Gestion des attributs

La requête HTTP (dans les pages ou dans la contrôleur) dispose de deux méthodes appelées setAttribute et getAttribute. Ces méthodes permettent de stocker ou de récupérer un objet dans dans la requête.

L'exemple classique est le suivant:
  1. Votre classe contrôleur veut passer un objet nommé panier de classe Panier à la vue JSP.
  2. Dans le code du contrôleur, vous effectuez l'instruction

    request.setAttribute("panier",panier);

  3. Dans le code de la page JSP, vous effectuez l'instruction

    Panier recupere = (Panier) request.getAttribute("panier");


Requête, session et contexte

Il existe plusieurs portées pour les attributs :

  • la requête HttpRequest permet de gérer des attributs, le cas le plus courant est une servlet qui passe un objet à une page JSP dans le cas d'une redirection via le requestDispatcher.
  • la session HttpSession permet de gérer des attributs pour l'ensemble d'une session, c'est à dire l'ensemble des pages de votre application pour un client.
  • le contexte ServletContext permet des attributs partagés pour toutes les sessions de  l'application.
Pour chaque objet que vous souhaitez partager via un attribut, il faut déterminer quelle sera 
  1. sa durée de vie: un attribut de contexte dure tant qu'on ne le supprime pas ou qu'on n'arrête pas le serveur. Un attribut de requête disparaît avec la requête.
  2. son utilisateur: si un élément est spécifique à un client, ce sera soit la requête soit la session. Sinon ce sera le contexte.
Exemple de la commande en session

Nous allons voir comment gérer un objet commande (similaire à un panier d'achats) dans notre session. Le code source de la classe Java modélisant une commande sera : 

package com.deguet;

import java.util.HashMap;
import java.util.Map;

public class CommandeEnSession {

 public Map<String, Integer> items;
 
 public CommandeEnSession(){
  items = new HashMap<String, Integer>();
 }
 
 public void addItem(String name){
  if (items.containsKey(name)) items.put(name, items.get(name)+1);
  else items.put(name, 1);
 }
 
 public int totalQuantity(){
  int result = 0;
  for (String name: items.keySet()) result += items.get(name);
  return result;
 }
 
 public int itemQuantity(){
  return items.size();
 }
 
 public String toString(){
  String result = "";
  for (String name: items.keySet())
   result += "("+name+" : "+items.get(name)+")";
  return result;
 }
}


Maintenant, pour ajouter des éléments à cette commande, l'action dans les vues devra ressembler à :


<a href="ajouterCommande?name=Pizza pour trois">Ajouter</a>

Cette action va déclencher le motif URL "ajouterCommande" avec comme paramètre le nom du plat. Il faut écrire le code contrôleur qui réagira à cette action:

if (action.equals("/ajouterCommande")){
 // récupération du paramètre de l'ajout, le plat à ajouter
 String plat = request.getParameter("name");
 System.out.println("Controler GET ajouter le plat "+plat);
 // si la commande n'existe pas dans la session, la créer
 CommandeEnSession commande = (CommandeEnSession) request.getSession().getAttribute("commande");
 if (commande == null) {
  commande = new CommandeEnSession();
  request.getSession().setAttribute("commande",commande);
 }
 // ajouter le plat
 commande.addItem(plat);
 System.out.println("La commande est  "+commande);
 // rediriger vers le menu
 response.sendRedirect("/menu");
 return;
}




Afin de vérifier que tout fonctionne correctement, nous allons maintenant voir comment afficher le panier dans la page commande.jsp:

<table class="table table-striped">
 <thead>
  <tr>
   <th>Plat</th>
   <th>Quantité</th>
  </tr>
 </thead>
 <tbody>
 <% 
 // si il existe une commande
 CommandeEnSession commande = (CommandeEnSession) session.getAttribute("commande");
 if (commande != null){
  for (String name : commande.items.keySet()){
   Integer qty = commande.items.get(name);
   %>
   <tr>
    <td><%=name %></td>
    <td><%=qty %></td> 
   </tr>
   <%
  }
 }
 %>       
 </tbody>
</table>


Nous avons vu comment créer un objet d'une classe Java, y ajouter des items et afficher son état dans une page JSP.




Fichier du restaurant avec commande gérée en session

Vous trouverez ici l'exemple du projet avec une gestion simple d'une commande en session. Vous trouverez également un widget dans l'entête JSPF permettant d'afficher l'état de la commande si il en existe une.