Les fêtes sont passées, voici la deuxième partie de mon article sur Grails consacrée à l’écriture de l’application Zencontact. Dans le premier article j’ai expliqué comment installer Grails et débuter son projet. Figurez-vous que depuis ce dernier article, j’ai trouvé comment utiliser les 2 librairies Prototype et Scriptaculous. Parfois il faut juste chercher un peu… Bref la version 2 est plus proche du cahier des charges de Zenika. Il ne manque presque rien je pense pour remplir le cahier des charges.

Aujourd’hui au programme :
– comment mettre à jour sa version de Grails ?
– Itérer une collection et afficher les résultats

Mettre à jour son application vers une nouvelle version de Grails
Pendant le break de la semaine de Noël, l’équipe de Grails a annoncé la sortie de la version 1.2 du framework Grails. C’est parfait, cela va me permettre de vous montrer comment mettre à jour votre version de Grails, ainsi que notre projet zencontact.
– Renommez votre ancien répertoire C:\grails en C:\grails-old par exemple
– Téléchargez et décompressez Grails 1.2 dans le répertoire C:\grails
– Téléchargez le projet de démarrage pour cet article, qui a été allégé depuis la version 1 via ce lien : grails_zencontactdemo_debut_article2.tar.gz
– Placez-vous à la racine du projet zencontactdemo après avoir décompressé l’archive
– Tapez simplement grails upgrade
– Faites aussi le ménage avec grails clean avant de relancer le serveur avec grails run-app afin de tester la mise à jour

Le script de mise à jour de Grails démarre. Nous pouvons constater que la version utilisée est bien la 1.2. Suivez les instructions, en principe cela ne devrait pas poser de problèmes.

macbook-pro-de-nicolas-martignole:zencontactdemo nicolas$ grails upgrade
Welcome to Grails 1.2.0 - http://grails.org/
Licensed under Apache Standard License 2.0
Grails home is set to: /Users/nicolas/Dev/grails/grails

Base Directory: /Users/nicolas/Dev/Zenika/zencontactdemo/zencontactdemo
Resolving dependencies...
Dependencies resolved in 1502ms.
Running script /Users/nicolas/Dev/grails/grails/scripts/Upgrade.groovy
Environment set to development
NOTE: Your application currently expects grails version [1.2-M4], this target will upgrade it to Grails 1.2.0 ...

		WARNING: This target will upgrade an older Grails application to 1.2.0.
		However, tag libraries provided by earlier versions of Grails found in grails-app/taglib will be removed.
		The target will not, however, delete tag libraries developed by yourself.
		Are you sure you want to continue?
				    (y, n)

Lors du premier redémarrage, vous verrez que Grails met aussi à jour les Plugins, ce qui peut parfois poser des soucis avec les versions « beta ». Mais ici, s’agissant de la version 1.2 il n’y aura aucuns soucis.

Les améliorations de la version 1.2 de Grails
La release notes de la version 1.2 est longue comme un guide d’installation de Windows. Cette version a été optimisée pour les performances. Spring 3.0 est utilisé par Grails et devient aussi utilisable pour votre code. N’importe quelle classe (java ou groovy) peut donc être annotée avec @Component afin d’être injectée dans une autre classe. Concernant la gestion des transactions, il est possible de définir un niveau de granularité plus bas pour les transactions, là où auparavant, l’ensemble d’un service Grails était transactionnel ou non. GORM est aussi configurable avec un ensemble de règle par défaut. J’aime beaucoup les nouveaux finders dynamiques qui traitent les Booleans. Enfin le support des requêtes nommées (named queries) la possibilité de faire des mappins « hasOne », la liste est longue.
Attention dans la version 1.2, WebFlow est maintenant un plugin à part qu’il faut installer. Il n’est plus livré par défaut. Basé sur Spring WebFlow, ce plugin est très pratique pour faire des questionnaires, des wizards, des formulaires multi-écrans.
Je pourrai écrire un article complet sur cette nouvelle version, mais croyez-moi, c’est une version très mature et très complète. Je dis cela pour les chagrineurs qui attendent que « Grails soit stable« .

Scriptaculous et Prototype sur la même page
Avec la version 1.2 il est possible d’utiliser les 2 librairies Javascript Prototype et Scriptaculous sans soucis. Il suffit de les déclarer à la suite dans notre fichier main.gsp qui sert de template pour Sitemesh:

