Accueil > Non classé > Grails – épisode 3 – Branchement des contrôleurs/vues/services

Grails – épisode 3 – Branchement des contrôleurs/vues/services

Résumé de l’épisode précédent …

Les objets du domaine ont été créés et mappés. Le premier lancement à permis de créer automatiquement la base de données. Il est maintenant temps de faire quelque chose de ces objets …

Relire l’épisode 2.

Création des contrôleurs

Les contrôleurs sont le cœur de l’application. Grails nous offre la possibilité de générer le code (quasi complet) des contrôleurs. Chaque objet du domaine se voit attribuer un contrôleur attitré.

Commençons par l’objet Song.

1
grails generate-controller com.excilys.grails.Song

Cette commande Grails génère donc le contrôleur spécialisé dans le traitement des objets Song. Jetons un œil sur le code produit:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
package com.excilys.grails

class SongController {

    def index = { redirect(action:list,params:params) }

    // the delete, save and update actions only accept POST requests
    static allowedMethods = [delete:'POST', save:'POST', update:'POST']

    def list = {
        params.max = Math.min( params.max ? params.max.toInteger() : 10,  100)
        [ songInstanceList: Song.list( params ), songInstanceTotal: Song.count() ]
    }

    def show = {
        def songInstance = Song.get( params.id )

        if(!songInstance) {
            flash.message = "Song not found with id ${params.id}"
            redirect(action:list)
        }
        else { return [ songInstance : songInstance ] }
    }

    def delete = {
        def songInstance = Song.get( params.id )
        if(songInstance) {
            try {
                songInstance.delete(flush:true)
                flash.message = "Song ${params.id} deleted"
                redirect(action:list)
            }
            catch(org.springframework.dao.DataIntegrityViolationException e) {
                flash.message = "Song ${params.id} could not be deleted"
                redirect(action:show,id:params.id)
            }
        }
        else {
            flash.message = "Song not found with id ${params.id}"
            redirect(action:list)
        }
    }

    def edit = {
        def songInstance = Song.get( params.id )

        if(!songInstance) {
            flash.message = "Song not found with id ${params.id}"
            redirect(action:list)
        }
        else {
            return [ songInstance : songInstance ]
        }
    }

    def update = {
        def songInstance = Song.get( params.id )
        if(songInstance) {
            if(params.version) {
                def version = params.version.toLong()
                if(songInstance.version > version) {

                    songInstance.errors.rejectValue("version", "song.optimistic.locking.failure", "Another user has updated this Song while you were editing.")
                    render(view:'edit',model:[songInstance:songInstance])
                    return
                }
            }
            songInstance.properties = params
            if(!songInstance.hasErrors() && songInstance.save()) {
                flash.message = "Song ${params.id} updated"
                redirect(action:show,id:songInstance.id)
            }
            else {
                render(view:'edit',model:[songInstance:songInstance])
            }
        }
        else {
            flash.message = "Song not found with id ${params.id}"
            redirect(action:list)
        }
    }

    def create = {
        def songInstance = new Song()
        songInstance.properties = params
        return ['songInstance':songInstance]
    }

    def save = {
        def songInstance = new Song(params)
        if(!songInstance.hasErrors() && songInstance.save()) {
            flash.message = "Song ${songInstance.id} created"
            redirect(action:show,id:songInstance.id)
        }
        else {
            render(view:'create',model:[songInstance:songInstance])
        }
    }
}

Pour chaque action CRUD, Grails a généré une méthode (parfois deux) spécifique.

  • list : permettra d’afficher la vue “list”. Le code permet déjà la pagination et le tri sur la sélection.
  • show: afficher un objet Song
  • save: permettra de valider puis d’enregistrer des objets Song depuis un formulaire. Ce formulaire sera affiché grâce à la méthode “create”
  • update: permettra de valider puis de mettre à jour des objets Song. Le formulaire d’édition sera affiché via la méthode “edit”.
  • delete: pour effacer

Faites de même pour l’objet Album:

1
grails generate-controller com.excilys.grails.Album

Pour les besoins d’une démonstration ou dans certains cas bien précis il est possible de changer l’intégralité du code généré dans un contrôleur par :


1
2
3
4
package com.excilys.grails
class SongController {
    def scaffold = Song
}

