Comment j’ai construit une plateforme BI sans me suicider (Pt 2).
LE BACKEND
Hooooo ! L’informatique ! ☺
Elle avance à un rythme effréné !
Avant, le choix de la technologie (Langage de programmation) était primordial avant de commencer un projet. Elle (la techno) déterminait ce qu’on pouvait faire ou non et les limites.
Je ne dis pas que ce n’est plus le cas, mais cette grande barrière entre les technos n’existe presque plus. De nos jours que ce soit Java, Python, JS/TS, ou même PHP ils sont tous capable de construire une application à très grande charge et hyper performante.
Javascript m’a toujours plus. Mais durant ce projet j’ai été ébahi de la puissance de cette technologie qu’on a tendance à sous-estimer. JS a su évoluer très vite tout en gardant sa charmante simplicité qui permet à tout le monde d’embrasser son univers sans prise de tête.
Sa mutation, Typescript, permet d’exploser nos limites et de le placer dans le rang du cercle restreint des langages matures tels que Java. Je ne fais pas de confrontation entre Java et JS mais c’est un fait que ce langage rivalise avec les technos compilées et continues d’être tout aussi performant et élégant.
En faisant mes recherches sur la problématique de la connexion aux multiples bases de données, j’ai testé plusieurs technos pour un Proof of Concept.
JAVA était bien trop ennuyeux avec ses très grandes fonctions pour faire de petite choses.
DotNet était très cool mais demandait plus de ressources et pour agrandir l’équipe allait couter plus cher.
PHP j’en avais un peu marre et la mise en place pour résoudre ce problème était trop pénible.
Puis je me suis dit : “bon voyons voir avec NodeJS”.
NodeJS est la chose la plus merveilleuse qui est arrivé à JS. Elle a permis à JS d’atteindre les sommets et de conquérir le monde. Mais celui-ci est trop open dans ses implémentations et normes ce qui laisse à chaque développeur le soin de faire ce qu’il veut comme il veut.
Donc il me fallait une organisation à la Java mais qui prendrait tous les avantages d’Angular.
Et là comme une révélation je me suis souvenu que je connaissais un truc similaire et son nom c’est : NEST JS. 🔥🚀
NESTJS LE SAUVEUR !
Mon choix dès le POC a été une évidence. Cette techno présentait tous les avantages d’Angular et permettait à tous ceux qui avaient souffert auparavant avec Express de découvrir les bienfaits de l’évolution et de se concentrer sur le plus important pendant que le Framework s’occupait du reste.
Je ne vous ferais pas la liste de tous ce qu’il y a à savoir sur NestJs mais je vous conseille cet article qui en parle plus en détails en plus de sa Documentation Officielle qui est l’une des meilleures au MONDE :
https://codeburst.io/why-you-should-use-nestjs-for-your-next-project-6a0f6c993be
CONNECTING TO THE DATABASES
Comme annoncé plusieurs fois l’entreprise possède des données entassées depuis plus de 17 ans dans différents serveurs et différentes bases de différents types.
Si j’avais voulu créer des entités & relations pour chaque base de chaque système j’aurais pris un an pour tout finir sans prendre en compte les erreurs et le risque de tout supprimé dans une table à cause d’une mauvaise manip.
Il fallait sortir du cadre, être ingénieux sur la méthode. J’avais la technologie, j’avais les besoins mais comment extraire les données 🤔.
C’est évident qu’il me fallait du Database First ! Mais la seule techno qui était vraiment à l’aise avec çà c’était .Net C# et je voulais çà avec Nest. Alors je me mis au travail.
D’abord la connexion aux bases progressivement en fonction de ceux qui m’intéresse d’abord :
Vu que j’avais choisi TypeOrm comme ORM il ne me restait plus qu’à définir chaque connexion dans le AppModule (comme ci-dessus) et en donnant les infos pour me connecter à une base. Sachant aussi que cela peut se faire de manière dynamique.
Puis je n’avais plus qu’à préciser à dans les modules la/les DB à utiliser :
Je me sentais déjà invincible ! 🚀🔥
Il restait cependant un autre problème.
Comment maintenant questionner ou manipuler facilement toutes ces données sans pour autant avoir à écrire toutes les Handlers dans mes Controllers et toutes les méthodes dans mes services ?
GraphQL peut-être ? 🤔
Dans ce cas aussi comment prévoir toutes les questions ou filtres possibles sans écrire toutes les resolvers ?
😂 Oui je suis vraiment très très LAZY 😂 Je cherche TOUJOURS des raccourcis.
Je me souviens qu’à cette période un de mes potes m’avait dit : “Bro tu sais toi tu veux toujours faire l’impossible, mais cette fois-ci ce que tu demandes même Google n’a pas la réponse” 😂😂😂😂.
En gros je voulais avoir une REST API mais qui se comporterais comme du GraphQL c’est à dire que dans les requêtes GET je pourrais filtrer à la volée les données que j’allais récupérer en base. Donc la requête GET construira indirectement la requête SQL.
Le pourquoi ?! Parce que dit comme çà cela paraît bizarre mais l’impact est énorme dans le système si directement en front je suis capable de poser des questions comme:
‘Quels sont les Utilisateurs actifs dont le log commence par F, qui ont un âge compris entre 30 et 40 ans et sont de nationalité sénégalaise ?’
et que je n’ai pas à anticiper çà dans le backend avec des méthodes prévu dans les Controllers et les Services, j’aurais un système incroyablement flexible et léger.
Qu’en est-il aussi de tous ces CRUDs à faire pour des centaines de tables et une panoplies de bases ?
Il me fallait trouver un moyen d’automatiser cela et de régler ce problème de redondance dès le départ.
C’est là qu’au milieu de tout ce chaos, après des jours et des jours de recherche, je ne sais plus comment, je tombe sur un microframework pour NestJS dit : “@nestjsx/crud”.
Je ne rêvais pas ! Ce truc existait vraiment et pourtant peu de gens étaient au courants 😭😭
NESTJSX/CRUD
Ce truc me permettait d’avoir, comme dans le code à droite ci-dessus, des CRUDs complets, la possibilité de Filtrer, paginer, relationner et trier les informations dont j’avais besoin pendant la récupération même en base avec des requêtes hypers optimisées et safe (en savoir plus) !
Par exemple voici la requêtes pour la question que j’avais posé sur les utilisateurs plus haut :
‘Quels sont les Utilisateurs actifs qui ont un âge compris entre 30 et 40 ans et sont de nationalité sénégalaise ?’
En réalité le SQL est construis en fonction de la requête (logique tu me dira mais boff fais un tour sur leur doc tu comprendra ce que je veux dire).
Dans le code l’implémentation est tout aussi simple. Par exemple pour avoir les endpoints ci-dessous ainsi que leurs documentations Swagger :
On aura dans le controller :
Et dans le service :
C’est Tout ! 😂
Dans le cas où, comme dans 60% du temps, on a besoin de plus de traitement spécifique, on aura qu’à faire un Override des méthodes existantes ou en créer de nouvelles comme dans toutes autres applis.
Exemple :
Donc j’étais capable de faire beaucoup plus de chose en peu de temps.
Je suis tellement fan de cette bibliothèque que j’ai fini par contribuer à son code sur GitHub pour ajouter de nouvelles fonctionnalités.
Mais il me restait toujours la question des entités 😭.
ENTITIES GENERATION
Comme je l’ai dit plutôt même si je pouvais spécifier dans la configuration de chaque BDD que l’app ne peut pas écraser la base, je pouvais pas accepter de passer mon temps à faire des entités pour TypeORM et mettre des mois à faire des relations par ci et par là.
Donc maintenant vous connaissez la chanson comme le générique c’est : Orbit prends un raccourci et Orbit génère !
Le prochain Héro c’est un package npm : typeorm-model-generator.
Comme son nom l’indique il permet de générer directement les entités TypeOrm en se basant sur la base qui lui a été donnée.
Avec une ligne comme celle-ci :
Je pouvais générer les entités pour toutes les tables de la base spécifiée avec les relations et tout !
Cette base solide et bien optimisée pour aller vite à permis de bien assoir le projet et de commencer à seulement deux mois du démarrage à enchainer les livraisons.
NESTJS ARCHITECTURE
La hiérarchie côté back est très similaire au front avec une méthodologie orientée helpers qui permet de partager les traitement entre plusieurs modules et de faciliter la réutilisation et l’évolution.
Au départ j’avais pensé chaque module comme un microservice déployé à part mais je me suis rendu compte que j’étais dans de l’Over-Engineering vis-à-vis du contexte et que les gains de performance étaient très discutables.
Ensuite j’ai gardé cette même idéologie que j’ai implémentée dans le même backend vu que Nest justement avait un design très orienté modularité qui te permet d’avoir plusieurs blocs indépendants dans ton appli.
ETL PROCESS
Vous vous doutez que quand on parle de BI, on parlera forcément de l’ETL.
Selon la définition d’internet, c’est un processus automatisé qui prend les données brutes, extrait l’information nécessaire à l’analyse, la transforme en un format qui peut répondre aux besoins opérationnels et la charge dans un Data Warehouse. L’ETL résume généralement les données afin de réduire leur taille et d’améliorer leur performance pour des types d’analyse spécifique.
Dans notre cas la phase de ‘Load’ sera plus associés à du ‘Visualize’ car même si parfois les données traitées sont mis dans des Warehouses.
La structure Backend bien géré nous permet de nous concentrer sur les algorithmes et processus à mettre en place afin de pouvoir exploiter ces grandes masses de données.
Il faut savoir que d’habitude pour une application “normale” comme on a l’habitude d’en faire pour de petits clients beaucoup de choses sont ignorés côté code. Tels que le temps d’appel à la BDD, le timing des boucles, leurs types, ainsi que la performance en temps et en ressources d’une fonction, d’un helper ou d’un Microservice.
Par contre, sous le poids de toutes ces données, la moindre petite optimisation est exponentielle sur l’ensemble d’un module.
Il nous est arrivé de passer plusieurs heures à faires des Benchmark entre une boucle FOR normale (for i …) et une boucle FOR OF. Plusieurs heures à tester plusieurs méthodes pour itérer sur des tableaux. Plusieurs jours à implémenter différents algorithmes de recherche, trie ou de graph et à mesurer leur performance.
Plusieurs semaines à mettre en place nos propres implémentations ou surcouche d’ORM car parfois certains besoins sont tellement spécifiques qu’aucune ORM ne pourrait prévoir cela donc on est obligé de créer nos propres solutions et de les optimiser.
Les données obtenues sont souvent transmises au frontend sous forme de statistiques afin que les components de DataViz puissent générer les Graphiques et Charts.
Certains modules qui sont très compliqué dans la logique où dans l’implémentation nous prennent beaucoup de temps à prévoir et peu de temps à implémenter.
Prenons comme exemple un de nos modules d’extractions/compilations qui permet à partir du front-end de choisir plusieurs sources de données et de les compiler en un seul tableau avec un croisement dynamique entre les sources choisies.
Une fois les filtres appliqués, il nous propose de les extraire (télécharger) sous différents formats :
Aussi complexe que ce soit nous l’avons comme suit dans le Backend:
Ci-dessus nous avons le module d’extraction dans le back qui se trouve parmi d’autres modules de type helpers.
Puis nous avons dans ‘extractions.entities.ts’ les différentes interfaces et/ou types qui définisse les objets manipulés et rendus.
Dans le controller nous trouvons successivement les documentations swagger, la définition de la route, le temps de validité du Cache REDIS et enfin un appelle vers le service responsable du traitement. (Les routes ont été effacés pour plus de visibilité)
Dans le fichier de définition du module nous avons les différentes dépendances vers les autres modules dont nous aurons besoin. Comme expliqué plutôt, il faut voir ces modules comme des blocs de lego à part entière qui peuvent sans problème être mis dans d’autre applications ou transformé en microservices.
Dans le service, qui représente l’ouvrier de ce module, nous retrouvons toutes les fonctions / méthodes permettant d’effectuer le ou les traitement(s).
D’abord nous avons la “PUBLIC FACADE” de cette classe qui représente le point d’entrée du service c’est elle que toutes les autres modules appelleront quand ils auront besoin de son aide.
Ensuite nous avons les “Querier” ils se charge de rassembler les données dont nous auront besoin pour faire le traitement. Ici ils sont assez spéciale car ils doivent effectuer une requête que si leur module est sélectionné. Notez la parallélisation des processus avec les “Promises” nous y reviendront plus tard.
Enfin nous avons les “WORKERS”. C’est le / les fonction(s) ou méthodes qui s’occupent de faire le plus difficile c’est à dire la logique et l’assemblage de tous les produits intermédiaires.
C’est là où le plus souvent, les notions d’Optimisation, de structure de données et d’algorithmique entre en jeu. Il sera l’un des éléments clés de la précision, de la rapidité et de la performance d’un traitement.
Il est souvent accompagnés d’utilities afin de toujours rester dans les normes solides et de faciliter la lisibilité du code.
Chaque module fait sont traitement en appliquant presque cette démarche mais bien évidement la logique et les besoins dans chacun d’entre eux.
PREDICTION & PREVISION AI (TENSORFLOW JS)
Machine Learning et DataScience avec … JavaScript 😨?! Oui, c’est possible🥳. Python n’est pas le seul langage que vous pouvez utiliser pour le faire.
Tensorflow a une version JS qui permet de s’approprier le ML ou DS avec JS. Je ne suis qu’à mais tout début avec Tensorflow donc cette partie “Prediction’” est actuellement gérée depuis l’ API de Power bi. Mais nous nous déportons de plus en plus vers Hal9 basée sur TF qui nous permettront d’avoir facilement plusieurs accès à la prédiction juste avec leurs APIs:
…
Une fois que tout cela a été mis en place les autres challenges étaient de toujours réussir à répondre aux différentes demandes en termes de Dashboard, de métriques, de fonctionnalités tout en gardant l’appli toujours plus rapide et disponible.
Nous allons aborder maintenant la dernière partie. Celle qui à chaque nouvelle feature représente un nouveau défi.
L’optimisation et le Scaling !