Aller au contenu

Bases de données NoSQL - MongoDB

Cours pour développeurs.euses Java/PHP

illustation blog.algomaster.io

Source de l’illustration : https://blog.algomaster.io/p/sql-vs-nosql-7-key-differences


Introduction à MongoDB

Qu’est-ce que le NoSQL ?

Les bases de données relationnelles existent depuis les années 70. Elles sont très utilisées et bien documentées, particulièrement utiles pour représenter les relations entre différentes entités. Cependant, elles imposent un schéma strict.

Aujourd’hui, avec le Big Data, nous faisons face à une multitude de données de types différents. Par exemple, pour stocker l’activité d’un utilisateur sur un site web qui évolue constamment (nouvelles pages, fonctionnalités), modifier le schéma d’une base relationnelle devient contraignant, voire impossible et contre-productif !

Les bases de données NoSQL offrent une plus grande flexibilité sur le schéma des données et permettent de gérer un large éventail de formats.

Note historique : À l’origine, NoSQL signifie No SQL (pas de SQL). Il a été rebaptisé “Not Only SQL” car la frontière entre les 2 familles est devenue plus floue.

Les 4 grandes familles NoSQL

Les bases de données orientées :

  1. Documents (MongoDB, CouchDB)
  2. Colonnes (Cassandra, HBase)
  3. Graphes (Neo4j, ArangoDB)
  4. Clé-Valeurs (Redis)

Présentation rapide des modèles

Comme je l’ai précisé plus haut, depuis quelques années, le volume de données à traiter sur le Web a mis à rude épreuve les SGBD relationnels qui ont été jugés non adaptés à de nombreuses montées en charge. Ce n’est pas forcément justifié.

Les grands acteurs du Big Data comme Google, Amazon, LinkedIn ou Facebook ont été amenés à créer leurs propres systèmes de stockage et de traitement de l’information (BigTable, Dynamo et Cassandra).

Des implémentations d’architectures open source comme Hadoop et la modélisation des bases de données SGBD comme HBase, Redis, Riak, MongoDB ou encore CouchDB ont permis de démocratiser ce nouveau domaine de l’informatique répartie.

Plus le modèle est complexe, moins le système est apte à évoluer rapidement en raison de la montée en charge.

1. Documents

Le modèle orienté documents est toujours basé sur une association clé-valeur dans laquelle la valeur est un document (du BSON qui ressemble à du JSON généralement ou XML).

Les implémentations les plus populaires sont CouchDB (Apache), RavenDB, Riak et bien sûr MongoDB.

2. Colonnes

Le modèle orienté colonnes ressemble à une table dénormalisée (sans la présence de NULL, toutefois) dont la structure est dynamique.

Les SGBD les plus connus sont HBase (implémentation du BigTable de Google) et Cassandra (projet Apache qui reprend à la fois l’architecture de Dynamo d’Amazon et BigTable).

3. Graphes

Le modèle de données orienté graphes se base sur les nœuds et les arcs orientés et éventuellement valués.

Ce modèle est très bien adapté au traitement des données des réseaux sociaux où on recherche les relations entre individus de proche en proche.

Les principales solutions du marché sont Neo4j (Java) et FlockDB (Twitter).

4. Clé-valeurs

Le mode de stockage du modèle clé-valeurs (key-value) s’apparente à une table de hachage persistante qui associe une clé à une valeur (de toute nature et de types divers. La clé 1 pouvant référencer un nom, la clé 2 une date, etc…).

C’est à l’application cliente de comprendre la structure de ce blob. L’intérêt de ces systèmes est de pouvoir mutualiser cette table sur un ou plusieurs serveurs.

Les SGBD les plus connus sont Memcached, CouchBase, Redis et Volde-mort (LinkedIn).

Pour ce cours, nous allons nous concentrer sur les bases de données orientées documents.


Bases de données orientées documents

Comparaison avec le modèle relationnel

Pas besoin d’entrer dans le détail pour expliquer les concepts et fonctionnement des SGBD Relationnels puisque vous les connaissez déjà. Juste un bref rappel.

Base de données relationnelle :

Il est facile de traiter des données structurées avec, mais si nous avons des données semi-structurées comme les fichiers JSON ou XML comme ci-dessous, cela va nous poser quelques soucis :

Données semi-structurées :

Désolé pour les exemples, j’ai repris des données américaines utilisée pour le Machine Learning.

schema semi-structuré

Il serait complexe de les mettre dans une base de données relationnelles. En effet, il n’y a pas de schéma strict et nous remarquons que nos données (ici des sandwichs) comportent des attributs différents !

Conclusions :

Solution : bases orientées documents

Avec les bases de données orientées document, nous allons stocker les informations dans des documents.

Un document est un ensemble de clé-valeur, c’est pour cette raison, que ces bases de données sont adéquates pour traiter nos données semi-structurées (texte, son, image, email,…).

Ces documents sont rangés ensuite dans ce que nous appelons des collections. De cette manière, nous faisons en sorte de rassembler les documents similaires entre eux. Enfin, nous plaçons nos collections dans des bases de données.

Concepts clés :

Outils connus : Couchbase, CouchDB et MongoDB (celui que nous allons voir).


Installation sous Windows

La première étape concerne l’installation sur nos machines. Vous avez le choix entre une installation en local ou une installation avec Docker. Je ne montrerai dans ce cours que l’installation via une image Docker sous windows.

Option 1 : Installation native Windows

Téléchargement et installation

  1. Télécharger MongoDB Community Server
  2. Installer MongoDB
    • Lancez l’installateur MSI
    • Choisissez Complete installation
    • Cochez Install MongoDB as a Service
    • Cochez Install MongoDB Compass (GUI optionnelle)
  3. Vérifier l’installation