<html>
<head>
  <title><g:layoutTitle default="Zencontact"/></title>
  <link rel="stylesheet" href="${resource(dir: 'css', file: 'main.css')}"/>
  <link rel="shortcut icon" href="${resource(dir: 'images', file: 'favicon.ico')}" type="image/x-icon"/>
  <g:layoutHead/>
<!-- ****************************************** //-->
  <g:javascript library="prototype"/>
  <g:javascript library="scriptaculous"/>
<!-- ****************************************** //-->
</head>
<body>
<div id="header">
  <g:link controller="contact" action="index">Accueil</g:link> |
  <g:link controller="contact" action="list">Liste des contacts</g:link> |
  <g:link controller="contact" action="create">Ajouter un contact</g:link>

</div>
<div id="content">
  <div id="contacts">
    <div class="contact">
      <g:layoutBody/>
    </div>
  </div>
</div>
<div id="footer">
  <p id="legal"><a href="http://www.touilleurexpress.fr/">Le Touilleur Express</a> Copyright© 2009-2010</p>
</div>

</body>
</html>

La version de l’article 1
A la fin de l’article 1 il était possible de créer un contact, de le lister, et d’afficher l’heure sur la page d’accueil. Pour vous montrer maintenant une nouvelle version, j’ai préparé une archive du code source du début de l’étape 2.
– Téléchargez le fichier grails_zencontactdemo_debut_article2.tar.gz
– Décompressez-le dans un nouveau répertoire et lancez l’application.

Comme je vous sens un peu fatigué après les fêtes je vous ai fait une vidéo avec ma belle voix que vous pouvez regarder en buvant une tisane (par exemple)

Itérer une collection
Comment itérer une collection et afficher le résultat ? Plus clairement, nous devons lister les contacts et les afficher sur une page. Voyons comment cela fonctionne. Tout d’abord, souvenez-vous, nous utilisons une class Controller avec Grails. Tout se passe dans l’unique class ContactController.groovy.
Pour récupérer la liste des contacts, nous utilisons un « finder ». C’est une méthode statique qui est déclaré sur la classe Contact et qui permet à Grails de vous câbler tout cela sans aucuns efforts. Voici quelques exemples pour expliquer ce principe. Sachez qu’avec IDEA IntelliJ, celui-ci est capable de vous proposer via l’auto-complétion des méthodes, ce qui aide pas mal lorsque vous codez.

// Pour obtenir la liste des Contact
def myList = Contact.list()

// Pour obtenir la liste des utilisateurs dont le nom de famille est Martin
def myList = Contact.findAllByNom("Martin")

// Qui ressemble à Mar
def myList = Contact.findAllByNomLike("Ma%")

// Seulement le premier trouvé
def c1 = Contact.findByNomLike("Ma%")

// Trouve toutes les personnes dont la date de naissance
// est inférieur à la date d'aujourd'hui
def myList=Contact.findAllByDateNaissanceLessThan(new Date())

// Combine 2 attributs pr la recherche
def nicolas=Contact.findByNomAndPrenom("nicolas","martignole")

Que pensez-vous de tout cela ? Pas mal non ? Imaginez que le tout devient des requêtes avec Spring et Hibernate. Il est possible de combiner 2 attributs de la classe Contact au maximum comme sur le dernier exemple. J’adore Grails car c’est précisément ce genre d’astuces qui font gagner du temps.

Pour vous expliquer comment construire la liste des contacts, nous allons casser un peu de code. Reprenez le fichier ContactController. Effacez pour l’instant tout ce qui se trouve dans la méthode list() et utilisez ce que vous venez de voir pour créer un objet myList. Comme vous pouvez le constater, on se fiche pas mal du type ici, à savoir un List. Pour retourner un objet à la vue, il suffit de le placer dans une Map, ce que je fais avec ces crochets à la fin de la méthode. Il n’y a pas de return car la dernière expression évaluée avec Grails est automatiquement retournée. La clé sera « contactInstanceList » et la valeur sera « myList ».

package org.letouilleur.demo

class ContactController {

  static allowedMethods = [save: "POST", update: "POST", delete: "POST"]

