Analyse, mise en place de MongoDb et amélioration d’Apis
Chose promise chose due, voici la suite du périple au royaume des micro-services. Aujourd’hui, dans cette seconde partie, nous allons attaquer un morceau un peu plus intéressant. Nous verrons comment utiliser MongoDb et nous apprendrons à construire des Apis plus complexes. A la fin de cet article vous serez donc capable d’implémenter un service beaucoup plus complet capable de persister des données et de les exploiter.
Prés-requis
Explications fil rouge
Avant de commencer faisons un point sur notre fil rouge. Nous souhaitons mettre en place un micro-service qui sera en mesure de gérer des traductions. Ce dernier sera potentiellement partagé entre plusieurs applications. Pour simplifier certaines problématiques fonctionnelles j’ai fait le choix de travailler avec deux collections Mongo. Une permettant de représenter des langues (languages) et l’autre des traductions (translations).
Vérification MongoDb
Avant de commencer, il est nécessaire de lancer une base de données, MongoDb. Pour ce faire c’est très simple, sur mac lancez la commande suivante
[shell]mongod –dbpath <chemin de vos fichiers de données>[/shell]
Le paramètre –dpath est libre. Une fois lancé, vous y trouverez tout les fichiers générés par MongoDb afin de persister vos données.
Configuration de spring boot
Maintenant, nous allons devoir paramétrer Spring Boot afin qu’il soit capable de communiquer avec notre base de données. Pour ce faire, rendez-vous dans les resources, ouvrez le fichier application.properties et ajoutez les lignes suivantes
Nous verrons plus tard si tout a bien fonctionné.
Nosqlclient
Dernière étape, facultative, vous permettra de vérifier simplement le comportement de votre application en contrôlant les données enregistrées. Lancez l’application nosqlclient que vous avez téléchargé durant la première partie. Cliquez sur connect puis create new afin de vous connecter à votre base MongoDb.
Vous avez quelques informations assez simple à compléter. Sauvegardez et cliquez sur connect now. Vous pouvez dés maintenant naviguer simplement dans votre base de donnée Mongo.
Implémentation
C’est le moment de passer à l’action. Nous allons implémenter nos endpoints qui permettront de fournir les différents services nécessaires pour satisfaire nos besoins. Dans cet article, nous allons rester simple et nous concentrer sur la création d’Apis basiques ainsi que de la persistance des données. Nous verrons la prochaine fois les problématiques d’identifications et de gestion de droits qui méritent à eux seuls au moins un article entier.
Avant de vous lancer tête baissée dans ce chantier, je vous conseille de vous renseigner sur le concept d’Apis RestFull. Ce sont un ensemble de bonnes pratiques permettant à vos Apis d’être « standard » et donc plus prédictibles. Pour ceux qui ne veulent pas passer trop de temps sur le sujet, Octo a sortie une cheat sheet assez sympa que vous pouvez consulter ici.
Structure du projet
Il y a plusieurs écoles concernant ce point. Certaine personne préférerons découper l’application par couches relatives aux types de classe (service, controller, etc.), d’autres plutôt en terme de groupements fonctionnels. Pour ma part, après quelques réflexions et discutions sur la question, je trouve la seconde solution plus pratique et plus parlante.
Dans ce projet nous auront donc une structure du type
Construction du modèle
Nous allons tout d’abord construire notre modèle. Comme nous l’avons vu précédemment, nous allons rester très simple. Nous n’aurons que deux collections.
Dans un premier temps il est donc nécessaire de créer deux packages, languages et translations et d’ajouter dans chacun d’eux une classe Kotlin qui contiendra le code de nos modèles.
Notre fichier Lang.kt qui contient le modèle de la langue ressemble à ceci
et le fichier Translation.kt à ceci
Les annotations
Vous remarquerez sans doute qu’il y a de nombreuses annotations qui vont nous permettre de générer nos collections (côté mongoDb). Ici nous pouvons voir l’utilisation de:
- @Document: Permet de définir le nom de la collection côté MongoDb qui sera associé à cette classe. Par défaut il utilisera le même nom collection cependant, vous pouvez modifier ce comportement en utilisant le paramètre collection= »nom de la collection » (comme sur les exemples ci-dessus)
- @Id: Il permet de lier l’id MongoDb avec une propriété du modèle. Si vous n’ajoutez pas cette annotation il y aura quand même un Id côté MongoDb.
- @Indexed: Création d’un index MongoDb. Cette annotation peut aussi recevoir des paramètres comme unique = true qui permettent d’ajouter des contraintes.
Les deux annotations @get:NotNull et @get:NotBlank ne sont pas liée à mongoDb. Elles sont simplement là pour qu’IntelliJ soit plus performant dans la validation de propriétés.
Il y a de nombreuses autres annotations disponibles mais pour le moment c’elles-ci devraient nous suffire.
Les propriétés
Je pense qu’il est inutile de s’attarder sur la classe Lang. Mis à part la propriété locale qui est un peu particulière et qui permet de représenter une langue, il n’y a aucune complexité.
Du côté de la classe Translation,nous avons un champ nommé translations qui vous a peut-être attiré l’œil. En effet, c’est une MutableMap. C’est à dire, une collection qui va stocker des couples clés valeurs. Il existe plusieurs type de map, ici j’ai fait le choix de prendre une MutableMap afin d’être en mesure de modifier les valeurs de la map.
Nous aurons ici une clés de type String et la valeur de type Any. Pourquoi ce choix ? C’est pour pouvoir gérer simplement des objets json complexes avec plusieurs niveaux. Ainsi, vous pourrez avoir des sections comme l’on trouve souvent dans les systèmes de traduction standard (i18n). Cela permet d’optimiser le chargement de vos pages en envoyant juste les traductions nécessaires.
Bref, on pourra par exemple facilement stocker le json suivant
A partir de maintenant, si vous lancez votre application elle devrait se connecter à votre base Mongo et créer, si nécessaire les deux collections.
Création du repository
Nous allons maintenant mettre en place deux repository. Ces derniers permettent d’introduire une couche d’abstraction au niveau de l’accès aux données qui nous permet de simplifier notre code et d’économiser du temps.
Créez deux fichiers, LangRepository.kt et TranslationRepository.kt respectivement dans vos packages languages et translations. Ils ressembleront à ceci:
Ces deux classes sont issue de MongoRepository qui permet d’abstraire toute une partie des opérations liées à Mongo. Il fournit des éléments tels que findAll, findById, etc. Cependant, vous aurez peut-être remarqué que le repository de lang possède une méthode custom supplémentaire. En effet, comme nous l’avons vu plus haut, j’ai fait le choix d’ajouter un technicalName au niveau des langues. Ce dernier est unique et plus pratique à utiliser q’un id. L’ajout de la petite ligne findByTechnicalName permet très simplement d’effectuer des recherches sur ce champ.
Pour ceux qui n’ont pas l’habitude, vous serez peut-être étonné de voir cette ligne. En effet, nous sommes dans une interface et nous ne définissons aucun code métier. Alors, comment ceci marche-t’il ?
En fait, c’est grâce à la définition de la méthode qui n’a pas était choisi au hasard et qui porte assez d’informations pour que les bonnes requêtes soient générées. C’est ce que l’on appelle les Query Methods. Le fonctionnement est assez simple, et repose sur des mots clés à enchaînés. Ainsi, dans notre cas, nous voulons faire une « recherche par technical name » donc « find by technicalName ». Un coup de camel case et on retrouve notre nom de méthode findByTechnicalName. Par manque de temps, nous n’entrerons pas dans les détails ici. Cependant, vous pouvez retrouver des informations pour aller plus loin dans cette documentation.
Création d’un service
Attaquons nous maintenant aux services. C’est ici que résidera toute l’intelligence de votre application. Comme précédemment, commencez par créer deux fichiers, LangService.kt et TranslationService.kt respectivement dans les Packages languages et translations. Ces classes sont des services qui auront besoin d’utiliser nos repository. Il est donc nécessaire de les déclarer comme l’exemple ci-dessous.
L’annotation @Service permet de déclarer que la classe est un service et l’annotation @Autowired suivit du constructeur permet de procéder à l’injection de notre repository dans le service. Enfin, il ne reste plus qu’a définir les différentes méthodes afin d’épauler votre constructeur. Du côté des langues, les choses sont assez simple. Il consiste presque uniquement à faire des appels de méthodes provenant du repository.
Du côté des traductions les choses sont plus complexes.
Les traductions – Les objets transient
Tout d’abord d’un point de vue technique, il va falloir trouver un moyen de travailler avec des objets partiels. En effet, lorsque vous allez vouloir créer ou mettre à jour une traduction vous n’allez pas forcément redemander un json représentant une langue entière. De plus, vous allez potentiellement devoir demander d’autres informations non persistées. Pour ce type de problème, il est nécessaire de créer des classes spécifiques qui représenteront vos objets.
Dans mon cas, j’ai créé deux nouvelles classes. LangRequest et TranslationRequest. Vous pouvez voir ci-dessous qu’elles ressemblent beaucoup aux classes modèles précédemment définies mais qu’elles disposent de certains paramètres optionnels.
Les traductions – Difficultés fonctionnelles
La seconde difficulté est fonctionnelle. Ainsi, si on reprend le fonctionnement normal des messages i18n (voir ici pour plus de détails), on s’aperçoit que le json que nous allons manipuler aura potentiellement beaucoup de niveau. Ceci pose un problème au niveau de la mise à jour de nos traductions. En effet, il serait pénible et contre productif de devoir renvoyer à chaque mise à jour toutes les clés, même celles qui n’ont pas été modifiées.
Les mises à jour sont ici assurées par la fonction updateById que vous retrouvez ci-dessous. Comme vous pouvez le voir, on utilise ici nos nouvelles classes afin de ne pas avoir de problèmes de validation. Dans un premier temps, on teste si il y a une langue dans la requête et si cette dernière existe.
Ensuite nous passons aux traductions. Dans un premier temps, nous allons tester la propriété override que j’ai discrètement inséré plus haut. Si elle est à true on ne se pose pas de questions, on efface tout pour insérer les données de la requête. Si elle est false ou absente, nous allons mettre à jour seulement les traductions présentes dans la requête. Pour ce faire j’ai implémenté une fonction de mise à jour basée sur un parcours d’arbre (je ne la détaillerais pas ici mais vous pouvez consulter le code sur GitHub pour en savoir plus)
Si on pousse le raisonnement au bout, nous aurons le même type de problématique à prendre en compte pour optimiser la consultation. C’est une fonctionnalité que je compte implémenter plus tard.
Création du controller
Il ne nous reste maintenant plus qu’à créer nos contrôleurs qui définiront nos endpoints. Ceux-ci auront pour mission de récupérer des paramètres, d’éventuellement faire deux trois contrôles, d’appeler le ou les bons services et de renvoyer une réponse. Voici ce que cela donne pour les langues
Le principe est à peu prêt le même que dans la première partie, avec de nouvelle annotation avec bien évidemment des options en plus permettant de récupérer des informations dans l’url (@PathVariable) ou dans le body (@RequestBody) de votre requête rest. Une fois ce contrôleur en place, relancez votre app. Ouvrez un client rest comme postman et faites une requête rest en post sur l’url localhost:8080/api/v1/langs avec les éléments définissant la langue en body.
Maintenant, ouvrez nosqlclient et actualisez vos collections (more > refresh collection). Ensuite dans les collections puis dans lang, vous devriez pouvoir retrouver votre enregistrement.
Les tests
Nous avions vu dans le premier article des tests unitaires. Si ces tests sont bien pour éprouver un code métier assez restreint, il est plus difficile de tester des apis simples car beaucoup des problèmes qui peuvent apparaître se situe au niveau de la persistance de nos données. Je vous propose d’aller un peu plus loin afin de pouvoir mettre en place des tests plus pertinents pour notre besoin.
Mise en place de Fongo
Fongo est une base de données in-memory (en mémoire Ram) qui va remplacer notre base Mongo uniquement durant l’exécution des tests. Rendez-vous dans le fichier build.gradle et ajoutez les trois lignes suivantes
[js]
testCompile(‘org.jetbrains.kotlin:kotlin-test’)
testCompile(‘org.jetbrains.kotlin:kotlin-test-junit’)
testCompile(‘com.github.fakemongo:fongo:2.2.0-RC1’)
[/js]
Configuration
Maintenant, nous allons créer un fichier de configuration de tests. Initialisez un fichier TestConfiguration.kt dans votre dossier de test. Ce dernier devrait ressembler à ceci:
Il permet de forcer l’utilisation de Fongo et de paramétrer des éléments qui seront utilisés uniquement dans votre contexte de tests.
Ecriture d’un test
Il est maintenant temps d’écrire nos tests. Ces derniers seront découpés en minimum deux fichiers par feature. Le premier représente une classe abstraite qui nous permettra d’initialiser simplement notre client Fongo. C’est aussi ici qu’on ferra le lien avec le repository à utiliser et qu’on initialisera des données initiales.
Ensuite nous pouvons passer à l’écriture de tests à proprement parler. Ils ressembleront fortement à ce qu’on a vu dans la première partie sauf qu’ici, il est possible d’écrire et de lire dans une fausse base sans avoir peur d’altérer ses données.
Nous n’irons pas plus loin sur ce point. Le but étant simplement de vous donner assez d’éléments pour démarrer. Cette méthode permet de tester simplement vos applications en prenant en compte partiellement la couche accès de données. Cependant, attention, vous êtes sur une fausse base mongo et il peut-arriver qu’il y ait des soucis lors de la connexion avec votre vraie base. C’est donc un bon début mais qui n’est pas suffisant pour être vraiment certain de ne pas rencontrer de problème en production.
Conclusion
C’est fini ! Nous avons vu beaucoup de choses dans l’article d’aujourd’hui il est donc normal d’être un peu perdu si vous débutez. N’hésitez pas à récupérer le projet chez-vous et à tester des choses afin de vous faire la main (il est disponible ici, le tag part2 représentant le code de cet article). Dans la prochaine partie, nous attaquerons le dernier gros sujet de ce dossier, la gestion des droits pour protéger nos Apis.
Bonne semaine et à bientôt 😉
Cadre en informatique dans une multinationale je suis un touche-à-tout passionné de nouvelles technologies.