Utilisez un terminal Windows :

# Dans l'invite de commandes (CMD)
mongod --version
mongosh --version

Configuration

Créer les répertoires de données :

Personnellement, je mets mes données sur mon disque D, mais faites comme cela vous convient.

mkdir C:\data\db

Démarrer MongoDB (si non installé comme service) :

mongod --dbpath C:\data\db

Se connecter à MongoDB :

mongosh

Option 2 : Installation avec Docker (recommandé)

Surtout que vous connaissez déjà Docker et pour nous, ça peut être bien pratique pour réviser (vous avez déjà le Docker Desktop).

Prérequis

Créer le fichier docker-compose.yml

services:
  mongodb:
    image: mongo:7.0
    container_name: my_mongo
    environment:
      MONGO_INITDB_ROOT_USERNAME: admin
      MONGO_INITDB_ROOT_PASSWORD: mongo123 # mettez ce que vous voulez !
    volumes:
      - ./mongo-data:/data/db
      - ./sample_data:/sample_data
    ports:
      - "27017:27017"
    restart: unless-stopped

Rappel sur le contenu de notre fichier docker-compose.yml :

Démarrer MongoDB avec Docker

docker-compose up -d

Vous devriez avoir ce résultat (sans le dossier sample_data pour vous):

lancement docker

docker ps

Vous devriez avoir ce résultat :

lancement docker

docker exec -it my_mongo mongosh -u admin -p mongo123

lancement docker

Tout à l’air ok. Nous avons récupéré notre image MongoDB, lancer notre container et démarrer notre MongoDB pour y accéder.

Arrêter MongoDB

docker-compose down

Vous devriez avoir ce résultat :

arrêt docker


Manipulation des données sous MongoDB (CRUD)

MongoDB Shell et le type JSON.

Lancer MongoDB Shell

Installation native : mongosh (n’oubliez pas de relancer votre container Docker)

Avec Docker : docker exec -it my_mongo mongosh -u admin -p mongo123

Dans ce premier lancement, vous pouvez voir une erreur réseau. J’avais un conflit au niveau des ports. J’avais un autre container qui tournait !

arrêt relance docker

Après correction (tout est ok) :

lancement docker

Commandes de base

/*
Affichera ceci :
admin   100.00 KiB
config   72.00 KiB
local    72.00 KiB
test>
*/

Types de fichiers : JSON vs BSON

Nous ne devrions rien avoir car notre base de données est vide. Il nous faut importer des données et donc nous allons travailler avec des fichiers JSON (JavaScript Object Notation).

Nous savons déjà qu’il fait partie de la famille des données semi-structurées. Les fichiers ne sont pas “compliqués”. En effet, si nous regardons notre exemple précédent de fichier JSON, il était facile à lire grâce à son système clé-valeur.

Cependant, il possède des défauts. Le premier est que nous travaillons principalement avec des données textuelles, ce qui complexifie le traitement des autres types de données. De plus, un fichier JSON malgré sa facilité de compréhension n’est pas léger.

Un autre type de fichier proche du JSON, nommé BSON(Binary Structured Object Notation) est qualifié de Binary JSON. Contrairement au format JSON, nous pouvons avoir plusieurs types différents, les fichiers sont plus légers et il est plus facile de les traiter. Le défaut de ce fichier est son manque de lisibilité, car il s’agit d’un fichier binaire.

MongoDB va stocker ses données au format BSON, mais lorsque nous allons les requêter, nous pourrons les apercevoir sous le format JSON. Pour importer des données, il faudra utiliser la commande mongoimport qui va importer des documents dans une collection à partir d’un fichier JSON.

Il faudra lancer cette commande hors du MongoDB Shell, pour sortir de la console nous utilisons la commande exit.

JSON (JavaScript Object Notation) :

BSON (Binary JSON) :

MongoDB stocke en BSON mais affiche en JSON

Importer des données

Avec l’installation native

# On est dans le dossier D\mongodb (cd d:\mongodb) qui contient notre fichier docker-compose.yml
mkdir sample_data

lancement docker

# Télécharger les fichiers JSON (exemples avec commandes powershell `Win + R` )
Invoke-WebRequest -Uri "https://dst-de.s3.eu-west-3.amazonaws.com/mongo_fr/companies.json" -OutFile "companies.json"
Invoke-WebRequest -Uri "https://dst-de.s3.eu-west-3.amazonaws.com/mongo_fr/grades.json" -OutFile "grades.json"
Invoke-WebRequest -Uri "https://dst-de.s3.eu-west-3.amazonaws.com/mongo_fr/zips.json" -OutFile "zips.json"
# Importer les données avec la commande ci-dessous
mongoimport --db sample --collection zips --file D:\sample_data\zips.json --username admin --password mongo123 --authenticationDatabase admin
curl -o companies.json https://dst-de.s3.eu-west-3.amazonaws.com/mongo_fr/companies.json
curl -o grades.json https://dst-de.s3.eu-west-3.amazonaws.com/mongo_fr/grades.json
curl -o zips.json https://dst-de.s3.eu-west-3.amazonaws.com/mongo_fr/zips.json

Avec Docker

Votre container tourne et vous pouvez le voir dans votre Docker Desktop :

lancement docker

mkdir sample_data
docker cp sample_data/zips.json my_mongo:/sample_data/
docker exec -it my_mongo mongoimport --db sample --collection zips --file /sample_data/zips.json --username admin --password mongo123 --authenticationDatabase admin

Vous devriez obtenir ce résultat :

lancement docker

Premier document

show dbs
// puis sélectionner la collection
use sample // va nous afficher "switched to db sample"

Résultat attendu :

