Archives de catégorie : MongoDB

Interroger un cluster MongoDB Atlas avec C++

J’ai décidé de me replonger dans le langage C++ le temps d’un exercice destiné aux étudiants suivant ma formation sur MongoDB en école d’ingénieur. Tout d’abord, nous avons effectué toutes les requêtes de l’exercice sur les salles (PDF) à partir de l’interpréteur JavaScript du shell MongoDB, en direction de nos clusters créés sur Atlas. Ensuite, nous avons traduit ces requêtes dans le langage de programmation préféré de chaque étudiant. N’ayant pas pratiqué le C++ depuis l’époque où la monnaie nationale était encore le franc, j’ai quant à moi pris le pari de revenir à mes premiers amours et d’utiliser le langage de Stroustrup comme base…ce fut un plaisir !

Tout d’abord, il m’a fallu installer le pilote (driver) dédié à ce langage : mongocxx.

Installation et compilation du pilote mongocxx

Rien de plus simple pour y parvenir :

    cd mongo-cxx-driver
    mkdir build && cd build
    cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local ..
    make -j$(nproc)
    sudo make install

Le programme principal

Une fois le driver installé, j’ai pu commencer à coder le petit programme pour accéder à mon cluster Atlas et y exécuter toutes les requêtes préparées dans mon header exercices.hpp . Le voici :

#include <iostream>
#include <bsoncxx/json.hpp>
#include <mongocxx/client.hpp>
#include <mongocxx/instance.hpp>
#include <mongocxx/uri.hpp>
#include <mongocxx/exception/exception.hpp>
#include "exercices.hpp" 

using bsoncxx::to_json;