Le code ci-dessus remplace intégralement le code présenté plus haut: le scaffolding intègre les actions list, show, create-save, edit-update, delete en une seule ligne de code ! Rien de mieux pour éblouir l’assistance ! Néanmoins, le véritable code reste caché et devient plus difficilement modifiable. Cette astuce est donc à utiliser avec parcimonie (personnellement j’ai préféré la bannir pour des raisons de lisibilité). Plus d’informations sur le scaffolding ici: http://grails.org/Scaffolding.

Ok ! Nous avons nos contrôleurs disponibles mais pour le moment aucune vue ne peut être affichée… elles n’existent pas encore !

Génération des vues

Tout comme pour les contrôleurs, Grails peut nous générer des vues à partir des objets du domaines:

1
2
grails generate-views com.excilys.grails.Song
grails generate-views com.excilys.grails.Album

Regardez maintenant dans le répertoire grails-app/views/. Deux nouveaux dossiers ont fait leur apparition. Dans chacun de ses dossiers, 4 vues (*.gsp) ont été générées. Chacune de ses vues correspond à une action du CRUD et est liée au contrôleur. Jetons un œil à la vue song/create.gsp: le formulaire de création est déjà entièrement écrit, le “submit” est dirigé vers l’action save du contrôleur Song.

A ce stade on dispose déjà d’une application fonctionnelle. Elle n’a encore aucune logique métier  mais est entièrement capable de gérer efficacement les objets du domaine (CRUD).

Lancement et premiers écrans

Lancez l’application:

1
grails run-app

Pour rappel, si vous souhaitez modifier le port:

1
grails -Dserver.port=9999 run-app

Utilisez votre navigateur favori : http://localhost:8080/albums/ (changez le port selon votre configuration, le 8080 est celui par défaut)

Welcome to Grails

Écran de bienvenue Grails

La gestion des chansons

Essayez de créer une nouvelle chanson:

  • cliquez sur le lien com.excilys.grails.SongController
  • cliquez sur “New song” dans la barre de navigation
  • remplissez les champs du formulaire (vous pouvez laisser la valeur de l’album à vide, car une chanson peut ne pas être inclue dans un album)
  • validez en cliquant sur “Create”

La nouvelle chanson apparait dans la liste. Vérifiez qu’elle est bien présente dans votre base de données.

Affichage de l'objet après création

Affichage de l'objet après création

Liste des chansons créées

Liste des chansons créées

Les validations sur les formulaires de création et de mise à jour sont également déjà implémentés. Pour tester:

  • reprenez la procédure de création d’une chanson
  • remplissez le formulaire en oubliant par exemple de rentrer la durée
  • validez
  • l’erreur est clairement affichée, le champ “duration” est encadré en rouge

Si vous êtes curieux, vous avez surement remarqué que la validation ne semble pas effective pour les autres champs. Pour le champ Album, ce comportement est voulu (rappelez-vous, une règle de gestion spécifie qu’une chanson peut appartenir à 0 ou 1 album). Mais pour le champ “name” et “artistName” ? Pourquoi, après validation, ces champs ne sont pas encadrés en rouge alors que l’entrée est vide ? La raison est très simple: par défaut, un champ String (automatiquement mappé sur un varchar en base) ne peut être NULL (comme toutes les autres propriétés) mais autorise la valeur “” (vide/blank). Grails comprend donc bien que le champ ne peut pas être rempli à NULL et insère donc un espace blanc, ce dernier n’étant pas interdit. Une vérification rapide dans la base après insertion vous le prouvera. Indiquons à Grails que la valeur vide n’est pas autorisée.

Pour cela:

  • ne stoppez pas votre application (Grails est capable de redéployer automatiquement à chaque changement)
  • éditez l’objet Song (grails-app/domain/com/excilys/grails/Song.groovy)
  • changez le bloc constraints et ajoutez:

1
2
name(blank:false)
artistName(blank:false)
  • enregistrez
  • Grails redéploie automatiquement (dans les 5s) à chaque changement

Une fois que Grails a fini de redéployer, retournez sur le formulaire de création d’une chanson et validez le formulaire alors que tous les champs sont vides. Cette fois, les 3 champs s’affichent en rouge.

Je vous laisse expérimenter la mise à jour et la suppression :) .

Gestion des albums

Insérer des chansons c’est bien, mais le but premier de l’application est de gérer des albums musicaux. Voyons ce qu’il se passe du coté de la gestion de ces Albums (cf: image sur la droite).

Ecran initial de création d'un album

Ecran initial de création d'un album