test> show dbs
admin   100.00 KiB
config   96.00 KiB
local    72.00 KiB
sample    1.68 MiB
test> use sample
switched to db sample
sample>

db fait référence à la base de données actuelle (sample). C’est une variable globale du shell de MongoDB. Donc, db va pointer vers notre base “sample”. Zips est le nom de notre collection dans la base de données. Une collection est l’équivalent d’une table dans une base de données relationnelle (comme MySQL). Si vous voulez lister les collections disponibles dans la base de données actuelle, vous pouvez utiliser la commande db.getCollectionNames().

db.zips.findOne()

Structure :

Base de Données (sample)
   └── Collection (zips)
       └── Document ({ _id: ..., city: 'BREMEN', ... })

Si vous ne trouvez rien, vérifiez avec ces commandes :

docker exec -it my_mongo bash
ls -la /sample_data/
cat /sample_data/zips.json
{
    "_id": {
        "$oid": "5c8eccc1caa187d17ca6ed16"
    },
    "city": "ALPINE",
    "zip": "35014",
    "loc": {
        "y": {
            "$numberDouble": "33.331165"
        },
        "x": {
            "$numberDouble": "86.208934"
        }
    },
    "pop": {
        "$numberInt": "3062"
    },
    "state": "AL"
}{
    "_id": {
        "$oid": "5c8eccc1caa187d17ca6ed17"
    },
    "city": "BESSEMER",
    "zip": "35020",
    "loc": {
        "y": {
            "$numberDouble": "33.409002"
        },
        "x": {
            "$numberDouble": "86.947547"
        }
    },
    "pop": {
        "$numberInt": "40549"
    },
    "state": "AL"
}

Résultat attendu :

base de données sample de mongo

{
  "_id": ObjectId("5c8eccc1caa187d17ca6ed17"),
  "city": "BESSEMER",
  "zip": "35020",
  "loc": { "y": 33.409002, "x": 86.947547 },
  "pop": 40549,
  "state": "AL"
}

Lecture des données

Pour ceux et celles qui n’aiment pas la saisie en lignes de commande, vous pouvez utiliser l’interface graphique Compass. Il faut la télécharger, l’installer et paramètrer le host (localhost), le port (21017), le user (admin), le password (mongo123) et l’authentification (admin).

ihm Compass pour MongoDB

findOne - Trouver un document

On cherche un document avec une population égale à 3450

db.zips.findOne({"pop": 3450})

Résultat :

{
  _id: ObjectId('5c8eccc1caa187d17ca75f81'),
  city: 'SHELL LAKE',
  zip: '54871',
  loc: { y: 45.753598, x: 91.960606 },
  pop: 3450,
  state: 'WI'
}

Document de l’état du Massachusetts :

db.zips.findOne({"state": "MA"})

base de données sample de mongo

find - Trouver plusieurs documents

Voici une liste de commandes :

// Tous les documents du Massachusetts
db.zips.find({"state": "MA"})

// Afficher les 20 suivants
db.zips.find({ "state": "MA" }).limit(20)

// afficher de 21 à 40
db.zips.find({ "state": "MA" }).skip(20).limit(20)
// skip(20) : ignore les 20 premiers documents.
// limit(20) : limite les résultats aux 20 documents suivants.

// connaitre le nombre total de documents :
db.zips.countDocuments({ "state": "MA" })
// résultat : sample> 474

Limiter les résultats

// limiter à 5 documents
db.zips.find({"state": "MA"}).limit(5)

L’écriture est particulière mais pas si compliquée, c’est toujours une syntaxe ({ “clef”: “valeur”}).


Insertion de documents

insertOne - Insérer un document

On va quand même ajouter des données françaises…

db.zips.insertOne({
  "city": "Paris",
  "pop": 2000000,
  "state": "France"
})

Résultat dans le terminal :

{
  acknowledged: true,
  insertedId: ObjectId('69aa8dc9cdf55740398563b1')
}

Note importante :

recherche de Paris

insertMany - Insérer plusieurs documents

Ici, on utilise une syntaxe avec des crochets qui correspond au format JSON [ ] pour ajouter Paris, Lyon et Marseille :

db.zips.insertMany([
  {"city": "Paris", "state": "France", "pop": 2000000},
  {"city": "Lyon", "state": "France", "pop": 500000},
  {"city": "Marseille", "state": "France", "pop": 850000}
])

Vous constatez que nos 3 objets ont bien été ajoutés :

recherche de Paris


Cependant, si nous effectuons une recherche avec la méthode findOne() pour la ville de Paris, nous allons afficher le premier document trouvé. Mais lorsque nous effectuons une recherche avec la méthode find(), nous allons avoir 2 documents !

recherche de Paris

Dans MongoDB, les doublons sont autorisés par défaut, car c’est une base de données NoSQL qui n’impose pas de contraintes d’unicité comme les bases de données relationnelles (UNIQUE dans SQL). Cependant, il existe plusieurs stratégies pour éviter ou gérer les doublons dans MongoDB…

Dans notre exemple, nous avons inséré 2 documents avec ({“city”: “Paris”}), ce qui est tout à fait valide pour MongoDB.

Stratégies pour éviter les doublons

Il existe 4 méthodes expliquées ci-dessous.

Utiliser un index Unique

La meilleure façon d’éviter les doublons est de créer un index unique avec createIndex sur le champ (ou combinaison de champs) qui doit être unique.

Exemple : Créer un Index Unique sur city :

db.zips.createIndex({ "city": 1 }, { unique: true })

Résultat : Si on essaie d’insérer un autre document avec ({“city”: “Paris”}), MongoDB rejettera l’insertion avec une erreur :