int main() {

    const std::string USERNAME      = "mongosensei";
    const std::string PASSWORD      = "xxxxx";
    const std::string DATABASE      = "xxxxx";
    const std::string COLLECTION    = "salles";
    const std::string ATLAS_CLUSTER = "xxx.mongodb.net";

    mongocxx::instance inst{};

    try {
        std::string uriString = "mongodb+srv://" + USERNAME + ":" + PASSWORD + "@" + ATLAS_CLUSTER + "/" + DATABASE;
        mongocxx::uri uri(uriString);
        mongocxx::client conn(uri);

        std::vector<bsoncxx::document::value> exercices = getExercices();

        for (const auto& exercice : exercices) {
            auto numExercice = exercice["num"].get_int32();
            auto filterDocument = exercice["filtre"].get_document();
            auto projectionDocument = exercice["options"]["projection"].get_document();
            
            auto options = mongocxx::options::find{};
            options.projection(projectionDocument.view());

            auto collection = conn[DATABASE][COLLECTION];
            auto cursor = collection.find(filterDocument.view(), options);

            int nbDocuments = 0;

            for (auto&& doc : cursor) {
                nbDocuments++;
                std::cout << bsoncxx::to_json(doc) << std::endl;
            }

            std::cout << "Nb de documents pour l'exercice " << numExercice << " : " << nbDocuments << std::endl;
        }

    } catch (const mongocxx::exception& e) {
        std::cerr << "Erreur de connexion : " << e.what() << std::endl;
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}

Ce programme fait appel à un fichier qui contient la solution à chacun des exercices contenus dans le PDF dont le lien a été donné plus haut. Voici ce à quoi il ressemble :

#ifndef EXERCICES_HPP
#define EXERCICES_HPP

#include <vector>
#include <bsoncxx/builder/basic/document.hpp>

time_t parseDate(const std::string& dateStr) {
    std::tm tm = {};
    strptime(dateStr.c_str(), "%Y-%m-%d", &tm);
    return mktime(&tm);
}

std::vector<bsoncxx::document::value> getExercices() {
    using bsoncxx::builder::basic::kvp;
    using bsoncxx::builder::basic::make_document;
    using bsoncxx::builder::basic::make_array;

    std::string date_str = "2021-11-15";
    auto timestamp = std::chrono::milliseconds{parseDate(date_str) * 1000};

    std::vector<bsoncxx::document::value> exercices = {
        make_document(
            kvp("num", 1),
            kvp("filtre", make_document(
                kvp("smac", true)
            )),
            kvp("options", make_document(
                kvp("projection", make_document(
                    kvp("_id", true),
                    kvp("nom", true)
                ))
            ))
        ),
        make_document(
            kvp("num", 2),
            kvp("filtre", make_document(
                kvp("capacite", make_document(
                    kvp("$gt", bsoncxx::types::b_int64{1000})
                ))
            )),
            kvp("options", make_document(
                kvp("projection", make_document(
                    kvp("_id", false),
                    kvp("nom", true)
                ))
            ))
        ),
        make_document(
            kvp("num", 3),
            kvp("filtre", make_document(
                kvp("adresse.numero", make_document(
                    kvp("$exists", false)
                ))
            )),
            kvp("options", make_document(
                kvp("projection", make_document(
                    kvp("_id", true)
                ))
            ))
        ),
        make_document(
            kvp("num", 4),
            kvp("filtre", make_document(
                kvp("avis", make_document(
                    kvp("$size", bsoncxx::types::b_int64{1})
                ))
            )),
            kvp("options", make_document(
                kvp("projection", make_document(
                    kvp("nom", true)
                ))
            ))
        ),
        make_document(
            kvp("num", 5),
            kvp("filtre", make_document(
                kvp("styles", "blues")
            )),
            kvp("options", make_document(
                kvp("projection", make_document(
                    kvp("_id", false),
                    kvp("styles", true)
                ))
            ))
        ),
        make_document(
            kvp("num", 6),
            kvp("filtre", make_document(
                kvp("styles.0", "blues")
            )),
            kvp("options", make_document(
                kvp("projection", make_document(
                    kvp("_id", false),
                    kvp("styles", true)
                ))
            ))
        ),
        make_document(
            kvp("num", 7),
            kvp("filtre", make_document(
                kvp("$and", make_array(
                    make_document(
                        kvp("adresse.codePostal", bsoncxx::types::b_regex{"^84"}),
                        kvp("capacite", make_document(
                            kvp("$lt", 500)
                        ))
                    )
                ))
            )),
            kvp("options", make_document(
                kvp("projection", make_document(
                    kvp("_id", false),
                    kvp("adresse.ville", true)
                ))
            ))
        ),
        make_document(
            kvp("num", 8),
            kvp("filtre", make_document(
                kvp("$or", make_array(
                    make_document(
                        kvp("_id", make_document(kvp("$mod", make_array(2, 0))))
                    ),
                    make_document(
                        kvp("avis", make_document(
                            kvp("$exists", false)
                        ))
                    )
                ))
            )),
            kvp("options", make_document(
                kvp("projection", make_document(
                    kvp("_id", true)
                ))
            ))
        ),
        make_document(
            kvp("num", 9),
            kvp("filtre", make_document(
                kvp("avis", make_document(
                    kvp("$elemMatch", make_document(
                        kvp("note", make_document(
                            kvp("$gte", 8),
                            kvp("$lte", 10)
                        ))
                    ))
                ))
            )),
            kvp("options", make_document(
                kvp("projection", make_document(
                    kvp("_id", false),
                    kvp("nom", true)
                ))
            ))
        ),
        make_document(
            kvp("num", 10),
            kvp("filtre", make_document(
                kvp("avis.date", make_document(
                        kvp("$gt", bsoncxx::types::b_date{timestamp}
                    ))
                ))
            ),
            kvp("options", make_document(
                kvp("projection", make_document(
                    kvp("_id", false),
                    kvp("nom", true)
                ))
            ))
        ),
    };

    return exercices;
}

#endif // EXERCICES_HPP

La fonction parseDate est là pour nous faciliter la transformation des dates pour leur utilisation avec mes méthodes de la lib BSON, elle ne sert ici qu’une fois.

Pour construire les documents, j’ai utilisé la version basic du builder BSON, la version stream étant beaucoup moins lisible pour moi.

Compilation et exécution du code

J’ai essayé plusieurs solutions :

  • la compilation avec c++
  • la compilation avec g++
  • l’installation de l’outil pkg-config, MongoDB proposant un fichier .pc

D’abord, la version avec pkg-config :

sudo apt install pkg-config
export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
c++ -o mongo_exercices --std=c++11 mongo_exercices.cpp $(pkg-config --cflags --libs libmongocxx)
./mongo_exercices

Ensuite la version avec g++, qui marche aussi avec c++ :

g++ -o mongo_exercices mongo_exercices.cpp -lmongocxx -lbsoncxx -std=c++11 \
    -I/usr/local/include/mongocxx/v_noabi \
    -I/usr/local/include/bsoncxx/v_noabi \
    -I/usr/local/include/bsoncxx/v_noabi/bsoncxx/third_party/mnmlstc \
    -L/usr/local/lib -Wl,-rpath=/usr/local/lib \
    && ./mongo_exercices

Voici la sortie produite par notre exécutable :

{ "_id" : 1, "nom" : "AJMI Jazz Club" }
{ "_id" : 2, "nom" : "Paloma" }
Nb de documents pour l'exercice 1 : 2
{ "nom" : "Paloma" }
Nb de documents pour l'exercice 2 : 1
{ "_id" : 3 }
{ "_id" : { "$oid" : "65e08cc6fa1cb047c0a5e5ea" } }
Nb de documents pour l'exercice 3 : 2
{ "_id" : 2, "nom" : "Paloma" }
Nb de documents pour l'exercice 4 : 1
{ "styles" : [ "jazz", "soul", "funk", "blues" ] }
{ "styles" : [ "blues", "rock" ] }
Nb de documents pour l'exercice 5 : 2
{ "styles" : [ "blues", "rock" ] }
Nb de documents pour l'exercice 6 : 1
{ "adresse" : { "ville" : "Avignon" } }
{ "adresse" : { "ville" : "Le Thor" } }
Nb de documents pour l'exercice 7 : 2
{ "_id" : 2 }
{ "_id" : 3 }
{ "_id" : { "$oid" : "65e08cc6fa1cb047c0a5e5ea" } }
Nb de documents pour l'exercice 8 : 3
{ "nom" : "AJMI Jazz Club" }
{ "nom" : "Paloma" }
Nb de documents pour l'exercice 9 : 2
{ "nom" : "AJMI Jazz Club" }
{ "nom" : "Paloma" }
Nb de documents pour l'exercice 10 : 2

Pour pousser plus loin votre expérience avec C++, suivez ce lien !

MongoDB – lookup et l’agrégation

Cette étape de recherche (lookup) permet d’effectuer l’équivalent d’une jointure externe dans un système de gestion de bases de données relationnelles (SGBDR) et ne peut opérer que sur des collections non shardées situées dans la même base de données.
Look up

Syntaxe de lookup

{
    $lookup:
    {
        from: < collection à joindre >,
        localField: < champ dans les documents de la collection de départ >,
        foreignField: < champ dans les documents de la collection à joindre>,
        as: < nom du tableau qui sera ajouté aux documents du jeu de résultat >
    }
}

Dans chacun des documents reçus en entrée, l’étape $lookup va rajouter un tableau contenant des données en provenance de la collection sur laquelle la jointure a été requise avant de passer ces documents à la prochaine étape du pipeline le cas échéant. Le terme de jointure n’est pas toujours très approprié notamment parce qu’il est possible d’effectuer des requêtes décorrélées, c’est à dire sans clause d’égalité.

Afin de voir $lookup en situation, nous allons utiliser trois collections qui stockeront des élèves, leurs devoirs individuels et les projets sur lesquels ils travaillent à plusieurs.

Elèves, au travail !

Les travaux individuels

Commençons par les travaux individuels de nos élèves, voici les collections impliquées, aucun index n’a été mis pour garder les choses les plus simples possibles mais évidemment qu’il faudra y songer !

db.eleves.insertMany([
    {"nom": "Sébastien Ferrandez", "code": "NAT123"},
    {"nom": "Evelyne Durand", "code": "NAT125"},
    {"nom": "Christian Ton", "code": "NAT120"},
    {"nom": "Claire Annela", "code": "NAT127"}
])

db.devoirs.insertMany([
    {"code": "NAT123", "matiere": "SVT", "note": 12},
    {"code": "NAT123", "matiere": "Maths", "note": 10},
    {"code": "NAT125", "matiere": "Maths", "note": 11.75},
    {"code": "NAT120", "matiere": "Français", "note": 18},
    {"code": "NAT127", "matiere": "Latin", "note": 19}
])

Nous allons partir d’eleves pour aller vers devoirs; dans les deux cas notre nom de champ de « jointure » est code:

db.eleves.aggregate([
   {
     $lookup: {
         "from": "devoirs",
         "localField": "code",
         "foreignField": "code",
         "as": "detail_notes"
     }
   }
])

Voici le résultat produit par ce pipeline, il est assez difficilement lisible mais nous avons l’idée générale : les notes sont « raccrochées » à l’élève !

{
	"_id": ObjectId("5d6e6115d9a18feb0291b605"),
	"nom": "Sébastien Ferrandez",
	"code": "NAT123",
	"detail_notes": [{
		"_id": ObjectId("5d6e6174d9a18feb0291b60e"),
		"code": "NAT123",
		"matiere": "SVT",
		"note": 12
	}, {
		"_id": ObjectId("5d6e6174d9a18feb0291b60f"),
		"code": "NAT123",
		"matiere": "Maths",
		"note": 10
	}]
} {
	"_id": ObjectId("5d6e6115d9a18feb0291b606"),
	"nom": "Evelyne Durand",
	"code": "NAT125",
	"detail_notes": [{
		"_id": ObjectId("5d6e6174d9a18feb0291b610"),
		"code": "NAT125",
		"matiere": "Maths",
		"note": 11.75
	}]
} {
	"_id": ObjectId("5d6e6115d9a18feb0291b607"),
	"nom": "Christian Ton",
	"code": "NAT120",
	"detail_notes": [{
		"_id": ObjectId("5d6e6174d9a18feb0291b611"),
		"code": "NAT120",
		"matiere": "Français",
		"note": 18
	}]
} {
	"_id": ObjectId("5d6e6115d9a18feb0291b608"),
	"nom": "Claire Annela",
	"code": "NAT127",
	"detail_notes": [{
		"_id": ObjectId("5d6e6174d9a18feb0291b612"),
		"code": "NAT127",
		"matiere": "Latin",
		"note": 19
	}]
}

Ajoutons une étape project pour rendre cet affichage plus « digeste »…Nous éliminons les id et les champs redondants:

db.eleves.aggregate([
   {
     $lookup: {
         "from": "devoirs",
         "localField": "code",
         "foreignField": "code",
         "as": "detail_notes"
     }
   },
   {
     $project: {
         "_id": 0,
         "detail_notes._id": 0,
         "detail_notes.code": 0
     }
   }
])

Voilà qui est nettement mieux:

{
	"nom": "Sébastien Ferrandez",
	"code": "NAT123",
	"detail_notes": [{
		"matiere": "SVT",
		"note": 12
	}, {
		"matiere": "Maths",
		"note": 10
	}]
} {
	"nom": "Evelyne Durand",
	"code": "NAT125",
	"detail_notes": [{
		"matiere": "Maths",
		"note": 11.75
	}]
} {
	"nom": "Christian Ton",
	"code": "NAT120",
	"detail_notes": [{
		"matiere": "Français",
		"note": 18
	}]
} {
	"nom": "Claire Annela",
	"code": "NAT127",
	"detail_notes": [{
		"matiere": "Latin",
		"note": 19
	}]
}

Les projets

Voici notre collection projets, elle contient les codes des élèves impliqués ainsi que la note obtenue:

db.projets.insertMany([
    {"codes": ["NAT123", "NAT125"], "matiere": "Dessin", "note": 15}
])

Le champ codes est un tableau ici, pour que chaque élève puisse récupérer sa note, il va falloir éclater ce tableau à l’aide d’unwind avant d’utiliser lookup dessus, nous allons partir de projets cette fois:

db.projets.aggregate([
   {
      $unwind: "$codes"
   },
   {
     $lookup: {
         "from": "eleves",
         "localField": "codes",
         "foreignField": "code",
         "as": "eleve"
     }
   },
   {
     $project: {
         "_id": 0,
         "codes": 0,
         "eleve._id": 0
     }
   }
])

Voilà le jeu de résultat lié:

{
	"matiere": "Dessin",
	"note": 15,
	"eleve": [{
		"nom": "Sébastien Ferrandez",
		"code": "NAT123"
	}]
} {
	"matiere": "Dessin",
	"note": 15,
	"eleve": [{
		"nom": "Evelyne Durand",
		"code": "NAT125"
	}]
}

Nous allons rajouter une étape addFields pour que le tableau contenant un seul document (eleve) devienne un document tout simple, voici le pipeline final:

db.projets.aggregate([
   {
      $unwind: "$codes"
   },
   {
     $lookup: {
         "from": "eleves",
         "localField": "codes",
         "foreignField": "code",
         "as": "eleve"
     }
   },
   {
     $addFields: {
       "eleve": { $arrayElemAt: ["$eleve", 0] }
     }
   },
   {
     $project: {
         "_id": 0,
         "codes": 0,
         "eleve._id": 0
     }
   }
])

Et voilà l’travail !

{
	"matiere": "Dessin",
	"note": 15,
	"eleve": {
		"nom": "Sébastien Ferrandez",
		"code": "NAT123"
	}
} {
	"matiere": "Dessin",
	"note": 15,
	"eleve": {
		"nom": "Evelyne Durand",
		"code": "NAT125"
	}
}

Introduction à MongoDB

Dans le cadre des cours de bases de données que j’enseigne au CNAM, j’ai décidé pour ma dernière séance de l’année 2013-2013 de faire une présentation de MongoDB à mes élèves.

Sans aller trop en détail dans la technique (on n’y parle pas de sharding, de map-reduce ou de GridFs), l’idée était ici de leur présenter cet aspect des bases de données qui trouve de plus en plus d’écho dans la communauté des développeurs d’applications en ligne.

snapshot2

Nous avons passé l’année à parler de schéma conceptuel, d’E/R, de DDL, de DML, il me fallait aussi présenter une vision alternative des bases de données car connaître l’état de l’art technique est, en ce qui me concerne, une qualité requise chez tout concepteur qui prétend l’être.

Vous pouvez télécharger cette présentation au format PDF.

MongoDB : un exemple de map-reduce

Le concept de map-reduce (on lit parfois les termes de paradigme de programmation ou de design pattern associés à map-reduce) n’a rien de nouveau : il s’inspire grandement des principes de base de la programmation fonctionnelle (i.e, par usage de fonctions mathématiques).

Comme son nom l’indique, map-reduce se compose de deux fonctions :

  • map, qui passe à reduce une clé et des données à traiter
  • reduce qui, pour chaque clé donnée par map, va opérer une réduction sur les données en provenance de map selon une logique que vous allez devoir écrire.

Map-reduce a connu un renouveau avec l’apparition des data stores de très grande capacité (on parle ici en pétaoctets) sur le web et doit en grande partie son retour sous les feux de la rampe à Google qui s’en est servi (et s’en sert toujours) pour distribuer/paralléliser les traitements, sur des clusters de machines. Le principe est simple: découper un gros problème en plusieurs petits, résolus par distribution à des nœuds fils (réducteurs) qui ensuite remontent le résultat de ces opérations aux nœuds de niveau supérieur, et ce récursivement jusqu’à remonter au la racine de l’arbre, qui a initié le calcul et en récupère donc le résultat final.

Ici nous n’aurons par cette notion de master nodes / worker nodes car nous travaillons sur une seule instance de MongoDB, située sur un seul serveur (ma machine) et non sur une architecture basée sur des shards ou des replica sets.

Image provenant de http://www.pal-blog.de/

Image provenant de PAL-Blog [http://www.pal-blog.de]


Map traverse tous les documents de votre collection MongoDB et va pousser des infos à reduce selon une clé définie. Passons à la pratique sans plus tarder…

Notre collection de test

Nous allons constituer une collection dénommée commandes et qui va contenir les commandes passées sur notre site web. Ces commandes seront évidemment des documents dont la structure est la suivante :

  • userid : l’identifiant de l’utilisateur à l’origine de la commande
  • date : la date de passage (ou de validation, pourquoi pas) de la commande
  • codepost : le code postal de la ville de résidence de l’utilisateur
  • articles : la liste des articles commandés (il y en a autant que souhaité)
  • totalttc : le prix TTC de la totalité de la commande
  • totalht : le prix HT de la totalité de la commande
  • tva : le montant de TVA applicable à la commande
sebastien.ferrandez@sebastien:~$ mongo
 MongoDB shell version: 2.0.6
 connecting to: test
>db.createCollection('commandes');
 { "ok" : 1 }
>show collections;
 commandes
 system.indexes

>db.commandes.insert({userid: 54845, date: new Date("Apr 28, 2013"), codepost:13100, articles:[{id:1,nom:'livre',prix:29.90}, {id:9, nom:'eponge', prix:2.90}], totalttc:32.80, tva:5.38, totalht:27.42});

>db.commandes.insert({userid: 54846, date: new Date("Apr 29, 2013"),codepost:13290, articles:[{id:45,nom:'robinet',prix:69.90}, {id:9, nom:'laitx6', prix:9.90}], totalttc:79.80, tva:13.08, totalht:66.72});

>db.commandes.insert({userid: 54847, date: new Date("Apr 30, 2013"),codepost:13008, articles:[{id:76,nom:'clavier',prix:49.90}, {id:2, nom:'fromage', prix:1.50}], totalttc:51.40, tva:8.42, totalht:42.98});

>db.commandes.insert({userid: 54848, date: new Date("Apr 28, 2013"),codepost:13600, articles:[{id:2987,nom:'presse',prix:2}], totalttc:2, tva:0.33, totalht:1.67});

>db.commandes.insert({userid: 54848, date: new Date("Apr 29, 2013"),codepost:13600, articles:[{id:2988,nom:'presse',prix:5.90}], totalttc:5.90, tva:0.97, totalht:4.93});

>db.commandes.insert({userid: 54848, date: new Date("Apr 30, 2013"),codepost:13600, articles:[{id:3989,nom:'presse',prix:1.20}], totalttc:1.20, tva:0.20, totalht:1});

>db.commandes.insert({userid: 54847, date: new Date("Apr 25, 2013"),codepost:13008, articles:[{id:2987,nom:'presse',prix:2}], totalttc:2, tva:0.33, totalht:1.67});

Structure du JSON

Voici la structure de l’un de nos documents :

{
    "_id": ObjectId("517fb463b53bb7169584f3c7"),
    "userid": 54847,
    "date": ISODate("2013-04-29T22:00:00Z"),
    "codepost": 13008,
    "articles": [
        {
            "id": 76,
            "nom": "clavier",
            "prix": 49.9
        },
        {
            "id": 2,
            "nom": "fromage",
            "prix": 1.5
        }
    ],
    "totalttc": 51.4,
    "tva": 8.42,
    "totalht": 42.98
}

Le JavaScript de notre map-reduce

Maintenant, nous allons écrire le map-reduce qui va opérer sur ces données; notre but est d’afficher, par code postal, le chiffre d’affaire généré par notre site en ligne. Créons donc un fichier mapred.js qui contiendra notre MR :

use commandes;

map = function() {
	emit(this['codepost'], {totalttc: this['totalttc']});
	};

reduce = function(cle, valeur) {
			var s = {somme : 0};
			valeur.forEach(function(article) {
				s.somme += article.totalttc;
			});
			return s;
		};

db.commandes.mapReduce(map, reduce, {out:'total_cmdes_par_ville'});

db.total_cmdes_par_ville.find();

And…action !

Nous injectons notre fichier JS dans Mongo

sebastien.ferrandez@sebastien:~$ mongo < /tmp/mapred.js

Et voici le résultat produit :

MongoDB shell version: 2.0.6
connecting to: test
function () {
    emit(this.codepost, {totalttc:this.totalttc});
}
function (cle, valeur) {
    var s = {somme:0};
    valeur.forEach(function (article) {s.somme += article.totalttc;});
    return s;
}
{
        "result" : "total_cmdes_par_ville",
        "timeMillis" : 33,
        "counts" : {
                "input" : 7,
                "emit" : 7,
                "reduce" : 2,
                "output" : 4
        },
        "ok" : 1,
}
{ "_id" : 13008, "value" : { "somme" : 53.4 } }
{ "_id" : 13100, "value" : { "totalttc" : 32.8 } }
{ "_id" : 13290, "value" : { "totalttc" : 79.8 } }
{ "_id" : 13600, "value" : { "somme" : 9.1 } }
bye

La fonction map va émettre (emit) des paires clé/valeur sur lesquelles reduce va travailler. Pour faire simple, reduce va faire la somme de tous les montants TTC des commandes pour un code postal donné. Une collection est créée pour abriter le résultat de l’exécution de map-reduce : nous avons décider de la nommer total_cmdes_par_ville. Elle est persistante, elle ne sera pas détruite une fois que vous vous déconnectez du shell MongoDB.

MongoDB : les bases pour bien débuter (3/3)

Revenons sur la collection MongoDB qui nous sert d’exemple depuis le début et effectuons quelques opérations de mise à jour élémentaires. Voici donc pour rappel à quoi ressemble notre collection :

sebastien.ferrandez@sebastien:~$ mongo
MongoDB shell version: 2.0.6
connecting to: test
> show dbs;
admin   0.203125GB
gens    0.203125GB
local   (empty)
> use gens;
switched to db gens
> db.gens.find();
{ "_id" : ObjectId("517a850cb1d6ce34f91af2d1"), "nom" : "ferrandez" }
{ "_id" : ObjectId("517a851eb1d6ce34f91af2d2"), "nom" : "ferrandez", "prenom" : "léo" }
{ "_id" : ObjectId("517a856bb1d6ce34f91af2d4"), "nom" : "ferrandez", "prenom" : "maïa", "age" : 6 }

mongo-db-logo

Opérations de mise à jour

Avec inc

Supposons qu’il nous faille mettre à jour l’âge d’une personne : aujourd’hui c’est l’anniversaire de Maïa, elle a 7 ans ! Voici plusieurs façons de faire : tout d’abord nous incrémentons la clé age de nos documents pour lesquels age est supérieur à 5.

db.gens.update({ age:{$gt: 5}}, {$inc: {age: 1}});

Avec set

Nous ciblons uniquement le prénom

db.gens.update({ prenom: "maïa"}, {$set: {age: 7}});

Nous faisons un mélange des deux précédentes requêtes :

db.gens.update({ age:{$gt: 5}}, {$set: {age: 7}});

Opérations de suppression

Suppression par ObjectId

> db.gens.find();
{ "_id" : ObjectId("517a850cb1d6ce34f91af2d1"), "nom" : "ferrandez" }
{ "_id" : ObjectId("517a851eb1d6ce34f91af2d2"), "nom" : "ferrandez", "prenom" : "léo" }
{ "_id" : ObjectId("517e33cf4937f8d068f9e9aa"), "nom" : "ferrandez", "prenom" : "maïa", "age" : 7 }
> db.gens.remove( {"_id": ObjectId("517e33cf4937f8d068f9e9aa")});
> db.gens.find();
{ "_id" : ObjectId("517a850cb1d6ce34f91af2d1"), "nom" : "ferrandez" }
{ "_id" : ObjectId("517a851eb1d6ce34f91af2d2"), "nom" : "ferrandez", "prenom" : "léo" }

Suppression par champ quelconque

Supprimons tous les gens qui s’appellent « ferrandez » :

> db.gens.find();
{ "_id" : ObjectId("517a850cb1d6ce34f91af2d1"), "nom" : "ferrandez" }
{ "_id" : ObjectId("517a851eb1d6ce34f91af2d2"), "nom" : "ferrandez", "prenom" : "léo" }
> db.gens.remove({nom: 'ferrandez'});
> db.gens.find();

Notez que cette fois-ci je n’ai pas mis de guillemets autour de la clé (nom) et j’ai mis des apostrophes autour du nom pour que vous voyez bien qu’il n’est pas obligatoire de mettre tout ça entre guillemets systématiquement !

Suppression de la première occurrence seulement

En mettant justOne à 1 (le premier paramètre), seul le premier document satisfaisant aux critères sera supprimé :

> db.gens.find();
{ "_id" : ObjectId("517e36c14937f8d068f9e9ab"), "nom" : "ferrandez", "prenom" : "maïa", "age" : 7 }
{ "_id" : ObjectId("517e36e54937f8d068f9e9ac"), "nom" : "ferrandez", "prenom" : "sébastien" }
> db.gens.remove({nom: 'ferrandez'}, 1);
> db.gens.find();
{ "_id" : ObjectId("517e36e54937f8d068f9e9ac"), "nom" : "ferrandez", "prenom" : "sébastien" }

Suppression de l’intégralité des documents d’une collection

> db.gens.remove();

Voilà ! Pour ceux d’entre vous qui ont déjà des connaissances en langage SQL, vous avez les bases pour débuter avec MongoDB. Nous allons bientôt rentrer en détail dans le fonctionnement de MongoDB et en particulier nous attarder sur l’aspect dénormalisation !

MongoDB : les bases pour bien débuter (2/3)

Dans le billet précédent, nous avons vu quelques unes des requêtes DDL nous permettant de manipuler les structures élémentaires de MongoDB, comme les bases de données, les collections et les documents. L’heure est maintenant venue de requêter sur nos données !

Le kit MongoDB, qu'on reçoit à toutes les confs (source http://xenodesystems.blogspot.mx/)

Le kit MongoDB, qu’on reçoit à toutes les confs (source http://xenodesystems.blogspot.mx/)

Notre jeu de données de démarrage

Il est très simple :

> db.gens.insert({"nom":"ferrandez"});
> db.gens.insert({"nom":"ferrandez", prenom: "léo"});
> db.gens.insert({"nom":"ferrandez", prenom: "maïa", age:6});
> db.gens.find();
{ "_id" : ObjectId("517a850cb1d6ce34f91af2d1"), "nom" : "ferrandez" }
{ "_id" : ObjectId("517a851eb1d6ce34f91af2d2"), "nom" : "ferrandez", "prenom" : "léo" }
{ "_id" : ObjectId("517a856bb1d6ce34f91af2d4"), "nom" : "ferrandez", "prenom" : "maïa", "age" : 6 }

MongoDB est schema-less (prononcez « skimaless »), nous pouvons donc très bien insérer des documents qui n’ont pas la même structure, regardez bien nos trois documents, ils ne comportent pas le même nombre de champs !

Poser des index

Vous le savez, pour que vos requêtes aient un temps d’exécution satisfaisant (surtout si elle sont appelées fréquemment), il est absolument nécessaire qu’elles s’appuient sur des index. MongoDB dispose d’index au niveau des collections (l’équivalent des tables en SQL). Le champ _id est indexé par défaut et il est impossible de le supprimer.

Sur un seul champ

Pour poser un index sur le champ nom dans l’ordre ascendant (1, ce sera -1 pour DESC) :

db.gens.ensureIndex({nom:1});

Les index composés

Pour poser un index sur les champs nom ET prenom (ASC tous les deux) :

db.gens.ensureIndex({nom:1, prenom:1});

Comme en SQL, si vos requêtes se basent sur prenom, l’index ne sera pas utilisé !
Si vous requêtez sur le nom ou sur le nom ET le prénom, celui-ci sera sollicité.

Les index uniques

Rien de plus simple :

db.gens.ensureIndex({nom:1}, {unique: true});

Ici en réalité la pose de cet index va échouer car notre champ nom contient les mêmes valeurs :

> db.gens.ensureIndex({nom:1}, {unique: true});
E11000 duplicate key error index: gens.gens.$nom_1  dup key: { : "ferrandez" }

Voir les index d’une collection

db.gens.getIndexes();
[
        {
                "v" : 1,
                "key" : {
                        "_id" : 1
                },
                "ns" : "gens.gens",
                "name" : "_id_"
        },
        {
                "v" : 1,
                "key" : {
                        "nom" : 1
                },
                "ns" : "gens.gens",
                "name" : "nom_1"
        },
        {
                "v" : 1,
                "key" : {
                        "nom" : 1,
                        "prenom" : 1
                },
                "ns" : "gens.gens",
                "name" : "nom_1_prenom_1"
        }
]

Supprimer un index

On va supprimer l’index nommé ‘nom_1_prenom_1’ :

> db.gens.dropIndex('nom_1_prenom_1')
{ "nIndexesWas" : 3, "ok" : 1 }
> db.gens.getIndexes();
[
        {
                "v" : 1,
                "key" : {
                        "_id" : 1
                },
                "ns" : "gens.gens",
                "name" : "_id_"
        },
        {
                "v" : 1,
                "key" : {
                        "nom" : 1
                },
                "ns" : "gens.gens",
                "name" : "nom_1"
        }
]

Nous voyons qu’il a été effectivement supprimé (et nous voyons aussi au passage qu’un index est posé par défaut par MongoDB sur _id).

Les requêtes

Sur une seule valeur

Cherchons toutes les personnes qui ont 6 ans :

> db.gens.find({age:6});
{ "_id" : ObjectId("517a856bb1d6ce34f91af2d4"), "nom" : "ferrandez", "prenom" : "maïa", "age" : 6 }

Cherchons toutes les personnes qui s’appellent « ferrandez » :

> db.gens.find({nom:"ferrandez"});
{ "_id" : ObjectId("517a850cb1d6ce34f91af2d1"), "nom" : "ferrandez" }
{ "_id" : ObjectId("517a851eb1d6ce34f91af2d2"), "nom" : "ferrandez", "prenom" : "léo" }
{ "_id" : ObjectId("517a856bb1d6ce34f91af2d4"), "nom" : "ferrandez", "prenom" : "maïa", "age" : 6 }

Sur deux valeurs

Cherchons toutes les personnes qui ont 6 ans et qui s’appellent « ferrandez » :

> db.gens.find({nom:"ferrandez", age:6});
{ "_id" : ObjectId("517a856bb1d6ce34f91af2d4"), "nom" : "ferrandez", "prenom" : "maïa", "age" : 6 }

Avec des opérateurs de comparaison

Cherchons tous les « ferrandez » qui ont plus de 6 ans (gt = greater than)

> db.gens.find({nom:"ferrandez", age: {$gt: 6}});

Cherchons tous les « ferrandez » qui ont moins de 6 ans (lt = less than)

> db.gens.find({nom:"ferrandez", age: {$lt: 6}});

Ces deux requêtes ne ramènent aucun résultat, ce qui n’est pas le cas des suivantes avec l’égalité…

> db.gens.find({nom:"ferrandez", age: {$lte: 6}});
{ "_id" : ObjectId("517a856bb1d6ce34f91af2d4"), "nom" : "ferrandez", "prenom" : "maïa", "age" : 6 }
> db.gens.find({nom:"ferrandez", age: {$gte: 6}});
{ "_id" : ObjectId("517a856bb1d6ce34f91af2d4"), "nom" : "ferrandez", "prenom" : "maïa", "age" : 6 }

L’équivalent du IN SQL

Les gens dont l’âge est 6, 7 ou 8 :

> db.gens.find({age: {$in: [6, 7, 8]}});
{ "_id" : ObjectId("517a856bb1d6ce34f91af2d4"), "nom" : "ferrandez", "prenom" : "maïa", "age" : 6 }

Les expressions régulières (regexp)

Les gens dont le nom commence par la lettre f :

> db.gens.find({nom:/^f/});
{ "_id" : ObjectId("517a850cb1d6ce34f91af2d1"), "nom" : "ferrandez" }
{ "_id" : ObjectId("517a851eb1d6ce34f91af2d2"), "nom" : "ferrandez", "prenom" : "léo" }
{ "_id" : ObjectId("517a856bb1d6ce34f91af2d4"), "nom" : "ferrandez", "prenom" : "maïa", "age" : 6 }

MongoDB : les bases pour bien débuter (1/3)

Le but de ce billet est d’effectuer une première incursion dans l’univers MongoDB en voyant les commandes de bases dans le shell mongo et en faisant un parallèle avec ce qui existe dans le monde du SQL. Nous allons commencer en manipulant les bases : bases de données, collections et documents. Le but n’est pas de se noyer d’emblée dans l’architecture interne de Mongo (le sharding, les replica sets, etc.) mais d’attaquer d’emblée des choses concrètes !

mongo_logo

L’emblème de MongoDB

Installation de MongoDB sur GNU Linux Debian
Nous allons faire les choses proprement au lieu d’insérer à la sauvage dans le fichier sources.list. Vous devez évidemment être sudoer pour effectuer ceci :

echo 'deb http://downloads-distro.mongodb.org/repo.fr.ian-sysvinit dist 10gen' > /tmp/mongodb.list
sudo cp /tmp/mongodb.list /etc/apt/sources.list.d/
rm /tmp/mongodb.list

sudo apt-get update
sudo apt-get install mongodb-10gen

A l’issue de l’installation, le serveur MongoDB doit être démarré automatiquement (le daemon mongod pour être plus précis).

Les principales commandes

Lancer le shell

Tout part de là ! En tapant « mongo » vous devriez y accéder car /bin doit être dans votre PATH. Si vous rencontrez des problèmes, lancez

/usr/bin/mongo

Quitter le shell

Rien de plus simple: faites donc Ctrl+C (ce bon vieux SIGINT !) ou tapez exit à l’invite, vous sortirez du shell.

Obtenir le numéro de version

Les données concernant votre installation apparaissent sous la forme d’un objet JSON, la commande a exécuter est en gras :

> db.runCommand({buildinfo: 1});
{
        "version" : "2.0.6",
        "gitVersion" : "nogitversion",
        "sysInfo" : "Linux z6 3.8-trunk-amd64 #1 SMP Debian 3.8.3-1~experimental.1 x86_64 BOOST_LIB_VERSION=1_49",
        "versionArray" : [
                2,
                0,
                6,
                0
        ],
        "bits" : 64,
        "debug" : false,
        "maxBsonObjectSize" : 16777216,
        "ok" : 1
}

Lister les bases de données

La commande permettant de faire ça est show dbs.

sebastien.ferrandez@sebastien:~$ mongo
MongoDB shell version: 2.0.6
connecting to: test
> show dbs;
local   (empty)

Comme je viens d’installer MongoDB, rien d’étonnant à ce qu’aucune base de données n’apparaisse ! Il est possible d’obtenir davantage d’informations en utilisant la base de données admin dont l’usage est réservé comme son nom l’indique à l’admin.

> use admin
switched to db admin
> db.runCommand({listDatabases: 1});
{
        "databases" : [
                {
                        "name" : "local",
                        "sizeOnDisk" : 1,
                        "empty" : true
                }
        ],
        "totalSize" : 0,
        "ok" : 1
}

Créer une collection

La collection en langage MongoDB est l’équivalent de la table en relationnel, on les crée soit en y insérant le tout premier document (l’équivalent du tuple en relationnel) soit en exécutant la commande suivante :

> db.createCollection('gens');
{ "ok" : 1 }

Insérer un document dans une collection

Notre collection s’appelle gens, insérons-y un premier document :

db.gens.insert( { nom: "ferrandez", prenom:"sebastien", age: 35 } );

Lister les documents contenus dans une collection

Pour faire l’équivalent d’un SELECT * FROM gens, j’utilise la commande find sans paramètres :

> db.gens.find();
{ "_id" : ObjectId("517941f3b12e1948c04f6d5e"), "nom" : "ferrandez", "prenom" : "sebastien", "age" : 35 }

Insérons un nouveau document et observons le changement dans le find :

> db.gens.insert( { nom: "ferrandez", prenom:"sandrine", age: 34, sexe:"F" } );
> db.gens.find();
{ "_id" : ObjectId("517941f3b12e1948c04f6d5e"), "nom" : "ferrandez", "prenom" : "sebastien", "age" : 35 }
{ "_id" : ObjectId("51794334b12e1948c04f6d5f"), "nom" : "ferrandez", "prenom" : "sandrine", "age" : 34, "sexe" : "F" }

Notez que, contrairement à une table, le nombre de champs n’est pas contraint par l’intention du schéma, en effet mon deuxième document a un champ de plus que le premier : sexe.
Dans les objets JSON que j’enregistre, un champ _id est déterminé par MongoDB. Il est évidemment tout à fait possible de forcer le sien :

> db.gens.insert( { _id: 10, nom: "ferrandez", prenom:"christophe", age: 40 } );
> db.gens.find();
{ "_id" : ObjectId("517941f3b12e1948c04f6d5e"), "nom" : "ferrandez", "prenom" : "sebastien", "age" : 35 }
{ "_id" : ObjectId("51794334b12e1948c04f6d5f"), "nom" : "ferrandez", "prenom" : "sandrine", "age" : 34, "sexe" : "F" }
{ "_id" : 10, "nom" : "ferrandez", "prenom" : "christophe", "age" : 40 }

Il est possible de manipuler des identifiants auto-incrémentés mais si vous insistez pour utiliser ce mécanisme offert par certains SGBDR comme MySQL c’est peut-être que vous avez une vision trop « relationnelle » de vos données ! Si toutefois vous persistiez à vouloir bénéficier de ce mécanisme, vous trouverez le lien à la fin de ce billet.

Supprimer une collection

Il vous faut utiliser drop, comme en SQL !

> db.gens.drop();
true

Supprimer une base de données

Deux manières de procéder, en faisant un use pour se connecter à la base de données à effacer :

> show dbs;
admin   0.203125GB
gens    0.203125GB
local   (empty)
> use gens;
switched to db gens
> db.runCommand({dropDatabase: 1});
{ "dropped" : "gens", "ok" : 1 }
> show dbs;
admin   0.203125GB
local   (empty)

ou bien en exécutant directement :

> db.gens.runCommand({dropDatabase: 1});
{ "dropped" : "gens", "ok" : 1 }

Pour aller plus loin…

Au sujet des identifiants auto-incrémentés : La documentation MongoDB (anglais)

MongoDB : une présentation en 10 slides

J’ai récemment déterré un petit diaporama créé pour une présentation d’une demi-heure de MongoDB aux développeurs du site e-commerce Allopneus (avec l’aide de mon compère Lucas Filippi).

Cette présentation a été rédigée à la suite de notre venue à la conférence MongoDB en juin 2012 sur Paris.

Le document en PDFTECH-TALK-001

Le site de Lucas Filippi : LUFI