Vous utilisez Node.js et vous souhaitez utiliser le gestionnaire de base de données NoSQL MongoDB ? Alors bienvenue dans ce petit cours !
Lorsque je me suis mis à utiliser MongoDB, j'ai eu quelques difficultés à rassembler certaines informations dont j'avais besoin, à savoir :
- Comment est structurée une "base de données" MongoDB ?
- Comment installer et manipuler le shell de MongoDB ?
- Comment faire le lien entre mon application Node.js et MongoDB ?
- Comment faire en sorte que ça fonctionne (important, ça !)
Bon, en réalité, toutes ces informations sont trouvables sur le Web, mais c'est disparate, et beaucoup en anglais. Ou alors ça propose d'utiliser des trucs comme Mongoose et autres modules, or moi, je veux "le faire moi-même", sinon ce n'est pas amusant.
Ce tutoriel est simplement une mise en route pour utiliser MongoDB avec Node.js : ce n'est pas un tutoriel sur MongoDB, ni sur Node.js !
So, venez, on va étudier tout ça !
- Fonctionnement de MongoDB et structure des données
- Installer MongoDB et faire quelques tests
- Se connecter depuis Node.js
- Opérations depuis Node.js
- Requêtes avancées
Fonctionnement de MongoDB et structure des données
Tout comme MySQL, MongoDB est un système de gestion de bases de données (SGBD). Il s'agit donc d'un programme qui va gérer nos différentes bases de données. Sauf qu'ici, il ne s'agit pas du bon vieux SQL, mais de bases de données NoSQL orientées documents !
Notez qu'une base de données MongoDB n'est pas faite pour remplacer une base de données relationnelle classique (par exemple MySQL, PostgreSQL, Oracle…). Il est courant de voir cohabiter deux SGBD : un relationnel, et un dit NoSQL.
Des collections et des documents
Chaque entrée d'une base MongoDB est appelée document. Un document n'est rien d'autre qu'un objet JSON contenant une série de clefs/valeurs. Voici un exemple de document :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | { _id: ObjectID("53dfe7bbfd06f94c156ee96e"), name: "Adrian Shephard", game: "Half-Life: Opposing Force", serie: [ "Half-Life", "Half-Life: Blue Shift", "Half-Life: Decay" ], options: { type: ["solo", "multiplayer"], os: "Windows" }, release: ISODate("1999-11-10T00:00:00Z") } |
Un document MongoDB est comparable à une entrée au sein d'une table SQL. Les documents sont rassemblés au sein de collections, qui sont donc les homologues des tables SQL. Voici un petit schéma démonstratif :
Le fait de structurer les documents en utilisant une syntaxe JSON les rend faciles à manipuler. Vous verrez, tout se fera en JavaScript, nul besoin d'apprendre un langage comme le SQL pour faire des requêtes, et l'accès aux propriétés des documents se fera en JavaScript "natif", comme s'il s'agissait d'objets tout à fait normaux !
En réalité, les documents ne sont pas stockés sous la forme de JSON, mais de BSON, qui est la représentation binaire du JSON. Mais ça ne change rien pour nous.
Considérons la collection Personnages. Cette collection va recevoir divers documents, un document par personnage. Le contenu de la collection pourrait s'écrire comme ceci :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // Document n° 1 { name: "Gordon Freeman", game: "Half-Life" } // Document n° 2 { name: "Chell Johnson", game: "Portal", gender: "F" } // Document n° 3 { name: "Soldier", game: "Team Fortress²", weapons: [ "Lance-roquettes", "Shotgun", "Pelle" ] } |
Quelque chose de flagrant doit vous sauter aux yeux : la structure de chaque document est différente !
Et pour cause, à l'inverse des tables SQL, aucune structure n'est imposée par MongoDB. Chaque document d'une collection peut différer d'un autre ! C'est au développeur de faire attention à garder une structure cohérente pour ne pas se perdre. Cette spécificité permet évidemment de gagner de l'espace, puisque si une clef est vide, il suffit de ne pas la déclarer. C'est donc un avantage en terme de stockage et accessoirement en terme de rapidité.
Formats spéciaux
Le premier code d'exemple montrait deux objets spéciaux : ObjectID
et ISODate
. Ce sont des "types" de données, reconnus par MongoDB.
ObjectID
ObjectID
représente l'identifiant d'un document, et est toujours contenu dans la propriété _id
.
Lorsque l'on insère un nouveau document dans une collection, MongoDB lui définit automatiquement une propriété _id
qui va contenir un objet ObjectID
, lequel représente l'identifiant du document.
C'est un peu l'équivalent d'un auto-incrément en SQL, sauf qu'ici c'est MongoDB qui gère le tout, et que l’identifiant est une chaîne de caractères aléatoire. Nous reviendrons sur les caractéristiques d'ObjectID
quand nous aborderons les requêtes.
ISODate
Cet objet permet de stocker une date au format ISO. Cela simplifie la manipulation des dates puisque ISODate
est un conteneur de Date
et supporte les mêmes méthodes (getHours()
, getMinutes()
…) :
1 2 | ISODate("1999-11-10T00:00:00Z").getHours(); ISODate("1999-11-10T00:00:00Z").getMinutes(); |
Nombres
Outre la gestion des ID et des dates ISO, MongoDB permet de gérer "manuellement" certains nombres. Pour ça, les objets NumberLong
et NumberInt
définissent respectivement des nombres en 64 bits et en 32 bits. Par défaut, MongoDB stocke les nombres sous la forme de nombres à virgule flottante.
Installer MongoDB et faire quelques tests
Installer MongoDB
MongoDB est disponible pour Windows, Mac OS, Linux et Solaris. Rendez-vous sur la page officielle de téléchargement pour télécharger la version qui correspond à votre système d'exploitation. Dans mon cas, il s'agit d'un Windows 64 bits.
Pour démarrer MongoDB, exécutez le fichier mongod.exe (Windows) ou mongod (Unix), situé dans le dossier bin du dossier dans lequel MongoDB est installé. Lors de son lancement, MongoDB recherchera le dossier /data/bd à la racine de votre disque. Si ce dossier n'existe pas, créez-le.
Vous pouvez évidemment créer le dossier /data/db ailleurs ; dans ce cas, il faudra spécifier son emplacement en utilisant le paramètre --dbpath
.
Pour plus de facilité, sous Windows, je vous conseille de vous créer un petit script .bat. Créez un nouveau fichier texte (.txt) avec le Bloc-Notes. Dedans, écrivez le chemin vers mongod.exe, suivi du paramètre --dbpath
, comme ceci :
1 2 | TITLE MongoDB "F:\Program Files\MongoDB\bin\mongod.exe" --dbpath "F:\Program Files\MongoDB\bin\data" |
Enregistrez le fichier, et changez l'extension en .bat. Un double clic sur le fichier .bat lancera la commande, et donc le démarrage de MongoDB.
Lancez donc MongoDB, et voici ce que vous devriez obtenir. Si la dernière ligne affiche waiting for connections, c'est bon, MongoDB fonctionne et attend vos consignes !
Faites attention, il y a deux exécutables : mongod.exe (mongod pour Unix) qui démarre le "serveur" MongoDB, et mongo.exe (mongo pour Unix), que l'on va utiliser maintenant, qui permet de dialoguer avec le serveur. mongo.exe (mongo pour Unix) est une version console d'un utilitaire tel que PHPMyAdmin (pardonnez-moi pour cette analogie ).
Quelques opérations
Maintenant que MongoDB est lancé, nous allons faire quelques manipulations. Nous allons pour cela ouvrir le shell (la console) nous permettant de commander MongoDB : mongo.exe (Windows) ou mongo (Unix) :
Lister et choisir la base de données
Par défaut, MongoDB se connecte à la base de données appelée test. La commande show dbs
permet d'afficher toutes les bases de données gérées par MongoDB. Voici ce que j'obtiens :
1 2 3 4 | > show dbs admin <empty> local 0.078GB test 0.078GB |
Maintenant que nous connaissons toutes les bases de données accessibles, changeons de base, avec la commande use <nom-de-la-base>
:
1 2 | > use local switched to db local |
Mais comment créer une base de données ?
Pour créer une base de données, par exemple la base tutoriel, il suffit de la sélectionner (même si elle n'existe pas) avec use
, puis d'y insérer des données. Lors de l'insertion, la base sera créée. Voyons donc comment insérer des données, mais auparavant, sélectionnez la base de données tutoriel :
1 2 | > use tutoriel switched to db tutoriel |
Insérer des documents
Nous sommes connectés à la base tutoriel, donc toutes les opérations que nous allons réaliser ici porteront sur cette base de données-là. Gardez cela en tête !
Pour insérer un document, rien de plus simple :
db.personnages.insert( { name: "Gordon Freeman", game: "Half-Life" } );
Cette simple commande insérera le document { name: "Gordon Freeman", game: "Half-Life" }
au sein de la collection personnages. Cette collection n'existe pas, mais sera créée automatiquement.
Faire des recherches
Pour afficher tous les documents d'une collection, il suffit de faire :
1 | > db.personnages.find()
|
Cette commande affichera tous les documents de la collection personnages.
Des critères peuvent être spécifiés en paramètre de la méthode find()
. Ces paramètres prennent la forme d'un objet, dont les propriétés à analyser correspondent à celles des documents :
1 | > db.personnages.find( { name : "Gordon Freeman" } ) |
Cette commande trouvera donc le document dont la clef name
vaut Gordon Freeman.
Se connecter depuis Node.js
Installer le module
Afin de pouvoir se connecter à une base de données MongoDB depuis Node.js, il convient d'installer le module NPM mongodb :
1 | npm install mongodb |
Connexion à la base de données
La première étape est évidemment de requérir le module mongodb :
1 | var MongoClient = require("mongodb").MongoClient; |
MongoClient est similaire au MongoClient que l'on peut trouver pour d'autres langages comme PHP, Python, Ruby…
Une fois que c'est fait, une connexion peut être amorcée grâce à la méthode connect()
:
1 2 3 4 5 | MongoClient.connect("mongodb://localhost/tutoriel", function(error, db) { if (error) return funcCallback(error); console.log("Connecté à la base de données 'tutoriel'"); }); |
connect()
reçoit deux paramètres : une URI définissant l'adresse du serveur MongoDB ainsi que la base de données à utiliser (il s’agit ici de la base tutoriel), et une fonction de callback (j'utilise ici une fonction anonyme).
La fonction de callback recevra deux paramètres : error
et db
. Si error
est défini, c'est qu'il s'est passé quelque chose empêchant la connexion. db
est un objet qui représente la base de données, et qui va nous permettre de communiquer avec cette dernière.
Opérations depuis Node.js
Maintenant que nous avons établi une connexion avec MongoDB depuis Node.js, voyons comment faire quelques manipulations.
L'ObjectID
Lorsqu'un document est ajouté à une collection, un identifiant lui est automatiquement attribué. Cet identifiant est stocké dans la propriété _id
et contient un objet de type ObjectID
. Ainsi, si on souhaite insérer ce document :
1 2 3 4 | { name: "Adrian Shephard", game: "Half-Life: Opposing Force" } |
MongoDB lui définit alors un identifiant :
1 2 3 4 5 | { _id: ObjectID("53dfe7bbfd06f94c156ee96e"), name: "Adrian Shephard", game: "Half-Life: Opposing Force" } |
C'est important pour la suite, surtout quand il va s'agir de récupérer le document correspondant à un ID donné !
Récupérer tous les documents d'une collection
Après s'être connecté, il suffit d'utiliser l'objet db
pour effectuer une requête. Ici, on va faire une requête sur la collection personnages
afin de lister tous les documents qui s'y trouvent. Pour ce faire, on utiliser la méthode find()
et, petit bonus, on demande de recevoir les résultats sous la forme d'un tableau, via la méthode toArray()
. Grâce à ça, il sera aisé de parcourir les résultats :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | MongoClient.connect("mongodb://localhost/tutoriel", function(error, db) { if (error) throw error; db.collection("personnages").find().toArray(function (error, results) { if (error) throw error; results.forEach(function(i, obj) { console.log( "ID : " + obj._id.toString() + "\n" // 53dfe7bbfd06f94c156ee96e "Nom : " + obj.name + "\n" // Adrian Shephard "Jeu : " + obj.game // Half-Life: Opposing Force ); }); }); }); |
Pour parcourir les résultats, une boucle for
ou forEach()
et le tour est joué.
Remarquez que pour récupérer l’identifiant, on accède directement à la propriété _id
. Mais il faut penser à appliquer la méthode toString()
, puisqu’il s'agit d'un objet ObjectID
.
Récupérer le document correspondant à un identifiant
La liste complète des documents ayant été récupérée, on peut imaginer que votre application permettra ensuite d'afficher les informations relatives à un des documents. Pour ce faire, il faut le récupérer, en utilisant son identifiant.
On peut utiliser la méthode find()
, mais comme un seul résultat sera retourné, autant privilégier findOne()
qui ne retourne que le premier résultat.
L'identifiant sera reçu sous la forme d'une chaîne de caractères (imaginons qu'il a été transmis par GET ou POST). Pour l'utiliser au sein de la requête, il va falloir le transformer en une instance d'ObjectID
. Il faut donc commencer par requérir cet objet ; j'ai choisi de le faire en utilisant :
1 | var MongoObjectID = require("mongodb").ObjectID; |
Souvenez-vous, pour définir une requête, il suffit de définir un objet dont les propriétés correspondent à ce que l'on souhaite trouver. Donc, si l'on souhaite rechercher le document dont l'identifiant est 53dfe7bbfd06f94c156ee96e, on créera :
1 | { _id: new MongoObjectID("53dfe7bbfd06f94c156ee96e") } |
Cet objet sera passé en paramètre de la méthode findOne()
(ou find()
). Voici le script complet :
1 2 3 4 5 6 7 8 9 10 11 12 13 | var MongoObjectID = require("mongodb").ObjectID; // Il nous faut ObjectID var idToFind = "53dfe7bbfd06f94c156ee96e"; // Identifiant, sous forme de texte var objToFind = { _id: new MongoObjectID(idToFind) }; // Objet qui va nous servir pour effectuer la recherche db.collection("personnages").findOne(objToFind, function(error, result) { if (error) throw error; console.log( "ID : " + result._id.toString() + "\n" // 53dfe7bbfd06f94c156ee96e "Nom : " + result.name + "\n" // Adrian Shephard "Jeu : " + result.game // Half-Life: Opposing Force ); }); |
Insérer un document
Insérer un nouveau document se fait avec la méthode insert()
et requiert un argument : le document à insérer ou un tableau contenant plusieurs documents à insérer. Il est donc possible d'insérer plusieurs documents en une fois. Pratique !
1 2 3 4 5 6 7 | var objNew = { name: "GLaDOS", game: "Portal" }; db.collection("personnages").insert(objNew, null, function (error, results) { if (error) throw error; console.log("Le document a bien été inséré"); }); |
La méthode insert()
admet 3 paramètres :
- Le document ou le tableau de documents à insérer ;
- Un objet contenant les options (optionnel) : voyez ici pour les options writeConcern. Un exemple d'option serait
{ w: 0 }
; - La callback, optionnelle
Insérer et/ou éditer
La méthode save()
fonctionne comme insert()
, à la différence que si _id
est spécifié et qu'un document contenant cet ID existe dans la base, elle va le mettre à jour. Si la méthode ne trouve pas de document correspondant ou si _id
n'est pas défini, le document est inséré, comme si insert()
était utilisée.
1 | db.collection("personnages").save(objNew, { w: 1 }); // Ce document sera inséré |
Éditer un document
Comme vu précédemment, il suffit d'utiliser save()
, mais update()
peut être préférée dans certains cas. En particulier car update()
autorise divers opérateurs, en particulier $set
, qui permet de définir si le document trouvé est remplacé par le nouveau document, ou bien si les données du document trouvé sont mises à jour avec les données correspondantes du nouveau document. Et ça change tout !
1 2 3 4 5 6 7 8 9 10 11 | // Exemple 1 : remplacement db.collection("personnages").update( { name: "GladOS"}, { name: "GladOS", game: "Portal 2"} ); // Exemple 2 : mise à jour, via $set db.collection("personnages").update( { name: "GladOS"}, { $set: { game: "Portal 2" } } ); |
- Dans le premier exemple, le document trouvé est complètement remplacé par le nouveau document. C'est donc un remplacement !
- Mais dans le second exemple, grâce à l'opérateur $set, seule la propriété
game
est mise à jour.
Supprimer un document
Dernier point à voir, remove()
qui permet de supprimer un document, ou tous les documents d'une collection si aucun argument n'est transmis. L'utilisation de remove()
est aussi simple que les autres, le premier argument étant le document à supprimer.
Si le document n'est pas spécifié, TOUS les documents présents dans la collection seront supprimés ! Soyez vigilants !
1 2 3 4 5 6 7 | var MongoObjectID = require("mongodb").ObjectID; // Il nous faut ObjectID var idToFind = "53dfe7bbfd06f94c156ee96e"; // Identifiant, sous forme de texte var objToFind = { _id: new MongoObjectID(idToFind) }; // Objet qui va nous servir pour effectuer la recherche db.collection("personnages").remove(objToFind, null, function(error, result) { if (error) throw error; }); |
Requêtes avancées
Nous n'avons vu que des requêtes simples, comme récupérer un document comportant un identifiant donné. Il est évidemment possible de réaliser des requêtes plus détaillées. On peut même y inclure des expressions régulières !
Plusieurs critères (AND)
Pour définir plusieurs critères, il suffit juste d'utiliser plusieurs propriétés au sein de l'objet de recherche :
1 | { game: "Half-Life", gender: "M" } |
Soit l'un soit l'autre (OR)
La syntaxe est ici quelque peu plus complexe. MongoDB met à disposition diverses propriétés réservées qui sont utilisées en tant que paramètres. Pour faire un OR, on utilise la propriété $or
. Cette dernière contient un tableau composé des critères de recherche. L'exemple montre un objet qui va récupérer tous les personnages du jeu Portal, ou, tous les hommes :
1 2 3 4 5 6 | { $or: [ { game: "Portal" }, { gender: "M" } ] } |
Conditions < <= > >= !=
Des conditions peuvent être ajoutées. Voici les propriétés réservées correspondantes :
Condition | Propriété |
---|---|
< | $lt |
<= | $lte |
> | $gt |
>= | $gte |
!= | $ne |
Trouve tous les personnages plus jeunes que 40 ans.
1 | { old: { $lt: 40 } } |
Trouve tous les personnages qui ont entre 18 et 40 ans exclus
1 | { old: { $gt: 18, $lt: 40 } |
Expressions régulières
L'utilisation de regex est autorisée, de façon très simple. La requête suivante trouve tous les personnages de jeux dont le nom commence par Half-Life. Les personnages du jeu Half-Life: Opposing Force seront donc trouvés aussi :
1 | { game: /^Half-Life/ } |
Voilà, j'espère que ce petit cours vous aura permis d'y voir plus clair !
Bien évidemment, ce tutoriel n'est pas complet et n'a pas pour vocation à remplacer la documentation.
Pour aller plus loin, n'hésitez pas à parcourir les documentations officielles :