db.zips.insertOne({ "city": "Paris", "pop": 2000000, "state": "France" })
// erreur : E11000 duplicate key error collection: sample.zips index: city_1 dup key: { city: "Paris" }

Utiliser updateOne() avec upsert

Si on souhaite mettre à jour un document existant ou l’insérer s’il n’existe pas, utilisez updateOne() avec l’option upsert: true.

Exemple :

db.zips.updateOne(
  { "city": "Paris" },  // notre critère de recherche
  { $set: { "pop": 2000000, "state": "France" } },  // la mise à jour
  { upsert: true }  // insertion se fait si le document n'existe pas !
)

Vérifier l’existence insertion

Exemple :

var existing = db.zips.findOne({ "city": "Paris" });
if (!existing) {
  db.zips.insertOne({ "city": "Paris", "pop": 2000000, "state": "France" });
} else {
  print("Document avec city=Paris existe déjà !");
}

Utiliser un champ uniqueId ou un id Personnalisé

Si on veut éviter les doublons sur plusieurs champs, on peut créer un champ composite unique ou utiliser un _id personnalisé. Exemple avec un Index Composite Unique :

db.zips.createIndex({ "city": 1, "state": 1 }, { unique: true })

Bonus pour afficher les doublons

db.zips.aggregate([
  { $group: {
      _id: "$city",
      count: { $sum: 1 },
      ids: { $push: "$_id" }
  }},
  { $match: {
      count: { $gt: 1 }
  }}
])

Résultat :

[
  {
    "_id": "Paris",
    "count": 2,
    "ids": [
      ObjectId("69aa8dc9cdf55740398563b1"),
      ObjectId("69aa8fcfcdf55740398563b2")
    ]
  }
]

En réalité, vous allez avoir des doublons pour de nombreuses villes… ;)

 {
    _id: 'SANTEE',
    count: 2,
    ids: [
      ObjectId('5c8eccc1caa187d17ca6f4ba'),
      ObjectId('5c8eccc1caa187d17ca74814')
    ]
  },
  {
    _id: 'HESSTON',
    count: 2,
    ids: [
      ObjectId('5c8eccc1caa187d17ca70fce'),
      ObjectId('5c8eccc1caa187d17ca74418')
    ]
  },
  {
    _id: 'BRADFORD',
    count: 12,
    ids: [
      ObjectId('5c8eccc1caa187d17ca6f1b0'),
      ObjectId('5c8eccc1caa187d17ca70543'),
      ObjectId('5c8eccc1caa187d17ca70ae3'),
      ObjectId('5c8eccc1caa187d17ca716e8'),
      ObjectId('5c8eccc1caa187d17ca71a7b'),
      ObjectId('5c8eccc1caa187d17ca72baf'),
      ObjectId('5c8eccc1caa187d17ca7358d'),
      ObjectId('5c8eccc1caa187d17ca73d1b'),
      ObjectId('5c8eccc1caa187d17ca7442f'),
      ObjectId('5c8eccc1caa187d17ca7478a'),
      ObjectId('5c8eccc1caa187d17ca74c76'),
      ObjectId('5c8eccc1caa187d17ca75460')
    ]
  },
  {
    _id: 'VERNON CENTER',
    count: 2,
    ids: [
      ObjectId('5c8eccc1caa187d17ca72093'),
      ObjectId('5c8eccc1caa187d17ca733d2')
    ]
  },
  {
    _id: 'BOONVILLE',
    count: 5,
    ids: [
      ObjectId('5c8eccc1caa187d17ca6f7f3'),
      ObjectId('5c8eccc1caa187d17ca70a52'),
      ObjectId('5c8eccc1caa187d17ca7268c'),
      ObjectId('5c8eccc1caa187d17ca7337a'),
      ObjectId('5c8eccc1caa187d17ca735db')
    ]
  },

Mise à jour de documents

updateOne() - Mettre à jour un document

Ici, on va modifier l’état de la ville Allemande de Bremen en lui assignant la valeur de France. C’est juste pour la démonstration.

recherche de Paris

// Changer l'état de BREMEN qui est "AL" pour Allemagne
// en "France"
db.zips.updateOne(
  {"city": "BREMEN"},
  {$set: {"state": "France"}}
)

// Vérifier la modification
db.zips.find({"state": "France"})

Résultats :

recherche de Paris

En texte : la commande

sample> db.zips.updateOne(
   {"city": "BREMEN"},
   {$set: {"state": "France"}}
 )
{
  acknowledged: true,
  insertedId: null,
  matchedCount: 1,
  modifiedCount: 1,
  upsertedCount: 0
}

En texte : la vérification

sample> db.zips.find({"state": "France"})
[
  {
    _id: ObjectId('5c8eccc1caa187d17ca6ed1d'),
    city: 'BREMEN',
    zip: '35033',
    loc: { y: 33.973664, x: 87.004281 },
    pop: 3448,
    state: 'France'
  },
  {
    _id: ObjectId('69aa8dc9cdf55740398563b1'),
    city: 'Paris',
    pop: 2000000,
    state: 'France'
  },
  {
    _id: ObjectId('69aa8fcfcdf55740398563b2'),
    city: 'Paris',
    state: 'France',
    pop: 2000000
  },
  {
    _id: ObjectId('69aa8fcfcdf55740398563b3'),
    city: 'Lyon',
    state: 'France',
    pop: 500000
  },
  {
    _id: ObjectId('69aa8fcfcdf55740398563b4'),
    city: 'Marseille',
    state: 'France',
    pop: 850000
  }
]

Opérateurs de mise à jour

$set pour remplacer une valeur : {$set: {"clef": valeur}}

db.zips.updateOne(
  {"city": "Paris"},
  {$set: {"pop": 2200000}}
)

