De MariaDB à MongoDB ep3 : extraction, boucles, conversion
Après avoir posé le concept, puis tenté les premières choses avec succès (pas nécessairement du premier coup), il est temps de monter en puissance avant le bouquet final. Donc aujourd’hui on met les doigts dans les tuples, boucles, changements de types, bref, ça va chauffer un peu.
Retour sur les tuples et les dictionnaires
Pour la faire court parce que je ne vais pas non plus vous faire un cours de Python aujourd’hui, les tuples sont des listes dont on ne peut pas changer le contenu. On peut les parcourir avec une boucle for. On les représente généralement entre parenthèses, les éléments séparés par une virgule (même un tuple avec un seul élément contient une virgule, me demandez pas pourquoi ils ont décidé ça). Ça ne vous rappelle rien ? Lorsqu’on a extrait les données de MariaDB, le résultat était le suivant pour extraire Skyfall :
1 |
({'dateAjout': '2013-10-08', 'id_type': 2L, 'pret': 0, 'nom_pret': '', 'titre': '007 Skyfall', 'annee': 2012L, 'id': 432L, 'types': 'Bluray', 't.id_type': 2L},) |
Et donc ici, notre élément est un dictionnaire, qui en Python, est représenté entre crochets, et contient des couples « clés : valeurs ». C’est fait exprès : si j’avais laissé MySQLdb extraire les données sans préciser de convertir en dictionnaire, j’aurais eu un tuple à la place, et LA grosse différence est que j’aurais du m’asseoir sur les noms des champs, je n’aurais eu que les valeurs en guise d’éléments. C’est vital, notamment parce que sinon je vais m’asseoir sur une capacité intéressante des dictionnaires, le fait de pouvoir les modifier, afin de les réinjecter ensuite dans MongoDB.
Une nécessaire adaptation du schéma de données
Dans mes premiers essais, j’ai affecté directement l’id du film dans le document. Avec MariaDB cet id est numérique, avec la propriété AUTO-INCREMENT, et pour lequel il tient un compteur. Ce qui veut dire qu’à chaque nouvel enregistrement (nouvelle ligne), il va utiliser ce compteur, l’attribuer au nouvel enregistrement, et l’incrémenter dans l’attente du prochain.
Cette propriété n’existe pas dans le monde MongoDB, par contre, il sait générer l’_id tout seul comme un grand. Cet identifiant, dans le monde MongoDB, est un ObjectId. Un exemple plus parlant, pour l’instant seulement avec le shell mongo :
1 2 3 |
> db.films.insert({"titre" : "007 Skyfall", "annee" : "2012", "type" : "Bluray", "pret" : "0", "dateAjout" : "2001-12-25" }) > db.films.find({ "titre" : "007 Skyfall" }) { "_id" : ObjectId("544fca1499abd8506afcc986"), "titre" : "007 Skyfall", "annee" : "2012", "type" : "Bluray", "pret" : "0", "dateAjout" : "2001-12-25" } |
Autre propriété : les types de données. Dans l’exemple au dessus, ainsi que dans les précédents où j’ai manipulé mes documents, je n’ai travaillé qu’avec des éléments de type string, des chaînes de caractères. Hors, pour reprendre l’extraction MariaDB au dessus, on a de la chaîne de caractères, et des nombres entiers. Je pense conserver cette propriété, ce qui réduira un peu la taille des données à stocker (j’y reviendrais).
Dernier point, qui sera une évolution par rapport au fonctionnement actuel : j’utilise deux variables et donc deux champs pour gérer le prêt d’un film : ‘pret’ qui est un entier court (tinyint(1)) qui indique si le film est prêté ou pas, et ‘nom_pret’ qui est une chaîne de caractères (varchar(128)) pour le nom de l’emprunteur. Je vais simplifier en n’utilisant que le champ ‘pret’ qui sera soit absent pour dire que le film est dispo, soit contiendra directement le nom du gus (un retour de prêt se traduisant par la suppression de la clé).
À vos marques, prêt ? Partez !
Premier tour de piste
On va regarder de près comment parcourir un résultat de requête MariaDB qui renvoie plusieurs réponses. Comme on a dit, c’est un tuple de dictionnaires. On va donc voir à quoi ça ressemble, et la meilleure méthode pour parcourir chaque résultat. Je vais prendre comme exemple la recherche de titre « 007 », qui contiendra donc, entre autres, les trois films James Bond. Histoire de vous rafraîchir la mémoire, je vais remettre en entier le script, et par la suite je n’ajouterais que les petits bouts :
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 |
#!/usr/bin/python # -*- coding: utf-8 -*- import sys import pymongo import MySQLdb as mdb from pymongo import Connection from pymongo.errors import ConnectionFailure def main(): base = mdb.connect('hote', 'utilisateur', 'motdepasse', 'basededonnees') #Remplacez par vos infos de connexion cursor = base.cursor(mdb.cursors.DictCursor) requete = """SELECT l.*, t.* FROM collect_liste_films l LEFT JOIN collect_types t ON l.id_type = t.id_type WHERE l.titre LIKE '%007%'""" ligne = cursor.execute(requete) data = cursor.fetchall() #On parcourt chaque élément du tuple data for film in data: print(film) annee = film['annee'] print(annee) if __name__ == "__main__": main() |
J’ai laissé l’ajout du module pymongo, même si on n’en a pas besoin pour cette première étape. Rien de très compliqué, Et si on l’exécute, le résultat est, je pense, limpide :
1 2 3 4 5 6 7 8 9 |
seboss666@heberg-new:~/web/www/api$ ./mongotest.py {'dateAjout': '2001-12-25', 'id_type': 1L, 'pret': 0, 'nom_pret': '', 'titre': '007 Casino Royale', 'annee': 2006L, 'id': 4L, 'types': 'DVD', 't.id_type': 1L} 2006 {'dateAjout': '2001-12-25', 'id_type': 1L, 'pret': 0, 'nom_pret': '', 'titre': '007 Quantum of solace', 'annee': 2008L, 'id': 5L, 'types': 'DVD', 't.id_type': 1L} 2008 {'dateAjout': '2001-12-25', 'id_type': 1L, 'pret': 0, 'nom_pret': '', 'titre': 'Les enfoir\xc3\xa9s 2007', 'annee': 0L, 'id': 193L, 'types': 'DVD', 't.id_type': 1L} 0 {'dateAjout': '2013-10-08', 'id_type': 2L, 'pret': 0, 'nom_pret': '', 'titre': '007 Skyfall', 'annee': 2012L, 'id': 432L, 'types': 'Bluray', 't.id_type': 2L} 2012 |
J’ai montré par la même occasion comment on accédait aux éléments du dictionnaire, ici en appelant la clé ‘annee’, on obtient la valeur (qui n’est pas renseignée pour les Enfoirés, je n’avais pas encore mis les garde-fous sur le formulaire).
Deuxième tour de piste
Pour chaque passage dans la boucle, on va adapter ce dont on a besoin. Ce qui veut dire ajouter ou modifier certains éléments, et en retirer d’autres :
- Ajouter, car je vais « renommer » le champ « types » en « type », ce qui revient à ajouter le nouveau avec la valeur de l’ancien, puis supprimer cet ancien
- Modifier, car on va remplacer le contenu de « pret » par celui de « nom_pret », si celui-ci n’est pas vide, sinon supprimer les deux
- Supprimer, car on ne conservera pas les champs « id_type », « t.id_type », « nom_pret », « types » et « id » (qui sera généré par MongoDB) devenus surperflus.
Plusieurs lignes seront nécessaires, mais rien de très compliqué, là encore (je remet juste la boucle) :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#On parcourt chaque élément du tuple data for film in data: print(film) #On commence par ajout et modifications film['type'] = film['types'] if (film['nom_pret'] != ""): film['pret'] = film['nom_pret'] else: del film['pret'] #Suppression des clés inutiles del film['types'] del film['nom_pret'] del film['id_type'] del film['t.id_type'] del film['id'] print(film) |
Probablement parce que je n’ai pas suffisamment d’expérience en Python, je n’ai pas trouvé de méthode élégante pour faire autrement qu’une seule ligne par champ à supprimer. Ceci dit je n’ai pas non plus de gros traitements à faire pour l’instant, car on ne travaille que sur quatre dictionnaires. Sur les 440, ça sera une autre paire de manches. Bref, vous remarquez que j’affiche deux fois film, une fois avant, une fois après. Et ça donne ça :
1 2 3 4 5 6 7 8 9 |
seboss666@heberg-new:~/web/www/api$ ./mongotest.py {'dateAjout': '2001-12-25', 'id_type': 1L, 'pret': 0, 'nom_pret': '', 'titre': '007 Casino Royale', 'annee': 2006L, 'id': 4L, 'types': 'DVD', 't.id_type': 1L} {'dateAjout': '2001-12-25', 'titre': '007 Casino Royale', 'annee': 2006L, 'type': 'DVD'} {'dateAjout': '2001-12-25', 'id_type': 1L, 'pret': 0, 'nom_pret': '', 'titre': '007 Quantum of solace', 'annee': 2008L, 'id': 5L, 'types': 'DVD', 't.id_type': 1L} {'dateAjout': '2001-12-25', 'titre': '007 Quantum of solace', 'annee': 2008L, 'type': 'DVD'} {'dateAjout': '2001-12-25', 'id_type': 1L, 'pret': 0, 'nom_pret': '', 'titre': 'Les enfoir\xc3\xa9s 2007', 'annee': 0L, 'id': 193L, 'types': 'DVD', 't.id_type': 1L} {'dateAjout': '2001-12-25', 'titre': 'Les enfoir\xc3\xa9s 2007', 'annee': 0L, 'type': 'DVD'} {'dateAjout': '2013-10-08', 'id_type': 2L, 'pret': 0, 'nom_pret': '', 'titre': '007 Skyfall', 'annee': 2012L, 'id': 432L, 'types': 'Bluray', 't.id_type': 2L} {'dateAjout': '2013-10-08', 'titre': '007 Skyfall', 'annee': 2012L, 'type': 'Bluray'} |
Pour rappel, il n’existe pas d’ordonnancement, le fait que les éléments ne soient pas dans le même ordre n’a aucune importance, à part pour la lisibilité.
Troisième tour de piste
Eh bien maintenant, on va tenter l’ajout du dictionnaire résultat dans MongoDB. On avait déjà détaillé la commande dans l’épisode précédent, il suffira donc de l’adapter ici au nom de variable :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
for film in data: #On commence par ajout et modifications film['type'] = film['types'] if (film['nom_pret'] != ""): film['pret'] = film['nom_pret'] else: del film['pret'] #Suppression des clés inutiles del film['types'] del film['nom_pret'] del film['id_type'] del film['t.id_type'] del film['id'] print(film) #On ajoute le film à MongoDB films.insert(film) |
J’ai supprimé le premier affichage de film et conservé le second, histoire de bien voir ce qui sera injecté dans MongoDB. On connaît déjà ce qui s’affiche quand on l’exécute, alors voyons voir le résultat en passant par le shell MongoDB :
1 2 3 4 5 |
> db.films.find() { "_id" : ObjectId("544fdd7fe0191820fd3a9af7"), "dateAjout" : "2001-12-25", "titre" : "007 Casino Royale", "annee" : NumberLong(2006), "type" : "DVD" } { "_id" : ObjectId("544fdd7fe0191820fd3a9af8"), "dateAjout" : "2001-12-25", "titre" : "007 Quantum of solace", "annee" : NumberLong(2008), "type" : "DVD" } { "_id" : ObjectId("544fdd7fe0191820fd3a9af9"), "dateAjout" : "2001-12-25", "titre" : "Les enfoirés 2007", "annee" : NumberLong(0), "type" : "DVD" } { "_id" : ObjectId("544fdd7fe0191820fd3a9afa"), "dateAjout" : "2013-10-08", "titre" : "007 Skyfall", "annee" : NumberLong(2012), "type" : "Bluray" } |
J’ai un « léger » problème : mon année est bien stockée, mais du fait que MySQLdb a décrété qu’un int(4) dans MariaDB devait se traduire par un entier long, elle est donc stockée sous la forme d’un entier long dans MongoDB, soit sous la forme d’une valeur sur 64bit. Ce qui est bien trop long, et consommera une blinde de ressources quand il s’agira d’attaquer les 400+ films, sans parler du futur. Tout ça pour une année sur quatre chiffres. Comme c’est la dernière valeur purement numérique des informations sur les films, j’ai deux choix : soit tenter de revenir à un entier plus court, soit le convertir en chaîne de caractères. Si l’on s’en réfère à cet article, avec quatre chiffres pour l’année je suis plus avisé de la stocker sous forme d’entier court, ce qui prendra moins de place et donc de ressources. On va donc ajouter une ligne dans la boucle pour tenter de corriger le tir, dans les ajouts/modifications, avant l’insertion du film :
1 |
film['annee'] = int(film['annee']) |
Au fait, entre chaque test, j’ai vidé la collection avec la commande db.films.remove() sans arguments, ce qui supprime tous les documents de la collection. Une fois fait, le résultat est édifiant (après avoir supprimé les anciens essais, encore une fois) :
1 2 3 4 5 |
> db.films.find() { "_id" : ObjectId("544fe47de01918211814c6e8"), "dateAjout" : "2001-12-25", "titre" : "007 Casino Royale", "annee" : 2006, "type" : "DVD" } { "_id" : ObjectId("544fe47de01918211814c6e9"), "dateAjout" : "2001-12-25", "titre" : "007 Quantum of solace", "annee" : 2008, "type" : "DVD" } { "_id" : ObjectId("544fe47de01918211814c6ea"), "dateAjout" : "2001-12-25", "titre" : "Les enfoirés 2007", "annee" : 0, "type" : "DVD" } { "_id" : ObjectId("544fe47de01918211814c6eb"), "dateAjout" : "2013-10-08", "titre" : "007 Skyfall", "annee" : 2012, "type" : "Bluray" } |
Et le sprint final ?
Et bien, notre script fonctionne, mais j’ai un problème de taille, au sens propre du terme : ici, les quatre résultats de la requête sont stockées dans la variable data, intégralement « interprétée » (fetchall() ) avant la boucle, ce qui peut paraître raisonnable. Mais ma base contient plus de quatre cents films, et tous les stocker dans une seule variable pourrait bien exploser les compteurs, et considérablement ralentir l’exécution. Il va donc falloir revoir quelque peu la méthode d’extraction. Mais on a déjà bien avancé, reposons les neurones jusqu’au prochain (et probablement dernier) épisode.