Publié par

Il y a 7 années -

Temps de lecture 5 minutes

Scripter le shell MongoDB

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 type DB qui permet d’interagir avec une base (par exemple à travers la fonction getCollectionNames ci-dessus) ;
  • collection.js : définit le type DBCollection de db.expendables ;
  • query.js : définit le type DBQuery renvoyé par find() ;
  • 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ême db.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.

 

Publié par

Commentaire

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Nous recrutons

Être un Xebian, c'est faire partie d'un groupe de passionnés ; C'est l'opportunité de travailler et de partager avec des pairs parmi les plus talentueux.