Voici les explications sur les éléments affichés à la suite de la commande updateOne() :

{
  acknowledged: true,
  insertedId: null,
  matchedCount: 1,
  modifiedCount: 1,
  upsertedCount: 0
}
Champ Valeur Description
acknowledged true Indique que la commande a été reconnue et exécutée par MongoDB.
insertedId null L’_id du document inséré (si upsert: true et qu’un nouveau document a été inséré).
matchedCount 1 Nombre de documents correspondants au critère de recherche ({ "city": "Paris" }).
modifiedCount 1 Nombre de documents effectivement modifiés.
upsertedCount 0 Nombre de documents insérés (si upsert: true et qu’aucun document ne correspondait).

Résultat :

 db.zips.findOne({"city": "Paris"})
{
  _id: ObjectId('69aa8dc9cdf55740398563b1'),
  city: 'Paris',
  pop: 2200000,
  state: 'France'
}

$push pour ajouter un tags (élément) à un document existant : {$push: {"tags": "nouveauTag"}}

db.zips.updateOne(
  { "city": "Paris" },  // critère de recherche 
  { $push: { "tags": "nouveauTag" } }  // on ajoute "nouveauTag" au tableau `tags`
)

Explications :

Vérification (nous avons bien tags: [ ‘nouveauTag’ ]) :

db.zips.findOne({"city": "Paris"})
{
  _id: ObjectId('69aa8dc9cdf55740398563b1'),
  city: 'Paris',
  pop: 2200000,
  state: 'France',
  tags: [ 'nouveauTag' ]
}

Pourquoi un tableau ?

Pour des raisons de flexibilité des Données :

Trouver les documents qui contiennent un tag spécifique :

db.zips.find({ "tags": "touristique" })

$push pour ajouter un tag à Paris avec la syntaxe { $push: { "tags": "nouveauTag" } } :

# premier tag
db.zips.updateOne(
  { "city": "Paris" },
  { $push: { "tags": "nouveauTag" } }
)
# second tag
db.zips.updateOne(
  { "city": "Paris" },
  { $push: { "tags": "secondTag" } }
)

push ajout nouveau tag

push ajout nouveau tag

$pull pour supprimer un tag de Paris :

db.zips.updateOne(
  { "city": "Paris" },
  { $pull: { "tags": "nouveauTag" } }
)

Je vous laisse vérifier (vous ne devriez avoir aucun résultat pour db.zips.find({ "tags": "nouveauTag" })).

find() pour trouver les villes avec un tag spécifique find({ "tags": "nom du tag" }) :

db.zips.find({ "tags": "touristique" })

$unwind, $group, $sort pour compter les occurrences par Tag :

db.zips.aggregate([
  { $unwind: "$tags" },
  { $group: { _id: "$tags", count: { $sum: 1 } } },
  { $sort: { count: -1 } }
])

Ici, il ne devrait en trouver qu’un dans notre cas [ { _id: 'secondTag', count: 1 } ]. Vous pouvez en créer d’autres pour réaliser des tests.

Utiliser un champ simple

On pourrait se poser la question de savoir : quand utiliser un champ simple ?.

Voici les 2 raisons légitimes :

Autre fonctions

$inc pour incrémenter une valeur : {$inc: {"pop": 50000}}

db.zips.updateOne(
  {"city": "Paris"},
  {$inc: {"pop": 50000}}
)

Résultat :

 db.zips.findOne({"city": "Paris"})
{
  _id: ObjectId('69aa8dc9cdf55740398563b1'),
  city: 'Paris',
  pop: 2250000,
  state: 'France',
  tags: [ 'secondTag' ]
}

updateMany - Mettre à jour plusieurs documents

//augmenter la population de toutes les villes françaises
db.zips.updateMany(
  {"state": "France"},
  {$inc: {"pop": 100}}
)

Je vous laissse faire les vérifications ! sachant que la population de BREMEN qui est en Allemagne et que nous avosn mis en France était de 3448.


Suppression de documents

deleteOne() - Supprimer un document

Soit, vous supprimez par l’id :

db.zips.deleteOne({"_id": ObjectId("...")})

Exemple : db.zips.deleteOne({"_id": ObjectId("69aa8dc9cdf55740398563b1")})

delete one

Soit, vous supprimez pas un critère :

db.zips.deleteOne({"city": "Paris"})

deleteMany() - Supprimer plusieurs documents

// supprimer toutes les villes françaises
db.zips.deleteMany({"state": "France"})
sample> db.zips.find({ "state": "France"})
[
  {
    _id: ObjectId('5c8eccc1caa187d17ca6ed1d'),
    city: 'BREMEN',
    zip: '35033',
    loc: { y: 33.973664, x: 87.004281 },
    pop: 3548,
    state: 'France'
  },
  {
    _id: ObjectId('69aa8fcfcdf55740398563b2'),
    city: 'Paris',
    state: 'France',
    pop: 2000100
  },
  {
    _id: ObjectId('69aa8fcfcdf55740398563b3'),
    city: 'Lyon',
    state: 'France',
    pop: 500100
  },
  {
    _id: ObjectId('69aa8fcfcdf55740398563b4'),
    city: 'Marseille',
    state: 'France',
    pop: 850100
  }
]
sample> db.zips.deleteMany({"state": "France"})
{ acknowledged: true, deletedCount: 4 }

# si vous exécutez cette ligne, aucune ville ne s'affichera !
db.zips.find({ "state": "France"})

Supprimer une collection

db.zips.drop()

Attention : La suppression est définitive !


Requêtes MQL avancées (Mongo Query Language)

Opérateurs de comparaison

