Le programmeur au travail

Adopter NoSQL avec MongoDB

Ted Neward

Télécharger l'exemple de code

Au cours des dix dernières années, depuis l'annonce de Microsoft .NET Framework en 2000 et sa première parution en 2002, les développeurs .NET se sont efforcés d'attraper au vol toutes les nouvelles fonctionnalités que Microsoft leur a jeté en pâture. Et comme si cela ne suffisait pas, « la communauté .NET »—à savoir les développeurs qui l'utilisent quotidiennement et les autres—s'est éclatée en créant des fonctions répondant à des besoins qui avaient échappé à la vigilance de Microsoft, ou pour le simple plaisir de créer du chaos et de la confusion (au choix).

L'une de ces « nouveautés » à émerger de la communauté non alignée sur la doxa Microsoft est le mouvement NoSQL, un groupe de développeurs qui s'est donné pour mission de défier ouvertement l'idée que toutes les données sont/seront/doivent être stockées dans un système de base de données relationnelle de quelque forme que ce soit. Tables, lignes, colonnes, clés primaires, contraintes de clé étrangère, discussions sur les nulls, débats pour décider si une clé primaire doit être naturelle ou non… ne respectent-il donc rien ?

Dans cet article, et dans les suivants, j'examinerai un des principaux outils défendus par les partisans du mouvement NoSQL : MongoDB, dont le nom provient de « humongous » (gigantesque), d'après le site Web qui lui est consacré (je n'invente rien !). Tout ce qui concerne MongoDB sera abordé : installation, exploration et utilisation à partir de l'infrastructure .NET Framework, y compris la prise en charge LINQ offerte ; son utilisation à partir d'autres environnements (applications de bureau, applications et services Web) ; et sa configuration de sorte que les admins Windows de production ne vous immolent pas en place publique.

Problème (ou, En quoi ça me concerne, encore ?)

Avant de plonger plus profondément dans les détails de MongoDB, il est légitime de se demander pourquoi un développeur .NET Framework devrait sacrifier la prochaine demi-heure de sa vie à lire cet article. Après tout, SQL Server existe en édition Express gratuite et redistribuable qui offre une option de stockage de données plus légère que les bases de données relationnelles liées à l'entreprise ou au centre de données traditionnelles, et il existe de nombreux outils et de bibliothèques permettant d'y accéder facilement, à commencer par les structures LINQ et Entity Framework de Microsoft.

Le problème est que la force du modèle relationnel—le modèle relationnel lui-même—est également sa plus grande faiblesse. La plupart des développeurs, en .NET, Java ou autre, sont capables—après quelques années d'expérience—de décrire de manière extrêmement détaillée comment tout n'entre pas parfaitement dans un modèle « carré » à base de tables/lignes/colonnes. Essayer de modéliser des données hiérarchiques peut rendre le développeur le plus expérimenté complètement dingue, à tel point que Joe Celko a consacré un livre—« SQL pour les malins, Troisième édition » (Morgan-Kaufmann, 2005)—au concept de modélisation de données hiérarchiques dans un modèle relationnel. Si vous ajoutez à cela la donnée de base qui veut que les bases de données relationnelles procurent une structure inflexible aux données (le schéma de la base de données), essayer de prendre en charge les suppléments ad hoc aux données frise l'incohérence. (Rapidement, à main levée : Combien d'entre-vous utilisent des bases de données dotées d'une colonne Notes, et pourquoi pas de colonnes Note1, Note2, Note3…?)

Personne au sein du mouvement NoSQL ne prétend que le modèle relationnel n'a pas ses forces ou que la base de données relationnelle va disparaître, mais un fait de base de la vie du développeur des vingt dernières années est que les développeurs ont fréquemment stockée des données dans des bases de données relationnelles qui ne sont pas (et parfois vraiment pas du tout) relationnelles par nature.

La base de données orientée document stocke des « documents » (maillage étroit de collections de données qui ne sont généralement pas connectées à d'autres éléments de données dans le système) plutôt que des « relations ». Par exemple, les entrées de blog dans un système de blogs sont entièrement déconnectées les unes des autres et même, en cas de référence de l'une à une autre, dans la plupart des cas la connexion s'effectue via un lien hypertexte qui a vocation à être déréférencé par le navigateur de l'utilisateur, pas de manière interne. Les commentaires faits à cette entrée de blog sont entièrement centrés sur cette entrée de blog, et il est rare que les utilisateurs veuillent voir l'agrégation de tous les commentaires, quelle que soit l'entrée qu'ils commentent.

De plus, les bases de données orientées document ont tendance à exceller dans les environnements de haute performance ou de haute simultanéité ; MongoDB est particulièrement adapté aux performances élevées, tandis qu'un de ses proches cousins, CouchDB, est plus à l'aise dans les scénarios de haute simultanéité. Tous deux renoncent à toute sorte de support de transaction multiobjet, ce qui signifie que bien qu'ils prennent en charge les modifications simultanées d’un même objet dans une base de données, toute tentative de modification de plus d'un objet à la fois ouvre une petite fenêtre de temps où ces modifications peuvent être vues « en passant ». Les documents sont mis à jour de manière atomique, mais en aucun cas une transaction ne peut couvrir des mises à jour de documents multiples. Cela ne signifie pas que MongoDB ne possède aucune durabilité. Cela signifie juste que l'instance MongoDB ne survivra pas à une coupure de courant aussi bien qu'une instance SQL Server. Les systèmes faisant usage d'une sémantique ACID (Atomicité, Cohérence, Isolation, Durabilité) complète sont plus adaptés aux systèmes de base de données relationnelle traditionnels. Il serait donc surprenant de trouver des données critiques dans une instance MongoDB dans un avenir proche, sauf peut-être sous forme de données répliquées ou en cache, sur un serveur Web.

En règle générale, MongoDB fonctionne bien avec les applications et les composants qui ont besoin de stocker des données auxquelles on peut accéder rapidement et fréquemment. Données d'analyse, préférences et paramètres d'utilisateur de site Web—et toute forme de système où les données ne sont pas totalement structurées ou doivent être structurellement flexibles—sont des candidats naturels à MongoDB. Cela ne signifie pas que MongoDB n'est pas parfaitement apte à être un magasin de données principal pour données opérationnelles ; cela signifie juste que MongoDB fonctionne bien dans les domaines qui ne conviennent pas vraiment aux systèmes de gestion de base de données relationnels (SGBDR) traditionnels, ainsi que dans les domaines pouvant convenir aux deux.

démarrage

Comment mentionné précédemment, MongoDB est un package logiciel en open source facilement téléchargeable depuis le site Web MongoDB, mongodb.com. Il suffit d'ouvrir le site Web dans un navigateur pour trouver les liens vers le package binaire téléchargeable Windows ; cherchez le lien Téléchargements dans la marge droite de la page. Si vous préférez les liens directs, cliquez sur mongodb.org/display/DOCS/Downloads. À l'heure où j'écris ces lignes, la version stable du produit est l'édition 1.2.4. Il ne s'agit de rien d'autre qu'un package zippé, dont l'installation est donc excessivement facile : Il suffit de dézipper le contenu où vous le souhaitez.

Sérieusement. C'est tout.

Le fichier .zip se décompresse en trois répertoires : bin, include et lib. Le seul répertoire d'intérêt est le répertoire bin, qui contient huit exécutables. Aucune autre dépendance (ou runtime) n'est nécessaire et en fait, seuls deux exécutables nous intéressent pour le moment. Il s'agit de mongod.exe, le processus de base de données MongoDB lui-même, et de mongo.exe, le client shell de ligne de commande, qui est généralement utilisé de la même manière que l'ancien client shell de ligne de commande SQL Server isql.exe : pour vérifier que les éléments sont bien installés et opérationnels, parcourir directement les données et effectuer des tâches administratives.

Vérifier que tout est correctement installé est aussi simple que de démarrer mongod depuis un client de ligne de commande. Par défaut, MongoDB souhaite stocker les données dans le chemin du système de fichiers par défaut, c:\data\db, mais ce paramètre est configurable à l'aide d'un fichier de texte passé par nom dans la ligne de commande via --config. En partant du principe qu'un sous-répertoire nommé db existe là où mongod doit être lancé, vérifier que tout est carré est aussi simple que ce qu'illustre la figure 1.

image: Firing up Mongod.exe to Verify Successful Installation

Figure 1 Exécuter Mongod.exe pour vérifier que l'installation a réussi

Si le répertoire n'existe pas, MongoDB ne le crée pas. Notez que sur ma boîte Windows 7, la boîte de dialogue « Cette application veut ouvrir un port » apparaît à l'écran au démarrage de MongoDB. Vérifiez que le port (27017 par défaut) est accessible, ou la connexion à ce port sera… délicate, dans le meilleur des cas. (Je développerai ce sujet dans un prochain article qui sera consacré à l'introduction de MongoDB dans un environnement de production.)

Une fois le serveur en marche, s'y connecter à l'aide du shell n'est pas plus compliqué : l'application mongo.exe lance un environnement de ligne de commande qui autorise une interaction directe avec le serveur, comme l'illustre la figure 2.

image: Mongo.exe Launches a Command-Line Environment that Allows Direct Interaction with the Server

Figure 2 Mongo.exe lance un environnement de ligne de commande qui autorise l'interaction directe avec le serveur

Par défaut, le shell se connecte à la base de données de « test ». Puisque l'objectif ici est juste de vérifier que tout fonctionne correctement, le test est concluant. Bien entendu, d'ici il est assez facile de créer des exemples de données qui fonctionnent avec MongoDB, comme un objet rapide qui décrit une personne. Cela nous permet d'observer rapidement comment MongoDB voit les données à démarrer, comme l'illustre la figure 3.

image: Creating Sample Data

Figure 3 Création d'exemples de données

Pour l'essentiel, MongoDB utilise JavaScript Object Notation (JSON) comme sa notation de données, ce qui explique à la fois sa flexibilité et la manière avec laquelle ses clients interagissent avec lui. En interne, MongoDB stocke les données en BSON, un sur-ensemble binaire de JSON, pour plus de facilité de stockage et d'indexation. JSON demeure toutefois le format d'entrée/sortie préféré de MongoDB, et est généralement le format documenté utilisé d'un bout à l'autre du site Web et du wiki MongoDB. Si vous n'êtes pas familiarisé avec JSON, il vaut mieux prendre le temps de se mettre à la page avant de plonger tête en avant dans MongoDB. Pendant ce temps, juste pour le plaisir, jetez un coup d'œil dans le répertoire où mongod stocke les données et vous verrez qu'une paire de fichiers nommés « test » a fait son apparition.

Assez joué. Il est temps de passer aux choses sérieuses. Quitter le shell se résume à taper «  exit », et fermer le serveur à taper Ctrl+C dans la fenêtre ou à la fermer. Le serveur capture le signal de fermeture et met fin à tous les programmes avant de quitter le processus.

Le serveur de MongoDB (sans oublier le shell, bien que son rôle soit secondaire ici) est écrit comme une application C++ native. Ca vous rappelle des souvenirs ? Y accéder nécessite donc une sorte de pilote .NET Framework qui sait comment se connecter via le socket ouvert pour faire passer des commandes et des données. MongoDB n'est pas fourni avec un pilote .NET Framework, mais la communauté s'est chargé de pallier cet oubli, la « communauté » étant ici un développeur du nom de Sam Corder, qui a mis au point un pilote .NET Framework et le support LINQ pour accéder à MongoDB. Le fruit de son travail existe en format source et binaire, sur le site github.com/samus/mongodb-csharp. Téléchargez soit les binaires sur cette page (consultez le coin supérieur droit), soit les sources pour construire votre propre pilote. Dans les deux cas, le résultat sera deux assemblys. MongoDB.Driver.dll et MongoDB.Linq.dll. Un petit Ajouter une référence au nœud Références du projet et le .NET Framework est prêt à fonctionner.

Écriture du code

En principe, l'ouverture d'une connexion à un serveur MongoDB en cours d'exécution n'est pas très différente de l'ouverture d'une connexion à n'importe quelle autre base de données, comme illustré en figure 4.

Figure 4 Ouverture d'une connexion à un serveur MongoDB

using System;
using MongoDB.Driver; 

namespace ConsoleApplication1
{
  class Program
  {
    static void Main(string[] args)
    {
      Mongo db = new Mongo();
      db.Connect(); //Connect to localhost on the default port
      db.Disconnect();
    }
  }
}

Découvrir l'objet créé précédemment n'est pas difficile, juste… différent… de ce à quoi les développeurs .NET Framework sont habitués (voir la figure 5).

Figure 5 Découverte d'un objet Mongo créé

using System;
using MongoDB.Driver; 

namespace ConsoleApplication1
{
  class Program
  {
    static void Main(string[] args)
    {
      Mongo db = new Mongo();
      db.Connect(); //Connect to localhost on the default port.
      Database test = db.getDB("test");
      IMongoCollection things = test.GetCollection("things");
      Document queryDoc = new Document();
      queryDoc.Append("lastname", "Neward");
      Document resultDoc = things.FindOne(queryDoc);
      Console.WriteLine(resultDoc);
      db.Disconnect();
    }
  }
}

Si cela est un peu impressionnant, pas de panique : c'est écrit « in extenso » car MongoDB stocke les données différemment des bases de données traditionnelles.

Pour commencer, souvenez-vous que les données insérées précédemment avaient trois champs : prénom, nom et âge, et que chacun d'eux sont des éléments permettant l'extraction des données. Plus important encore, la ligne dans laquelle elles étaient stockées, expulsée de manière plutôt cavalière, était « test.things.save() », ce qui implique que les données sont stockées dans un emplacement appelé « things ». En langage MongoDB, « things » est une collection, et implicitement, toutes les données sont stockées dans une collection. Les collections quant à elles contiennent des documents, qui comportent des paires clé/valeur où les valeurs peuvent être des collections supplémentaires. Dans ce cas, les « things » est une collection stockée à l'intérieur d'une base de données qui, comme précédemment mentionné, est la base de données de test.

Par conséquent, récupérer les données signifie d'abord de se connecter au serveur MongoDB, puis à la base de données de test, puis de trouver la collection « things ». C'est ce que font les quatre premières lignes de la figure 5 : créer un objet Mongo qui représente la connexion, se connecte au serveur, se connecte à la base de données de test, puis accède à la collection « things ».

Une fois la collection renvoyée, le code peut émettre une demande de recherche d'un document via l'appel FindOne. Mais, comme avec toutes les bases de données, le client ne veut pas récupérer tous les documents de la collection pour y trouver ensuite celui qui l'intéresse. D'une manière ou d'une autre, la demande doit être contrainte. Dans MongoDB, cela s'obtient en créant un Document qui contient les champs et les données à rechercher dans ces champs, un concept connu sous l'appellation de requête par l'exemple, ou QBE (Query By Example). Étant donné que l'objectif consiste à trouver le document contenant un champ « Nom » dont la valeur est définie à « Neward », un document contenant un champ « Nom » et sa valeur est créée et transmise comme paramètre de FindOne. Si la requête aboutit, elle renvoie un autre Document contenant toutes les données en question (plus un champ supplémentaire) ; autrement, elle renvoie Null.

Au fait, la version abrégée de cette description peut se résumer à :

Document anotherResult = 
         db["test"]["things"].FindOne(
           new Document().Append("lastname", "Neward"));
       Console.WriteLine(anotherResult);

Quand la requête est exécutée, non seulement les valeurs envoyées à l'origine apparaissent, mais une nouvelle également, un champ _id qui contient un objet ObjectId. Il s'agit de l'identificateur unique de l'objet qui a été inséré silencieusement par la base de données lors du stockage des nouvelles données. Toute tentative de modification de cet objet devra préserver ce champ sans quoi la base de données estimera qu'il s'agit d'un nouvel objet qui est envoyé. En règle générale, cela s'effectue par modification du Document renvoyé par la requête :

anotherResult["age"] = 39;
       things.Update(resultDoc);
       Console.WriteLine(
         db["test"]["things"].FindOne(
           new Document().Append("lastname", "Neward")));

Toutefois, il est toujours possible de créer une nouvelle instance de Document et de renseigner manuellement le champ _id de sorte qu'il corresponde à ObjectId, si vous préférez :

Document ted = new Document();
       ted["_id"] = new MongoDB.Driver.Oid("4b61494aff75000000002e77");
       ted["firstname"] = "Ted";
       ted["lastname"] = "Neward";
       ted["age"] = 40;
       things.Update(ted);
       Console.WriteLine(
         db["test"]["things"].FindOne(
           new Document().Append("lastname", "Neward")));

Bien sûr, si l'identificateur _id est déjà connu, il peut également être utilisé comme critère de la requête.

Notez que le Document est non typé. Presque n'importe quoi peut être stocké dans un champ sous n'importe quel nom, y compris certains types de valeur .NET Framework principaux, tels que DateTime. Techniquement, comme indiqué, MongoDB stocke les données BSON, qui comprennent certaines extensions des types JSON traditionnels (chaîne, entier, booléen, double et null—bien que les nulls ne sont autorisés que sur les objets, pas dans les collections) tels que l'objet ObjectId précédemment mentionné, les données binaires, les expressions régulières et le code JavaScript incorporé. Pour le moment, nous laisserons ces deux derniers de côté. Le fait que BSON puisse stocker les données binaires signifie que tout ce qui est réductible à un tableau d'octets peut être stocké, et signifie concrètement que MongoDB peut stocker n'importe quoi, même s'il n'est pas forcément en mesure d'effectuer des requêtes dans ce magma binaire.

Pas encore mort (ou fini) !

Il reste beaucoup à dire sur MongoDB, à commencer par le support LINQ ; l'exécution de requêtes côté serveur plus complexes qui dépassent les simples fonctions de requêtes de style QBE montrées ici ; et l'introduction de MongoDB dans un environnement de batterie de serveurs de production. Mais pour le moment, cet article et l'examen approfondi d'IntelliSense devrait suffire à mettre le programmeur au travail.

Au fait, si vous souhaitez que je traite une rubrique particulière, faites-moi signe. Sans démagogie, c'est votre édito après tout. Bon codage !

Ted Neward co-dirige Neward & Associates, une firme indépendante spécialisée dans les systèmes de plateforme d'entreprise .NET Framework et Java. Auteur de plus de 100 articles, Ted Neward est MVP C#, porte-parole de l'INETA et a rédigé ou corédigé plus d'une dizaine d'ouvrages, y compris le prochain « Professional F# 2.0 » (Wrox). Il accepte des missions de conseil et de tutorat. Vous pouvez le contacter à l'adresse ted@tedneward.com consulter son blog à l'adresse blogs.tedneward.com.

Je remercie l'expert technique suivant d'avoir relu cet article : Kyle Banker et Sam Corder