Ce formulaire n’est pas exempt de défauts:

  • le champ description serait plus explicite en étant présenté sous la forme d’une zone de texte plus large (textarea)
  • la date de publication ne devrait pas être à a la discrétion de l’utilisateur
  • on pourrait ajouter la possibilité de sélectionner les chansons à ajouter dans un nouvel album (un combobox multiple par exemple, ou une liste de boutons à cocher)
Le champ description

Deux solutions peuvent être retenues pour afficher un textarea:

  • la solution automatique : on indique à Grails que l’on souhaite afficher un textearea et on régénère la vue
  • la solution manuelle: le textarea est ajouté à la main

Ce premier “souci” fait clairement apparaître l’une des première limitation de Grails: il est impossible de “générer à tout-va”. (il aurait été utopique de penser cela). La génération ne peut servir qu’au départ: elle accélère grandement la première phase de programmation de l’application mais ne peux en aucun cas être utilisée tout au long du cycle de développement. En effet, une modification manuelle (un changement de style, un ajout métier, un changement de disposition dans le code HTML, …) condamne de facto la génération automatique des vues (ce raisonnement s’applique également aux contrôleurs) qui écraserait sans états d’âme les modifications apportées. En résumé, les commandes de génération ne sont à utiliser qu’une seule fois au début du projet et sont à bannir par la suite.

Ceci étant dit, et puisque nous n’avons fait encore aucune modification sur nos vues (et que nous sommes encore au début de notre projet),  indiquons à Grails que le champ description doit être présenté comme un textarea. Cela se passe dans le bloc “constraints” de la classe Album:

1
2
3
4
static constraints = {
name(blank:false)
description (widget:'textarea', nullable:true, blank:false)
}

J’en profite au passage pour indiquer que le champ “name” n’accepte pas de valeurs vides et que “description” peut être nullable.

Regénérons la vue:

1
grails generate-all com.excilys.grails.Album

Note: vous aurez remarqué que j’ai utilisé l’argument “generate-all” à la place de “generate-views”. Il existe 3 arguments:

  • generate-controller CLASS_DOMAIN : génére uniquement le contrôleur associé à l’objet domaine
  • generate-views CLASS_DOMAIN : génère uniquement les vues
  • generate-all CLASS_DOMAIN : génère les vues et le contrôleur

Rafraichissez votre page de création d’un album : la zone “description” est maintenant un textarea.

Vous pourrez vous inspirer du code généré pour les prochains changements de présentation que je vous conseille vivement d’effectuer manuellement sauf si vous décidez d’utiliser massivement la génération (ce qui me paraît franchement compliqué sur le long terme).

La date de publication

Cette date ne devrait pas être présentée à l’utilisateur lors de la création mais automatiquement déduite par le serveur (disons que la date de publication correspond à la date de création en base, ceci est un choix métier).

Plusieurs solutions:

  • dans le contrôleur, action save, modifier la date avant la sauvegarde en base
  • déléguer cette date de publication à une date auto-gérée

Dans notre cas, puisque la date de publication est la date de création, on peut tout simplement la renommer en “dateCreated”. Grails est capable de gérer automatiquement les dates de créations et/ou de mise à jour (de façon transparente). Il suffit donc de renommer “publication” et le travail est terminé. Pour gérer la date de mise à jour de facon automatique, créez un champ typé Date et nommé “lastUpdated”. N’oubliez pas de régénérer la vue (ou de répercuter les changements).

L’autre solution consiste à injecter la date actuelle juste avant la création dans le contrôleur.

1
albumInstance.publication = new Date ()

Au final, quelle que soit la solution retenue, on édite le formulaire de création afin d’enlever purement et simplement la partie dédiée a la date de publication.

Modifier la vue

Il serait assez pratique de pouvoir sélectionner les chansons à inclure directement à la création d’un album. Pour cela, nous allons afficher un combobox à selection multiple. Ce combobox contiendra les chansons encore orphelines (non incluse dans un album).

Pour afficher le combobox, nous allons utiliser une taglib fournit par Grails :  <g:select/>.

Editez le fichier grails-app/view/album/create.gsp et ajoutez:


1
2
3
       <label for="songs">Chansons:</label>

        <g:select name="songs"  id="songs" multiple="yes"></g:select>

Voilà un, certes, joli combo mais vide …

Deux solutions pour le remplir:

  • depuis la vue
  • depuis le controlleur

Le but étant de rapatrier une liste d’objet Song. La taglib défini un attribut nommé “from” qui accepte une liste d’objet.

Depuis la vue

1
<g:select from="${com.excilys.grails.Song.findAllByAlbumIsNull()}" name="songs" value="${albumInstance?.songs}" id="songs" multiple="yes"></g:select>