Opérateur Description Exemple
$eq Égalité {"pop": {$eq: 1000}}
$ne Différence {"state": {$ne: "AL"}}
$gt Strictement supérieur {"pop": {$gt: 10000}}
$lt Strictement inférieur {"pop": {$lt: 5000}}
$gte Supérieur ou égal {"pop": {$gte: 12678}}
$lte Inférieur ou égal {"pop": {$lte: 20000}}

Je suppose que vous connaissez le sens des opérateurs ci-dessus.

Exemples

// population >= 12678
db.zips.find({"pop": {$gte: 12678}})

// population entre 10000 et 20000
db.zips.find({
  "pop": {$gte: 10000, $lte: 20000}
})

Je ne mets plus forcément les résultats. Testez sur votre machine ! pour voir tous les résultats, il vous faut saisir it et entrée.


Opérateurs logiques

La structure est particulière car les opérateurs $and et $or sont placés avant les critères.

Opérateur Description
$and ET logique
$or OU logique
$nor NON-OU logique
$not NON logique

Exemples

Opérateur $and :

// population entre 10000 et 20000 en Californie
db.zips.find({
  $and: [
    {"state": "CA"},
    {"pop": {$gte: 10000}},
    {"pop": {$lte: 20000}}
  ]
})

Opérateur $or :

// ici, l'État CA pour Californie ou TX pour Texas
db.zips.find({
  $or: [
    {"state": "CA"},
    {"state": "TX"}
  ]
})

Remarque : dans la suite nous allons utiliser aussi une autre collection qu’il vous faudra importer dans MongoDB, c’est grades.json.

Voici la syntaxe si vous l’avez oublié (par contre, il faudra sortir du Shell MongoDB en faisant un exit) :

docker exec -it my_mongo mongoimport --db sample --collection grades --file /sample_data/grades.json --username admin --password mongo123 --authenticationDatabase admin

puis :

docker exec -it my_mongo mongosh -u admin -p mongo123
show dbs
use sample
show collections

Vous obtiendrez l’affichage de nos 2 colelctions :

show collections
grades
zips

Comparaison entre champs :

// comparer deux champs du même document : le code de l'étudiant et celui de la classe
db.grades.find({
  $expr: {$eq: ["$student_id", "$class_id"]}
})

Résultat (début) :

comparaisons grades


Méthodes Cursor

sort - Trier les résultats

// tri croissant (1)
db.grades.find().sort({"student_id": 1})

// tri décroissant (-1)
db.grades.find().sort({"student_id": -1})

// tri sur plusieurs champs
db.grades.find().sort({"student_id": -1, "class_id": 1})

limit - Limiter les résultats

// 5 premiers documents
db.zips.find().limit(5)

// combinaison sort + limit
db.zips.find().sort({"city": 1}).limit(5)

count - Compter les documents

// Nombre total de documents (29469)
db.zips.find().count()

// Nombre de documents filtrés (1523)
db.zips.find({"state": "CA"}).count()

Projection

Sélectionner des champs spécifiques

Remarquez la syntaxe particulère avec la virgule ,.

// afficher uniquement la population
db.zips.find({}, {"pop": 1})

// afficher population et ville
db.zips.find({}, {"pop": 1, "city": 1})

// exclure un champ
db.zips.find({}, {"pop": 0})

// exclure l'_id
db.zips.find({}, {"pop": 1, "city": 1, "_id": 0})

Dot Notation (les champs imbriqués)

dot sous-champs

// accéder aux sous-champs
db.zips.find({}, {"loc.y": 1, "loc.x": 1})

// premier élément d'un tableau (il vous faudra récupérer le fichier companies.json)
db.companies.find({}, {"acquisitions.0.acquired_year": 1})

vérifiez que ça existe :

db.companies.find({ "acquisitions.acquired_year": { $exists: true } })

Syntaxe dans le Compass : { "acquisitions.0.acquired_year": { $exists: true }, "acquisitions.0.acquired_year": 1 }

Voici un extrait d’un document de Companies :

extrait document companies

Remarque : MongoDB inclut toujours le champ parent (acquisitions) dans le résultat, même si le sous-champ (acquired_year) n’existe pas ou si le tableau est vide. Cela permet de conserver la structure du document dans les résultats. C’est un peu déroutant si vous êtes habitué.e à du SQL standard !

Opérateur $elemMatch

// itérer sur un tableau
db.companies.find({
  "acquisitions": {
    "$elemMatch": {
      "acquired_year": 2010,
      "acquired_month": 3,
      "acquired_day": 2
    }
  }
}, {"name": 1})

Résultat :

companies itération


Agrégations et Index

Framework d’agrégation

Concept de Pipeline

Un pipeline est une série d’étapes (stages) qui transforment progressivement les données.

db.collection.aggregate([
  {$stage1: {...}},
  {$stage2: {...}},
  {$stage3: {...}}
])

Stage $match - Filtrer

db.companies.aggregate([
  {$match: {"offices.city": "Seattle"}}
])

Stage $project - Projeter et transformer

db.companies.aggregate([
  {$match: {"offices.city": "Seattle"}},
  {$project: {"_id": 1, "name": 1}}
])

// renommer un champ
db.companies.aggregate([
  {$project: {"society": "$name"}}
])

Stage $group - Grouper et agréger

// nombre de sociétés par année de fondation
db.companies.aggregate([
  {$group: {
    "_id": "$founded_year",
    "nb_companies": {$sum: 1}
  }}
])

// tri
db.companies.aggregate([
  {$group: {
    "_id": "$founded_year",
    "nb_companies": {$sum: 1}
  }},
  {$sort: {"nb_companies": -1}}
])

Fonctions d’agrégation