  def index = {
    redirect(uri: '/')
  }

  def list = {
    // Recupere la liste des contacts
    def myList=Contact.list()

    // place dans une Map cette liste avec la cle contactInstanceList
    [contactInstanceList: myList]
  }
...

Convention au lieu de configuration : pour afficher le résultat il suffit de créer un fichier list.gsp dans le répertoire views/contact. Ce fichier sera automatiquement appelé par Grails à la fin de l’exécution de la méthode list. Nous allons donc récupérer une liste de Contacts. Pour itérer cette collection du côté de la vue, nous utiliserons le tag <g:each>. Ce tag prend en argument une collection, et vous retourne une variable qu’il suffit d’utiliser. L’attribut status est un compteur très pratique lorsque vous souhaitez afficher une ligne sur deux dans un tableau avec une autre couleur. Il est possible d’utiliser le tag fieldValue ou directement le nom du curseur (ici contactInstanceList) et d’appeler l’attribut qui vous intéresse.

<g:each in="${contactInstanceList}"
                status="i"
                var="contactInstance">
    // version 1
    ${fieldValue(bean: contactInstance, field: "prenom")}
    // version 2 (voir explication)
   ${contactInstance?.nom}
    <br/>
</g:each>

Vous voyez ce point d’interrogation à la ligne 7 ?
C’est un pattern de Groovy appelé Safe Dereferencing qui dit en quelque sorte : « …si tu as une valeur pour contactInstance alors appelle getNom() sinon ne fait rien« . C’est un moyen de gérer les valeurs nulles sans if/then/else supplémentaire.

Voilà comment afficher une collection : appeler l’un des finders, retourner la liste vers la vue et itérer la collection avec un curseur. Pour terminer nous allons afficher les noms des utilisateurs par ordre alphabétique. C’est très simple, il suffit d’utiliser le finder adéquat :

 def list = {
    // Recupere la liste des contacts triee par Nom
    def myList=Contact.listOrderByNom()

    // place dans une Map cette liste avec la cle contactInstanceList
    [contactInstanceList: myList]
  }

Voilà je vous laisse souffler et imaginez ce qu’il faudrait faire en Java avec du Spring et un peu d’Hibernate… Ce que j’aime dans Grails c’est que si vous voulez tester quelque chose rapidement, c’est faisable.

Paginer nos 102020 enregistrements sans pleurer
Tant qu’à faire, nous allons aussi préparer de quoi gérer la pagination. Imaginez que la base de données contienne beaucoup d’enregistrements. Il serait souhaitable de limiter le nombre d’enregistrement retournés et de paginer les résultats non ? Tout d’abord nous ajoutons le tag paginate dans le fichier list.gsp après le tag g:each. Ce tag va générer automatiquement les liens de navigation, le nombre de pages, bref toute l’artillerie nécessaire pour naviguer dans la liste des résultats. Si ce tag ne vous plaît pas, n’oubliez pas que vous pouvez créer le vôtre.
Pour l’instant on note l’apparition d’un nouveau paramètre appelé contactInstanceTotal dans la vue :

<g:each in="${contactInstanceList}"
                status="i"
                var="contactInstance">
    // version 1
    ${fieldValue(bean: contactInstance, field: "prenom")}
    // version 2 (voir explication)
   ${contactInstance?.nom}
    <br/>
</g:each>
  <g:paginate total="${contactInstanceTotal}"/>

Je modifie maintenant le code du Controller afin de gérer la pagination. Il est intéressant de vous expliquer en détail ce qui se passe dans ces 2 lignes car nous avons là plusieurs notions qui n’existent pas en Java, et qui viennent du monde de Groovy.

