Le shell interactif de MongoDB s’appuie sur un interpréteur Javascript. Au-delà de la simple exécution de requêtes, il met donc à disposition de l’utilisateur toutes les fonctionnalités d’un vrai langage de programmation. Dans cet article, nous allons voir à travers quelques exemples comment automatiser des tâches répétitives, factoriser du code dans nos scripts, et même étendre les fonctionnalités du shell.
Commençons par créer une base et insérer quelques documents :
> use test > db.expendables.insert( { name: 'Sylvester' } ); > db.expendables.insert( { name: 'Jean-Claude' } ); > db.expendables.insert( { name: 'Chuck' } );
Lorsqu’une requête renvoie un seul document, c’est un objet Javascript :
> var single = db.expendables.findOne(); > single.name; Sylvester
Quand elle renvoie plusieurs documents, le résultat est également un objet. Si vous utilisez souvent MongoDB, vous connaissez sans doute sa méthode forEach
:
> db.expendables.find().forEach( function(single) { print(single.name); }); Sylvester Jean-Claude Chuck
forEach
applique son argument à tous les résultats de la requête, quel que soit leur nombre. Pour un contrôle plus fin, nous pouvons utiliser le résultat de find()
comme un itérateur :
> var query = db.expendables.find(); > query.hasNext(); true > var single = query.next(); > single.name; Sylvester
Tout ceci ne se limite pas aux résultats de requêtes : les objets internes de MongoDB (bases, collections…) peuvent également être manipulés par du code. L’exemple suivant, un peu plus élaboré, définit une fonction qui indique le taux d’occupation relatif des collections d’une base :
> var collectionRatios = function(db) { var total = db.stats().storageSize; var names = db.getCollectionNames(); // Cette collection doit être ajoutée à la main, car elle est cachée par getCollectionNames : names.push('system.namespaces'); names.forEach(function (collectionName) { var current = db[collectionName].storageSize(); var percent = Math.round(current / total * 10000) / 100; print(percent + '%\t' + collectionName); }); }; > collectionRatios(db); 33.33% expendables 33.33% system.indexes 33.33% system.namespaces
Read the source, Luke
L’environnement du shell est initialisé par un ensemble de scripts qui, comme le reste des sources de MongoDB, sont disponibles sur GitHub. On trouve entre autres :
db.js
: définit le typeDB
qui permet d’interagir avec une base (par exemple à travers la fonctiongetCollectionNames
ci-dessus) ;collection.js
: définit le typeDBCollection
dedb.expendables
;query.js
: définit le typeDBQuery
renvoyé parfind()
;util.js
: contient une myriade d’utilitaires, notamment les fonctions d’affichage (print
,tojson
,tojsononeline
…).
La lecture de ces fichiers nous permet d’améliorer nos premiers exemples. Ainsi, au lieu de créer des fonctions indépendantes, nous pouvons les attacher au prototype des objets concernés, pour en faire des méthodes :
> DB.prototype.collectionRatios = function() { collectionRatios(this); }; > use test // recréer l'objet db pour que la modification prenne effet > db.collectionRatios(); 33.33% expendables 33.33% system.indexes 33.33% system.namespaces
Those damn dirty apes
Avec un peu de monkey patching, nous pouvons même changer le comportement de méthodes existantes. Voici comment modifier update
pour qu’elle affiche le nombre de documents mis à jour :
// Sauvegarde d'un alias vers la méthode d'origine > DBCollection.prototype.__update__ = DBCollection.prototype.update; // Écrasement de la méthode > DBCollection.prototype.update = function(query, obj, upsert, multi) { this.__update__(query, obj, upsert, multi); // Appel à l'alias this._db.printLastCount("updated"); // Ajout de la nouvelle fonctionnalité (implémentée ci-dessous) }; > DB.prototype.printLastCount = function(operationDescription) { var result = this.getLastErrorObj(); if (!result.err && result.hasOwnProperty('n')) { var documentDescription = (result.n < 2) ? ' document ' : ' documents '; print(result.n + documentDescription + operationDescription); } }; > db.expendables.update( {}, {$set: {muscle: true}}, false, true ); 3 documents updated
J’en vois déjà certains froncer les sourcils, et ils ont raison : on imagine aisément les dérapages possibles de cette pratique. Il faudra faire preuve de mesure : l’ajout de simples traces me paraît raisonnable, mais tout ce qui pourrait changer le contrat d’appel aux méthodes est à bannir. D’un autre côté, vous avez maintenant une nouvelle blague pour votre collègue qui oublie de verrouiller son écran :
> DBCollection.prototype.update = function(query, obj, upsert, multi) { print("I'm afraid I can't let you do that, Dave"); };
Appliquer les changements de manière permanente
La commande mongo
accepte des scripts en argument, ils seront tous évalués dans le contexte global du shell. Ceci permet de pré-configurer sa session avec un script utilitaire. Il est également possible d’évaluer directement des expressions Javascript, ce qui offre un moyen de paramétrer nos scripts à la ligne de commande :
# Session interactive : $ mongo myUtils.js --shell # Lancement d'un script non interactif : $ mongo myUtils.js myScript.js # Lancement d'un script avec paramétrage : $ mongo --eval 'var myParam = 1;' myUtils.js myScript.js
Enfin, si un fichier .mongorc.js est présent dans le répertoire personnel de l’utilisateur, le shell l’exécutera au démarrage. Certains développeurs ont partagé des personnalisations intéressantes, notamment le très complet mongo-hacker (merci à Charles Blonde pour le lien).
Conclusion
À mon avis, utiliser un langage existant était un choix judicieux (ceux qui ont programmé en PL/SQL me comprendront). Javascript, par nature très ouvert, offre des possibilités intéressantes, mais aussi une puissance à utiliser avec modération. Il faudra garder en tête quelques règles :
- penser aux performances avec la volumétrie réelle : un
forEach
sur 15 millions de documents mettra quelques temps à se terminer, et mêmedb.stats()
a un coût ; - ne pas réinventer les fonctions prédéfinies du shell, et ne pas oublier qu’elles peuvent aussi s’appeler entre elles ;
- communiquer et documenter clairement les changements.