D’où sort cette méthode ?! “Song.findAllByAlbumIsNull()” ??  Groovy ! C’est en effet une des propriétés de Groovy: les méthodes dynamiques. De fait, Grails est capable de générer une requête en utilisant le nom de la méthode comme “paramètre”. Il suffit de lire la méthode pour en comprendre le sens ! Pour plus d’informations sur ce sujet, vous pouvez lire cette page : http://grails.org/doc/1.1.x/guide/single.html#5.4.1%20Dynamic%20Finders

Depuis le contrôleur

La requête sera sensiblement la même. Le résultat également. Il s’agit là plus de goût personnel (beaucoup de personnes considèrent que la vue ne devrait jamais implémenter de code métier comme de la recherche en base).

On ajoute donc le résultat de la requête dans le modèle. La taglib prendra en paramètre la liste dans le modèle.


1
2
3
4
5
6
7
8
    def create = {
        def albumInstance = new Album()
        albumInstance.properties = params

        def availableSongs = Song.findAllByAlbumIsNull()

        return ['albumInstance':albumInstance, 'availableSongs':availableSongs]
    }

1
<g:select from="${availableSongs}" name="songs" value="${albumInstance?.songs}" id="songs" multiple="yes"></g:select>

Si vous regardez le code source HTML généré par la taglib, vous remarquerez que les balises “option” ont toutes pour valeur le contenu de cette même balise: autrement dit, une chaîne de caractère (résultat de l’appel à la méthode toString()). Si vous essayer de valider, Grails vous affichera une erreur: il lui est impossible d’aller chercher les objets Song à lier dans la mesure où il attend que vous lui passiez des Long (les id de chaque objet sont en effet des Long par défaut).

La taglib nous propose deux attributs supplémentaire afin de pallier ce problème:

  • optionId : donnez l’attribut que vous souhaitez récupérer comme valeur de la balise “option”
  • optionValue : donnez l’attribut que vous souhaitez récupérer comme corps de la balise “option” (ce qui est visible sur la page)

Ce qui nous donne au final:

1
<g:select optionId="id" optionValue="name" from="${com.excilys.grails.Song.findAllByAlbumIsNull()}" name="songs" value="${albumInstance?.songs}" id="songs" multiple="yes"></g:select>
Ecran final de création d'un album

Écran final de création d

Affichage d'un album

Affichage d'un album

Les services

De la même façon que pour les contrôleurs ou les vues, Grails fournit une commande afin de générer des services. Un service est une classe qui embarque du code métier complexe.

1
grails create-service com.excilys.grails.Song

Parce que cette classe est dans le dossier grails-app/service/, Grails va automatiquement créer pour vous un bean associé dans le contexte Spring. Pour utiliser un service dans un contrôleur (ou depuis un autre service) il suffit de l’ajouter en tant qu’attribut:


1
2
3
4
5
class SongController {

 def songService
 // ...
}

Grails s’occupera d’injecter automatiquement une instance de ce service et libre à vous d’utiliser les méthodes que vous aurez écrites.

La suite au prochain numéro !!

Dans le prochain épisode, nous verrons comment sécuriser notre application. Nous ajouterons également la gestion des flux RSS sur les albums et les chansons.

Ressources

Téléchargez le code source complet de l’épisode: albums_episode3.zip
Tags sur le SVN Google code: http://excilys.googlecode.com/svn/projects/grails-albums/tags/grails-albums_article_3/
Grails scaffolding: http://grails.org/Scaffolding
Grails dynamic finders: http://grails.org/doc/1.1.x/guide/single.html#5.4.1%20Dynamic%20Finders
Documentation globale: http://grails.org/doc/latest/