Fonction Description
$sum Somme
$avg Moyenne
$min Minimum
$max Maximum
$first Premier élément
$last Dernier élément

Index

Pourquoi les index ?

Les index améliorent les performances des requêtes en permettant une recherche plus rapide.

Créer un index simple

Déjà abordé en début de cours, juste un petit rappel de la syntaxe et voir les performances que cela impliquent.

// index sur le champ "city"
db.zips.createIndex({"city": 1})

Créer un index composé

// index sur "state" puis "city"
db.zips.createIndex({"state": 1, "city": 1})

Analyser les performances

// avant index
db.zips.find({"city": "BOSTON"}).explain("executionStats")

// créer l'index
db.zips.createIndex({"city": 1})

// après index (beaucoup plus rapide)
db.zips.find({"city": "BOSTON"}).explain("executionStats")

Lister les index

db.zips.getIndexes()

Supprimer un index

db.zips.dropIndex({"city": 1})

Requêter MongoDB avec Java

Configuration Maven

Ajouter la dépendance MongoDB

pom.xml :

<dependencies>
    <!-- MongoDB Java Driver -->
    <dependency>
        <groupId>org.mongodb</groupId>
        <artifactId>mongodb-driver-sync</artifactId>
        <version>4.11.1</version>
    </dependency>
</dependencies>

Connexion à MongoDB

import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.MongoCollection;
import org.bson.Document;

public class MongoDBExample {
    public static void main(String[] args) {
        // Connexion à MongoDB
        String connectionString = "mongodb://admin:mongo123@localhost:27017";
        MongoClient mongoClient = MongoClients.create(connectionString);
        
        // Sélectionner la base de données
        MongoDatabase database = mongoClient.getDatabase("sample");
        
        // Sélectionner la collection
        MongoCollection<Document> collection = database.getCollection("zips");
        
        // Fermer la connexion
        mongoClient.close();
    }
}

Opérations CRUD en Java

Insérer un document

import org.bson.Document;

// Créer un document
Document doc = new Document("city", "Paris")
    .append("state", "France")
    .append("pop", 2000000);

// Insérer
collection.insertOne(doc);
System.out.println("Document inséré !");

Insérer plusieurs documents

import java.util.Arrays;

List<Document> documents = Arrays.asList(
    new Document("city", "Lyon").append("state", "France").append("pop", 500000),
    new Document("city", "Marseille").append("state", "France").append("pop", 850000)
);

collection.insertMany(documents);

Lire des documents

import com.mongodb.client.FindIterable;
import com.mongodb.client.MongoCursor;

// Trouver un document
Document doc = collection.find(new Document("city", "Paris")).first();
System.out.println(doc.toJson());

// Trouver plusieurs documents
FindIterable<Document> docs = collection.find(new Document("state", "France"));

try (MongoCursor<Document> cursor = docs.iterator()) {
    while (cursor.hasNext()) {
        System.out.println(cursor.next().toJson());
    }
}

Mettre à jour un document

import com.mongodb.client.model.Filters;
import com.mongodb.client.model.Updates;

// Mise à jour simple
collection.updateOne(
    Filters.eq("city", "Paris"),
    Updates.set("pop", 2200000)
);

// Mise à jour multiple
collection.updateMany(
    Filters.eq("state", "France"),
    Updates.inc("pop", 100)
);

Supprimer un document

// Supprimer un document
collection.deleteOne(Filters.eq("city", "Paris"));

// Supprimer plusieurs documents
collection.deleteMany(Filters.eq("state", "France"));

Requêtes avancées en Java

Filtres complexes

import static com.mongodb.client.model.Filters.*;

// Population entre 10000 et 20000
FindIterable<Document> docs = collection.find(
    and(
        gte("pop", 10000),
        lte("pop", 20000)
    )
);

// État CA ou TX (déjà fait en ligne de commande)
FindIterable<Document> docs2 = collection.find(
    or(
        eq("state", "CA"),
        eq("state", "TX")
    )
);

Projection (ce que l’on veut afficher ou récupérer)

import static com.mongodb.client.model.Projections.*;

// Afficher uniquement city et pop
FindIterable<Document> docs = collection.find()
    .projection(fields(include("city", "pop"), excludeId()));

Tri et limite

import static com.mongodb.client.model.Sorts.*;

// Trier par city (ordre croissant), limiter à 5
FindIterable<Document> docs = collection.find()
    .sort(ascending("city"))
    .limit(5);

Agrégation

import com.mongodb.client.AggregateIterable;
import java.util.Arrays;
import static com.mongodb.client.model.Aggregates.*;
import static com.mongodb.client.model.Accumulators.*;

// Nombre de villes par état
AggregateIterable<Document> result = collection.aggregate(Arrays.asList(
    group("$state", sum("count", 1)),
    sort(descending("count"))
));

result.forEach(doc -> System.out.println(doc.toJson()));

Requêter MongoDB avec PHP

Installation du driver MongoDB

composer require mongodb/mongodb

Installation de l’extension PHP

Windows :

  1. Télécharger l’extension : https://pecl.php.net/package/mongodb
  2. Copier php_mongodb.dll dans le dossier ext de PHP
  3. Ajouter dans php.ini
   extension=mongodb
  1. Redémarrer le serveur web (Apache/Nginx)

Connexion à MongoDB pour php

<?php
require 'vendor/autoload.php';

use MongoDB\Client;

// Connexion
$client = new Client("mongodb://admin:mongo123@localhost:27017");

// Sélectionner la base de données
$database = $client->sample;

// Sélectionner la collection
$collection = $database->zips;
?>

Opérations CRUD en PHP

Insérer un document