  def list = {
    params.max = Math.min(params.max ? params.max.toInteger() : 10, 100)
    [contactInstanceList: Contact.listOrderByNom(params), contactInstanceTotal: Contact.count()]
  }

Tout d’abord les paramètres de la requête HTTP (GET ou POST) sont automatiquement placés dans une Map appelé params.
Vous pouvez écrire params.get(« max ») ou directement params.max afin de récupérer la valeur d’un paramètre HTTP.

params.max

Ensuite vous reconnaissez l’opérateur ternaire du monde java qui dit « si le parametre max est spécifié alors prendre sa valeur en le transformant en un Interger, sinon prend 10 »

params.max ? params.max.toInteger() : 10

Cela me permet de vous donner un petit cours de Groovy et de vous parler de l’opérateur Elvis. L’opérateur Elvis comme en Java est un opérateur ternaire (If… then… else…) qui se lit de cette façon :

<condition> ? <expr1> : <expr2>

Si la condition est vraie alors l’opérateur 1 est évalué, sinon ce sera l’opérateur 2. Nous pourrions par exemple écrire en Java ce bout de code qui affecte le nom de la personne :



String name = (person.getName() != null) ? person.getName() : "unknown"

Groovy étant capable de simplifier cela, un nouvel opérateur noté ?: permet de raccourcir cette expression simplement :


// groovy
def name = person.getName() ?: "unknown"

L’opérateur Elvis en Groovy retourne le contenu de ce qu’il a sur la tête (à gauche) si cette expression est vraie, sinon ce qu’il a en dessous (à droite).

Reprenons les quelques lignes de la méthode list de ContactController :


  def list = {
    params.max = Math.min(params.max ? params.max.toInteger() : 10, 100)
    [contactInstanceList: Contact.listOrderByNom(params), contactInstanceTotal: Contact.count()]
  }

La première ligne s’assure que le paramètre max existe et qu’il est dans une plage de valeur authorisée. La seconde ligne passe directement la map « params » à la méthode listOrderByNom. Grails utilise à mort ce principe de convention au lieu de la configuration. Il se trouve que cette méthode listXXX recherche automatiquement une clé « max » pour toute Map que vous lui passez en argument… Là je reconnais qu’il faut lire la documentation pour le savoir. Nous perdons en expressivité ce que l’on gagne en raccourci. Bref ce paramètre max va permettre de limiter le nombre d’enregistrements retournés. Enfin pour initialiser le nombre de contacts, nous utilisons la méthode count qui se charge de vous donner le nombre de contacts en base. C’est simple non ?

Faire un lien vers une autre page (méga super dur)
Bon quoi d’autre ? Il faut afficher une image devant chaque contact et créer un lien sur une image afin d’aller vers la page édition lorsque la personne clique dessus. Voici tout d’abord le code partiel de la page list.gsp :

// Iteration sur la liste des contacts tries par nom
<g:each in="${contactInstanceListByName}"
                status="i"
                var="contactInstance">
     <img src='<g:resource dir="images" file="people.png"/>' alt="icon"/>
     ${contactInstance.prenom}
     ${contactInstance.nom}

     <g:link action="show" id="${contactInstance.id}">
       <img src="${resource(dir: 'images', file: 'edit.png')}" alt="edit" border="0"/>
     </g:link>
     <br/>
   </g:each>
   <div class="paginateButtons">
     <g:paginate total="${contactInstanceTotal}"/>
   </div>

<div class="contact">
  <g:include action="create" controller="contact"/>
</div>

Ligne 5 : chargement d’une image
Ligne 9 : utilisation du tag g:link. Nous allons appeler la méthode show du controleur ContactController. A noter que vous pouvez spécifier un autre controleur, que vous pouvez passer des paramètres, etc. Ce tag est assez puissant, la documentation vous donnera un aperçu de ce qu’il est possible de faire facilement.
Ligne 14/15 et 16 nous retrouvons notre pagination avec une feuille de style pour l’affichage.

Dans le cahier des charges de Zenika il est demandé aussi d’utiliser une autre page et de l’inclure dans la page qui liste les contacts. Le tag g:include permet de spécifier le nom d’un controller et d’une action précise afin de charger la vue. Ligne 19 nous appelons l’action create de ContactController, et voilà c’est tout pour l’instant.

Reste à faire
Il reste maintenant 2 étapes compliquées :
– mettre en place l’édition « in-place » du nom de famille
– mettre en place un formulaire Ajax afin d’ajouter rapidement un nouvel utilisateur

Nous verrons dans le troisième article comment écrire un tag personnalisé et ensuite comment mettre en place un formulaire Ajax en 3mn top chrono.

A demain pour la suite !

8 réflexions sur « Grails étape 2: itérer une collection et afficher le résultat »