Share
  1. Pierre-Yves RICAU
    26/12/2009 à 10:39 | #1

    Cet article amène des questions sur ce qui est habituellement considéré comme de “bonnes pratiques” :

    Le modèle en couche (Vues/Contrôleurs => Services => DAO) est ici court-circuité, avec un appel des DAO directement depuis les contrôleurs. Voir même, Ô ultime transgression, depuis les vues :-) .

    Cela apporte clairement des avantages en terme de productivité. Cependant, ne court t’on pas le risque qu’un développeur débutant sur ces techno utilise ces raccourcis à mauvais escient ?

    Typiquement, je verrai bien une boucle for dans une vue, avec à chaque itération un appel vers la base de donnée pour charger une entité, plutôt que de tout charger en une seule fois.

    Scénario hyper classique, qu’on retrouve dans de nombreuses applications JEE (hibernate + lazy loading + OpenSessionInViewfilter, ça vous dit quelque chose ;-) ?)

  2. Pierre-Yves RICAU
    26/12/2009 à 10:53 | #2

    A noter l’article de Xebia ( http://blog.xebia.fr/2009/12/23/grails-scaffolding/ ) qui met en avant le scaffolding dynamique pour faciliter la maintenance des applications. Ce point de vue différent est aussi intéressant.

  3. 31/12/2009 à 00:17 | #3

    Très bonne série d’article, pour répondre à Pierre-Yves c’est difficile à faire avec Grails, peut-être même plus qu’avec une bonne vielle page JSP. Je suis même moins inquiet qu’il y a quelques années avec justement ensuite le bourrage de crâne : faites des DAO, séparez vos couches, etc. C’était important, cela reste important, mais maintenant c’est fait par Grails.

    Nicolas

    Blog le Touilleur Express

  4. 10/01/2010 à 16:26 | #4

    @Pierre-Yves RICAU Un point de vue pas si différent en fait, Cyril a raison le “generate-all” ça craint ;) Mais le “scaffold = true” (à mettre dans un Controller) permet de dépasser les limitations décrites ici.

    L’argument du développeur débutant, tu peux le transposer sur n’importe quelle techno. Un développeur débutant (ou mauvais développeur) pourra toujours mal utiliser un outil, même un simple marteau ;) Et plus un outil est complexe plus il a de chances d’être mal utilisé. Or entre Grails et JEE, je te laisse juger lequel est le plus complexe :p

    Bravo Cyril pour les articles !

  5. ala1986
    15/01/2010 à 11:56 | #5

    Bonjour a tous,
    J’ai un petit problème concernant la modification de la date de publication. Je ne comprend pas bien l’astuce.
    Est-ce-que en remplace le champ “Date publication” par “Date dateCreated” seulement et la date devient caché pour l’utilsateur dans la creation.
    Il marche pas parceque on a changé le label de la date seulement, donc il sera affiché dans la fenetre date created et non pas publication, et la date il reste la meme il s’affiche pour l’utilisateur.

    Concernant la modification de vue en ajoutant le combobox a travers le controller, j’ai ajouté ca “def availableSongs = Song.findAllByAlbumIsNull()” et “‘availableSongs':availableSongs” dans return.
    J’ai ajouté ca “Chansons:

    “”
    dans views.
    ou on ajoute ca? J’ai ajouter apres ca ”

    Lorsque on génère l’application de nouveau il efface tout que j’ai ajouté.
    Il revient comme elle est.
    Merci beaucoup d’avance.

  6. Cyril BROUILLARD
    15/01/2010 à 13:46 | #6

    @ala1986
    L’astuce consiste simplement à dire à Grail (ou plutôt GORM, la couche Grails au dessus d’Hibernate) : “gère moi automatiquement les dates d’insertion et de suppression lors de l’écriture d’objets en base”.

    Conformément au paradigme COC (Convention Over Configuration), Grails nous dit : “ajoutez le champ dateCreated et le champs lastUpdated et je vous gère ces dates de façon transparente et sans aucun ajout de code”.

    Pour faire cette modification (qui n’est en soit pas obligatoire, mais qui facilite quand même pas mal la vie), l’objectif est de remplacer totalement tout ce qui a trait à datePublication. (dans la vue et dans le domain). Donc au pire si tu as des souci le mieux est encore d’enlever completement l’objet domain, le re-écrire puis régénérer.

    Bon courage et bonne continuation avec Grails ;)

  7. benjamin lemoine
    30/08/2012 à 22:36 | #7

    Salut,

    Je fais actuellement le tuto avec Grails 2 et il y’a une différence pour le select, @optionId@ n’existe apperement plus et est remplacé par @optionKey@. De plus un @optionKey=”id”@ ne retourne pas l’id sous forme de Long mais de String. Le workaround que j’ai trouvé est donc de le passer en Long avec @optionKey=”{{it.id?.toLong()}}”@. Cela semble fonctionner, après débutant totalement avec grails si il y ‘a mieux, je suis toute ouïe.

  8. benjamin lemoine
    30/08/2012 à 22:37 | #8

    Juste un petit ajout, si il y’avait possibilité d’avoir des liens non seulement vers les premiers mais aussi vers la suite du tuto ca serait bien pratique!

  1. 24/12/2009 à 10:42 | #1
  2. 04/01/2010 à 15:36 | #2