Archives par étiquette : 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 : 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 (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