<?php
// Insérer un document
$result = $collection->insertOne([
    'city' => 'Paris',
    'state' => 'France',
    'pop' => 2000000
]);

echo "Document inséré avec l'ID : " . $result->getInsertedId();
?>

Insérer plusieurs documents

<?php
$result = $collection->insertMany([
    ['city' => 'Lyon', 'state' => 'France', 'pop' => 500000],
    ['city' => 'Marseille', 'state' => 'France', 'pop' => 850000]
]);

echo "Nombre de documents insérés : " . $result->getInsertedCount();
?>

Lire des documents

<?php
// Trouver un document
$document = $collection->findOne(['city' => 'Paris']);
print_r($document);

// Trouver plusieurs documents
$cursor = $collection->find(['state' => 'France']);

foreach ($cursor as $document) {
    echo $document['city'] . " - " . $document['pop'] . "\n";
}
?>

Mettre à jour un document

<?php
// Mise à jour simple
$result = $collection->updateOne(
    ['city' => 'Paris'],
    ['$set' => ['pop' => 2200000]]
);

echo "Documents modifiés : " . $result->getModifiedCount();

// Mise à jour multiple
$result = $collection->updateMany(
    ['state' => 'France'],
    ['$inc' => ['pop' => 100]]
);
?>

Supprimer un document

<?php
// Supprimer un document
$result = $collection->deleteOne(['city' => 'Paris']);
echo "Documents supprimés : " . $result->getDeletedCount();

// Supprimer plusieurs documents
$result = $collection->deleteMany(['state' => 'France']);
?>

Requêtes avancées en PHP

Filtres complexes

<?php
// Population entre 10000 et 20000
$cursor = $collection->find([
    'pop' => ['$gte' => 10000, '$lte' => 20000]
]);

// Opérateur $and
$cursor = $collection->find([
    '$and' => [
        ['state' => 'CA'],
        ['pop' => ['$gte' => 10000]]
    ]
]);
?>

Projection

<?php
// Afficher uniquement city et pop
$cursor = $collection->find(
    [],
    ['projection' => ['city' => 1, 'pop' => 1, '_id' => 0]]
);
?>

Tri et limite

<?php
$cursor = $collection->find(
    [],
    [
        'sort' => ['city' => 1],
        'limit' => 5
    ]
);
?>

Agrégation

<?php
// Nombre de villes par état
$cursor = $collection->aggregate([
    ['$group' => [
        '_id' => '$state',
        'count' => ['$sum' => 1]
    ]],
    ['$sort' => ['count' => -1]]
]);

foreach ($cursor as $document) {
    echo $document['_id'] . " : " . $document['count'] . "\n";
}
?>

Modélisation de données avec MongoDB

Méthodologie

Les 4 étapes

  1. Créer le design de l’application
    • Définir les besoins métier
    • Identifier les cas d’usage
  2. Définir le modèle des données
    • Structurer les documents
    • Déterminer les collections
  3. Améliorer l’application
    • Ajouter de nouvelles fonctionnalités
  4. Améliorer le modèle des données
    • Adapter la structure aux nouveaux besoins

Ces étapes 3 et 4 sont itératives (approche CI/CD)


Relations entre collections

One-to-One (1-1)

Exemple : Un utilisateur a un profil

Document embarqué :

{
  "_id": 1,
  "username": "johnny_depp",
  "profile": {
    "firstname": "Johnny",
    "lastname": "Depp",
    "age": 30
  }
}

One-to-Many (1-N)

Exemple : Un artiste a plusieurs chansons

Option 1 : Tableau embarqué :

{
  "_id": 1,
  "name": "ABBA",
  "songs": [
    {"title": "Gimme Gimme Gimme", "year": 1979},
    {"title": "Dancing Queen", "year": 1976},
    {"title": "Money, Money, Money", "year": 1976}
  ]
}

Option 2 : Référence :

// Collection artistes
{
  "_id": 1,
  "name": "ABBA",
  "nationality": "Swedish"
}

// Collection Chansons
{
  "_id": 101,
  "title": "Gimme Gimme Gimme",
  "artist_id": 1
}

Many-to-Many (N-N)

Exemple : Étudiants et cours

// Collection étudiants
{
  "_id": 1,
  "name": "Alicia",
  "course_ids": [1, 2, 3]
}

// Collection cours
{
  "_id": 1,
  "title": "Mathematiques",
  "student_ids": [1, 5, 8]
}

Choix entre embarqué et référence

Documents embarqués

Avantages :

Inconvénients :

Quand utiliser :

Références

Avantages :

Inconvénients :

Quand utiliser :


ANNEXE : Commandes utiles

Commandes MongoDB Shell

// Bases de données
show dbs                    // Lister les bases
use nomBase                 // Utiliser/créer une base
db.dropDatabase()           // Supprimer la base courante

// Collections
show collections            // Lister les collections
db.createCollection("nom")  // Créer une collection
db.collection.drop()        // Supprimer une collection

// Documents
db.collection.find()        // Tout afficher
db.collection.find().pretty() // Affichage formaté
db.collection.count()       // Compter
db.collection.findOne()     // Un document

// Index
db.collection.getIndexes()  // Lister les index
db.collection.createIndex({"field": 1}) // Créer index
db.collection.dropIndex({"field": 1})   // Supprimer index

Opérateurs MongoDB

Comparaison

Logique

Éléments

Tableaux

Mise à jour


CONCLUSION

MongoDB est une base de données NoSQL puissante et flexible, particulièrement adaptée pour :

Points clés à retenir :

  1. Schéma flexible
  2. Documents JSON/BSON
  3. Requêtes puissantes (MQL + agrégation)
  4. Index pour la performance
  5. Intégration facile avec Java et PHP

Pour aller plus loin :