  1. me semble qu’il manque un chouia d’explication sur la partie pagination:
    => Tu dis que listOrderBy() cherche automatiquement une clé max dans la map passée en paramètre, mais ça ne suffit pas :). la méthode prend aussi automatiquement en compte l’offset (et l’order asc/desc), parce que sinon ta pagination elle va pas bien fonctionner me semble, non?

    Pour ceux que ça interesse: http://grails.org/doc/latest/ref/Domain%20Classes/listOrderBy.html et http://grails.org/doc/latest/ref/Domain%20Classes/list.html

  2. Pendant que j’y suis, pour la partie sur l’opérateur elvis, je ne vois pas le rapport avec le code que tu expliques (où je ne vois pas apparaitre l’opérateur elvis mais l’utilisation classique du condition ? sivrai:sifaux. Par contre, tu peux parler de Groovy Truth, parce qu’en java, params.get(toto) ça va pas tout le temps retourner un boolean, alors qu’avec Groovy, params.toto , oui.)

    http://docs.codehaus.org/display/GROOVY/Groovy+Truth
    http://mrhaki.blogspot.com/2009/08/groovy-goodness-elvis-operator.html
    http://mrhaki.blogspot.com/

    (je pense que tu avais peut-être l’opérateur elvis dans ton code de départ mais que tu as du forcer le type de retour…et du coup, elvis is gone)

  3. @Christophe : tu as tout juste, merci pour le complément d’information. J’y ai passé quelques heures et j’avoue qu’à la fin de l’article j’ai un peu relaché les explications et les détails. C’est long à écrire ce genre d’article, on ne s’en rend pas très bien compte.

  4. Encore un article très intéressant sur le sujet, merci!

    « Il est possible de combiner 2 attributs de la classe Contact au maximum » => et si j’en veux 3 ? Je dois coder ma méthode ? C’est parce que ça fait des noms trop longs à lire ?

    Symfony/Propel (Php) propose ce type de finder, avec en prime des notions de jointures pour tout récupérer d’un coup, du genre : findAllJoinAddress, findAllJoinAll, findAllJoinAllExceptAddress …

    Par pure curiosité… ça existe en Grails ?

  5. @Piwaï : Grails introduit ce que l’on appelle « Query by Example » QBE. Imaginons que tu cherches l’ensemble des utilisateurs correspondant à trois critères ou plus. Il te suffit de créer un Contact que l’on appelle userLikePiwai par exemple:
    def userLikePiwai = new Contact(nom:’waï’, prenom:’pierre’, email: ‘piwai@test.fr’)

    et ensuite de t’en servir pour lancer une recherche
    def listOfResults= Contact.findAll(userLikePiwai)

    Le principe est donc de créer un objet et de placer les attributs de cet objet comme étant des critères de restriction, puis de le passer à la méthode find ou findAll.

    Ensuite tu peux aller plus loin et utiliser le moteur de construction. Disons que tu souhaites la liste des Pierre M* dont la date de naissance est dans une plage de valeur. Voilà ce que cela donne :

    def entries = Contact.createCriteria().list {
    like(‘nom’, ‘M%’)
    and {
    between(‘dateNaissance’,new Date()-50,new Date())
    eq(‘prenom’, ‘Pierre’)
    }
    maxResults(10)
    order(‘nom’, ‘desc’)
    }

    Enfin tu peux utiliser le langage HQL directement si tu préfères, mais je trouve cela moins lisible.
    Regarde
    http://www.grails.org/Hibernate+Criteria+Builder

  6. @Piwaï: Grails propose les dynamic finders, qui sont effectivement limités à l’équivalent d’une clause where sur 2 colonnes d’une même table. Puis, le Query By Example, mais là encore, ce n’est pas tjs suffisant. Pour aller plus loin, voir la notion de Criteria où là tu peux te lacher. Tu peux aussi directement du Hibernate Query Language.

    Je conseille vivement l’investissement dans Grails In Action pour ceux qui ont envie d’en savoir plus. De plus, y a souvent des promos chez Manning en cette periode de fête (ex: hier, Groovy in action Rev 2 pour 10$). Aujourd’hui (le 31/12), 50% sur tous les bouquins Spring => http://archive.constantcontact.com/fs043/1101335703814/archive/1102861583978.html (dont Groovy et Grails in action)

Les commentaires sont fermés.