7 Août 2024
Dans ce cinquième article consacré au langage Rust, nous allons nous intéresser à une thématique classique des applications d'entreprise, la persistance dans des bases de données relationnelles. A l'occasion des troisième et quatrième articles, nous avons en effet assuré la persistance des données dans un simple fichier dont nous avons rendu le contenu accessible en lecture et en écriture via une interface REST JSON. La conception proposée lors du troisième article prévoyait d'implémenter le gestionnaire de paramètres autrement que par un simple stockage fichier. C'est donc sur la base d'un client PostgreSQL synchrone que nous allons développer cette implémentation alternative. Le diagramme suivant illustre cette intention.
Le test unitaire qui va permettre de vérifier le fonctionnement de cette nouvelle implémentation est semblable à celui qui avait été mis en place pour tester la persistance des paramètres dans un fichier. Commençons avec le cas nominal de récupération d'un paramètre existant.
La différence avec la base de test précédente se situe dans la fonction de construction du ParameterRepository. Dans notre cas, cette fonction s'appuie sur une structure précisant les paramètres de connexion au serveur de base de données.
Le connecteur testé comprend une structure mémorisant les paramètres de connexion et propose une fonction de récupération des paramètres consistant à appeler une procédure stockée dont on verra le détail ultérieurement. Ce connecteur requiert l'import d'un module supplémentaire.
Ce module étant importé, l'implémentation de la fonction de récupération des paramètres de configuration consiste dans un premier temps à créer une connexion à la base (postgres::Client).
Cette connexion, ou plus exactement un client PostgreSQL, peut être utilisé pour requêter la base de données. La procédure stockée qui permettra de récupérer un paramètre à partir de son nom sera nommée "get_parameter_value" et prendra en paramètre le nom du paramètre à lire.
On notera ici que la requête est instanciée avec un paramètre $1 qui est substitué lors de l'appel. On préfèrera cette modalité de construction à une concaténation de chaînes de caractères afin d'éviter les attaques par injection de code SQL. Le résultat de la requête est traitée par la méthode "extract_parameter".
Une fois le code Rust prêt, il reste encore à créer le schéma de base de données et la procédure stockée attendue (et à alimenter la table PARAMETERS avec une entrée correspondant au paramètre attendu).
Pour finaliser le test de cette première fonction, on ajoutera trois fonctions de test :
La fonction de lecture d'un paramètre étant réalisée, on peut compléter l'implémentation du DatabaseParameterRepository avec la fonction "upsert". Le test suivant permet de vérifier l'insertion d'un nouveau paramètre:
Et la fonction suivante permet de tester la mise à jour d'un paramètre existant:
Dans les deux cas, il est nécessaire que la base de données soit dans un état maitrisé. On peut ainsi compléter la fonction de récupération de la connexion de test par une initialisation du contenu de la base.
L'implémentation de la méthode "upsert" est la suivante dans le code Rust:
Et la suivante au niveau de la procédure stockée appelée:
A ce stade, il ne reste plus qu'à mettre en place la fonction de suppression d'un paramètre, dont l'implémentation est similaire à celle de l'upsert et pourra être consulté au niveau du repository git.
Avant de passer à l'intégration de connecteur PostgreSQL au sein du serveur REST déployé dans le cadre de l'article précédent, notons que l'implémentation proposée crée une connexion à chaque appel de méthode. Une telle approche peut sembler inefficace. En effet, on serait tenté d'établir la connexion avec la base de données dans le constructeur et de gérer, dans la structure du DatabaseParameterRepository, la connexion plutôt que les paramètres de cette connexion. Le problème avec cette approche est qu'elle ne permet pas de respecter l'immutabilité du repository lors d'une lecture de paramètre. En effet le "Client" PostgreSQL est "modifié" lors de tout appel en base de données, y compris une simple requête de sélection. Pour des raisons de simplicité d'implémentation, et afin de ne pas devancer des besoins de résistance au facteur d'échelle, on acceptera d'établir une connexion à chaque requête.
Il ne nous reste plus qu'un pas à franchir pour intégrer cette implémentation du gestionnaire de paramètres derrière notre serveur REST. Un dernier pas, mais comme dirait Indiana, "c'est en général à ce moment que le sol se dérobe sous vos pieds". En lançant le serveur REST avec ce repository et en appelant le service GET sur un paramètre existant, on obtient une erreur.
Cannot start a runtime from within a runtime. This happens because a function (like block_on) attempted to block the current thread while the thread is being used to drive asynchronous tasks.
En y regardant de plus près, on peut en effet constater que l'établissement de connexion s'appuie sur la création d'un nouveau "thread".
Il existe des implémentations alternatives de clients PostgreSQL pour Rust, mais toutes s'appuient sur une approche asynchrone. Cette mésaventure explique que de nombreux développeurs Rust considèrent que le choix d'une approche synchrone ou asynchrone est un choix fondamental à ne pas négliger. Et force est de reconnaître que c'est un point qui a ici été négligé.
A suivre...