I. Présentation de Grails▲
Projet initié en 2005 par Graeme Rocher, il avait pour but d'apporter un équivalent de Ruby on Rails au monde Java. Le nom Grails est d'ailleurs une contraction de Groovy, langage sur lequel il est basé et de Rails, l'ensemble formant Grails (Les Graals anglais, ce qui explique le logo).
Aujourd'hui, nous en sommes à la version 2.4.4 (sortie en octobre 2014).
Mais, du coup, Grails, c'est quoi exactement ?
Il s'agit d'un framework Open Source se basant sur une architecture MVCModel-View-Controller qui fonctionne sur une JVMJava Virtual Machine. Sa philosophie tourne autour des points suivants :
- java-like dynamic language : les scripts Groovy sont compilés en byte-code, permettant de profiter de la puissance de Java. Il s'appuie sur son écosystème en utilisant Spring, Hibernate, etc. ;
- convention over configuration : système de configuration tendant à simplifier la vie du développeur (et limiter le code à écrire/maintenir). Par exemple, les objets du modèle auront le même nom que les tables auxquelles ils sont associés. Le mapping devient automatique ;
- DRYDon't Repeat Yourself : éviter la redondance de code ;
- MDAModel Driven Architecture : une portion du code est créée à partir du modèle, on obtient ainsi une partie du squelette de l'application. Il est donc conseillé de débuter par la génération du modèle ;
- prototypage : grâce au scaffolding, il est très facile de générer un premier prototype (même incomplet) de l'application. Il devient aisé de générer des interfaces fonctionnelles directement issues du modèle de données.
II. Définition du projet▲
Pour les besoins pédagogiques de l'article, l'application nommée it-resto, va être développée.
Mais, dans tout projet, il faut un cahier des charges. Alors, décrivons rapidement ce que nous souhaitons.
It-resto permet aux utilisateurs de participer à un événement. Pour chaque événement, il est possible de voter pour un ou plusieurs restaurants parmi une liste précise. L'ensemble des votes permet de définir le restaurant qui accueillera l'événement.
Un cas d'utilisation simple serait, au sein d'un groupe de collègues, de permettre de sélectionner le lieu où ils choisissent de manger durant leurs pauses de midi.
L'utilisateur pourra :
- s'authentifier ;
- créer un événement ;
- participer à un événement (sans limitation de droits), en votant (une seule fois) pour un ou plusieurs restaurants dans un événement.
L'administrateur pourra gérer (Create, Update, Read et Delete) les utilisateurs, les restaurants, les événements et les votes.
Les événements passés et leurs votes associés seront automatiquement supprimés toutes les nuits.
III. Premier pas avec Grails▲
Avant d'aller plus loin, il est conseillé d'avoir installé et configuré l'environnement Grails.
Le projet est défini… nous savons où aller. Alors, allons-y !
Pour créer le projet it-retso, il faut lancer la commande depuis un shell (ou sous Windows, une invite de commande DOS, Babun, ConEmu, etc.) :
grails create-app it-resto
Un squelette d'arborescence du projet est créé dans le répertoire it-resto.
Le répertoire grails-app est le plus important. C'est ici que vont se trouver les principales ressources du projet :
- conf : ensemble des fichiers de configuration pour Spring, Hibernate et le projet en cours ;
- controllers : pas de surprise, ce sont les classes contôleurs ;
- views : il s'agit des vues du projet, au format gsp ;
- le répertoire lib stockera les différentes bibliothèques du projet, qu'elles soient ajoutées via maven, ivy ou manuellement ;
- src : les sources java et/ou groovy ;
- pour finir, test… comme son nom l'indique, on y retrouve les tests unitaires, mais aussi les tests d'intégration.
Pour tester que tout s'est bien déroulé, il est possible de lancer le projet (grails run-app) et d'accéder au projet (http://localhost:8080/it-resto/).
Pour information, Grails est nativement propulsé par Tomcat.
IV. Mise en place du modèle▲
L'un des principes de base de Grails est d'être orienté modèle. La première étape est donc de générer le modèle de données (pour aller plus loin : MDA).
Notre modèle possède quatre éléments distincts :
- Restaurant : modélisation des restaurants à sélectionner par les utilisateurs dans un événement ;
- User : utilisateur enregistré ;
- Event : événement de base, qui englobera les votes des utilisateurs ;
- Vote : sur chaque événement, un utilisateur pourra sélectionner un ou plusieurs restaurants s'il souhaite y participer.
Afin de simplifier la compréhension de tous, un petit schéma :
Pour aller plus vite, passons en mode interactif. Ce mode permet de travailler directement dans la console Grails, sans avoir à relancer le framework à chaque commande. De plus, les changements sont pris en compte dynamiquement.
Lancez Grails, avec la commande suivante :
grails
Vous êtes à présent dans la console Grails. Il faut générer les quatre éléments du modèle de données (le domain).
2.
3.
4.
create-domain-class it.resto.restaurant
create-domain-class it.resto.user
create-domain-class it.resto.event
create-domain-class it.resto.vote
À chaque fois, deux fichiers sont créés :
- le premier, dans grails-app/domain/it/resto/*.groovy, pour la définition du domain ;
- le second, dans test/unit/it/resto/*Spec.groovy pour les tests unitaires ;
En fonction du besoin du projet, les différents domain doivent être complétés. Pour le domain Restaurant, on aura :
2.
3.
4.
5.
6.
7.
class
Restaurant {
String name
static
constraints =
{
name blank: false
, unique:true
}
}
Le code Groovy a une syntaxe assez proche de Java, mais un peu plus épurée. Pour en savoir un peu plus, vous pouvez consulter cette introduction à Groovy.
L'objet it.resto.Restaurant n'est défini que par son nom (name). Ensuite, dans le bloc constraints, on peut définir toutes les contraintes sur les différents champs.
Dans notre cas, le nom du restaurant sera non null (par défaut dans les domain Grails), non vide (blank:false) et unique (unique:true).
Bien que plus complète, la déclaration d'un User est assez similaire.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
class
User {
String name
String lastName
String login
String password
String email
boolean
admin=
false
String toString
(
) {
return
this
.lastName +
' '
+
this
.name
}
static
constraints =
{
name blank: false
lastName blank: false
login size
: 5
..15
, blank: false
, unique: true
password size
: 5
..15
, blank: false
email email: true
}
}
Cette fois, les contraintes sont plus importantes. On notera surtout que la taille du login et du password doivent être comprises entre 5 et 15 caractères (size: 5..15). Le champ email, quant à lui, doit répondre aux contraintes d'un email (email: true).
Maintenant, passons à la classe it.resto.Event, qui devra être liée avec plusieurs votes.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
class
Event {
String name
Date eventDate
User owner
static
hasMany =
[votes: Vote]
String toString
(
) {
return
this
.name;
}
static
constraints =
{
name blank: false
eventDate nullable: true
}
}
En regardant l'ensemble des contraintes, on se rend compte que tous les champs sont obligatoires.
La relation de type 1 ↔ N entre l'Event et les Vote est défini avec le mot clef hasMany, qui déclare une liste de type it.resto.Vote, accessible par le champ votes.
Pour chaque Event, on garde également une trace de l'utilisateur qui crée l'Event, dans le champ owner, qui permet de faire une relation de type 1 ↔1 entre un Event et un User.
Terminons avec le Vote qui va relier un Event, un User et la liste des Restaurant que le participant aura sélectionnés.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
class
Vote {
User user
static
hasMany =
[restaurants: Restaurant]
static
belongsTo =
[event:Event]
String toString
(
) {
def
listRestautant
for
(
restaurant in
restaurants) {
listRestautant +=
restaurant.name +
' - '
}
return
this
.user.login +
' : '
+
listRestautant
}
static
constraints =
{
}
}
La classe it.resto.User définie dans le Vote représente la relation lien direct de type 1↔1, ce qui signifie qu'un vote est attaché à un seul et unique User.
Les références vers les restaurants sélectionnés par l'utilisateur sont déclarées avec le mot clef hasMany. Un Vote est relié à un ou plusieurs Restaurant.
L'Event est déclaré avec belongsTo, indiquant que le Vote lui appartient. On retrouve son équivalent en hasMany dans la classe it.resto.Event.
En supprimant un Event, les Vote associés seront également supprimés.
Dans tous les cas, l'identifiant technique id est implicite.
Voilà, nous sommes prêts, GORMGrails Object Relationnal Mapping va faire le reste. Il s'agit d'une surcouche au framework hibernate qui génère automatiquement toute une série de méthodes telles que load, save, exist… mais également une partie plus « magique » ; des méthodes de recherches comme findBy* qui sont définies en fonction de la classe. Celles-ci sont utilisables directement dans la suite du projet (implémentation implicite), ce qui réduit le code de l'application et les tests à réaliser.
Par exemple, pour la classe it.resto.Restaurant, on retrouvera la findByName ou findByNameLike.
Dans le tableau ci-dessous, quelques exemples de suffixe pour le findBy.
findBy… |
|
InList |
Dans une liste de valeurs données |
LessThan |
Inférieur à une valeur donnée |
LessThanEquals |
Inférieur ou égal à une valeur donnée |
GreaterThan |
Supérieur à une valeur donnée |
GreaterThanEquals |
Supérieur ou égal à une valeur donnée |
Like |
Recherche d'un pattern dans une chaîne |
Ilike |
Similaire au Like, mais insensible à la casse |
NotEqual |
Différent de la valeur donnée |
InRange |
Se situant entre deux valeurs ordonnées |
Rlike |
Similaire au Like, mais avec la possibilité de réaliser des expressions régulières |
Between |
Se situant entre deux valeurs |
IsNotNull |
Contenu n'est pas null |
IsNull |
Contenu est null |
V. Mise en place des premiers tests▲
En parallèle de la création des différentes classes de domain (quatre fichiers dans it-resto/grails-app/domain/it/resto), Grails a également généré des classes de test (dans it-resto/grails-app/test/unit/it/resto). Elles se présentent toutes de la même manière avec une méthode setup, qui sera exécutée avant chaque méthode de test, ainsi qu'une méthode cleanup exécutée après le test.
Il ne reste plus qu'à écrire les méthodes de test pour valider le bon fonctionnement des classes du domain.
Ces méthodes doivent être écrites en plusieurs sections, connues sous le nom de given-when-then :
- la section given décrit l'état du système avant de débuter le scénario. Ce sont les conditions préalables ;
- la section when est le déroulement du scénario à spécifier ;
- la section then va décrire et tester les changements attendus suite au scénario réalisé dans la section when.
Un test sur la création et la validation du domain Restaurant ressemblera à ce qui suit. Dans ce premier test, nous allons vérifier que les contraintes sur le nom du restaurant sont bien respectées (ni null, ni blank).
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
void
'test constraint Restaurant'
(
) {
given:
mockForConstraintsTests Restaurant
when: 'the restaurant name is null'
def
restaurant =
new
Restaurant
(
)
restaurant.name =
null
then: 'validation should fail'
!
restaurant.validate
(
)
restaurant.hasErrors
(
)
print
restaurant.errors['name'
]
when: 'the restaurant name is blank'
restaurant.name =
''
then: 'validation should fail'
!
restaurant.validate
(
)
restaurant.hasErrors
(
)
print
restaurant.errors['name'
]
}
Dans la section given, la seule chose définie est le mockForConstraintsTests qui « mocke » la classe it.resto.Restaurant (toute la machine Grails n'est pas lancée durant les tests). Ce « mock » ajoute la méthode valide() à la classe, mais permet aussi de détecter plus simplement des erreurs avec la propriété errors. De plus, les messages sont « allégés » et plus simples à analyser.
Par contre, il y a une limitation. Il ne faut pas oublier que le contexte d'exécution est un test unitaire et non pas d'intégration. Il n'y a pas d'ajout de méthodes au runtime par GORM par exemple (findBy*, etc).
Dans la section when, c'est la zone d'exécution du scénario de test. Ici, c'est un objet it.resto.Restaurant qui est créé, mais le nom (name) est null (explicitement déclaré).
Dans la section then, on valide l'état de sortie du scénario. La méthode valide() va déterminer si toutes les contraintes sont respectées.
Toutes les instructions de la section then doivent être vraies, le contraire indiquant une erreur du test. Pour simplifier un peu plus la détection des erreurs, la méthode hasErrors() indique si des erreurs ont été rencontrées, puis le message est disponible en fonction du champ en erreur. Dans cet exemple, ce sera restaurant.errors['name'].
Pour démarrer le test, lancez la commande test-app (ou grails test-app si vous avez quitté le mode interactif). À la fin, un rapport est disponible dans \target\test-reports\.
Il est recommandé de réaliser cette étape au fur et à mesure de la création des classes du domain.
VI. Peuplement de la base▲
Le squelette de l'application a été réalisé et le modèle de données est prêt (définition des objets domain ainsi que les tests unitaires sur les contraintes). Avant de réaliser les premiers écrans et pour avoir des éléments à y afficher, il faut peupler la base de données.
Cela se fait simplement, depuis le fichier grails-app/conf/BootStrap.groovy qui contient la méthode init exécutée au lancement de l'application et la méthode destroy quand l'application est arrêtée.
Il suffit de mettre en place le code suivant dans la méthode init :
2.
3.
4.
5.
if
(!
Restaurant.count
(
)) {
print
'Create'
new
Restaurant
(
name: 'King Croissant'
).save
(
failOnError: true
)
new
Restaurant
(
name: 'Les trois mi-temps'
).save
(
failOnError: true
)
}
Restaurant.count() dénombre les restaurants en base. Cette condition évite de créer systématiquement les mêmes éléments à chaque lancement de l'application (ce qui arrive régulièrement en phase de développement).
Pour créer un nouveau Restaurant et le stocker en base, on le fera de cette façon :
new
Restaurant
(
name: 'King Croissant'
).save
(
failOnError: true
)
La même chose doit être faite pour les autres éléments du modèle : User, Event et Vote.
Pour la base de données sous-jacente, Grails propose une connexion à la base H2 en natif, en mode stockage en mémoire et une console d'administration accessible via http://localhost:8080/it-reso/console. L'identifiant par défaut est admin et il n'y a pas de mot de passe.
Attention ! Si vous utilisez cette base pour votre application de production, n'oubliez pas de changer le mot de passe admin et de bloquer l'accès à la console, sinon cela revient à exposer tout le contenu de la base sur Internet.
La configuration de la base se fait dans le fichier grails-app/conf/DataSource.groovy, au niveau de la propriété environments.development.datasource :
2.
3.
4.
dataSource {
dbCreate =
"create-drop"
url =
"jdbc:h2:mem:devDb;MVCC=TRUE;LOCK_TIMEOUT=10000"
}
Initialement, la propriété dbCreate est à create-drop, mais elle peut prendre d'autres valeurs, selon le besoin :
- create : supprime les tables, les index, etc. avant de recréer le schéma au démarrage ;
- create-drop : semblable à create, mais en supprimant les tables à l'arrêt de l'application ;
- update : met à jour la table en créant les tables et les index manquants, sans perdre les données existantes ;
- validate : ne fait aucun changement dans la base de données, mais compare le schéma existant avec le schéma configuré dans l'application (objet domain) et génère un rapport.
Attention : create-drop et create sont à manier avec précaution en dehors d'un environnement de développement.
On retrouve des configurations équivalentes pour les environnements de test et de production.
VII. Conclusion▲
Voilà, nous venons de voir comment :
- créer une application Grails à partir de zéro ;
- mettre en place le modèle de données et poser des contraintes sur celui-ci ;
- réaliser des tests de validation du modèle ;
- peupler la base en vue des premiers développements.
Dans la suite de cet article, nous réaliserons les différents écrans de l'application et irons jusqu'au déploiement complet de l'application it-resto.
Pour aller plus loin, toutes les sources de l'article sont sur GitHub : https://github.com/masson-r/it-resto
La documentation de Grails, très complète, est disponible ici : http://grails.org/doc/2.4.4/guide/index.html
Enfin, pour ceux qui sont allergiques aux lignes de commandes, il existe le GTSGrails Tool Suite qui permet de simplifier l'approche de l'environnement Grails.
VIII. Remerciements▲
Je remercie l'équipe de Developpez, mais surtout :
- Mickael Baron, qui m'a aidé tout au long de mon apprentissage des procédures du site Developpez.com ;
- Robin56 ;
- Franouch et f-leb, pour leurs relectures.
Je remercie plus particulièrement Daniel pour sa relecture technique attentive et Nicolas pour ses tests sur it-resto.