Source de l’illustration : https://blog.algomaster.io/p/sql-vs-nosql-7-key-differences
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.
No SQL
Les bases de données orientées :
Documents
Colonnes
Graphes
Clé-Valeurs
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.
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).
BSON
Les implémentations les plus populaires sont CouchDB (Apache), RavenDB, Riak et bien sûr MongoDB.
MongoDB
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).
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).
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.
documents
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 structurées
Données semi-structurées :
Désolé pour les exemples, j’ai repris des données américaines utilisée pour le Machine Learning.
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 :
Avec les bases de données orientées document, nous allons stocker les informations dans des documents.
orientées document
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.
collections
Concepts clés :
ensemble de paires clé-valeur
regroupement de documents
ensemble de collections
Outils connus : Couchbase, CouchDB et MongoDB (celui que nous allons voir).
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.
Complete installation
Install MongoDB as a Service
Install MongoDB Compass
Utilisez un terminal Windows :
# Dans l'invite de commandes (CMD) mongod --version mongosh --version
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
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).
Docker Desktop
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 :
docker-compose.yml
version
services
mongodb
image
container_name
environment
volumes
ports
docker-compose up -d
Vous devriez avoir ce résultat (sans le dossier sample_data pour vous):
docker ps
Vous devriez avoir ce résultat :
docker exec -it my_mongo mongosh -u admin -p mongo123
Tout à l’air ok. Nous avons récupéré notre image MongoDB, lancer notre container et démarrer notre MongoDB pour y accéder.
docker-compose down
MongoDB Shell et le type JSON.
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 !
Après correction (tout est ok) :
show dbs
/* Affichera ceci : admin 100.00 KiB config 72.00 KiB local 72.00 KiB test> */
Pour utiliser/créer une base de données : use maBaseDeDonnees
use maBaseDeDonnees
Pour afficher les collections : show collections
show collections
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.
mongoimport
Il faudra lancer cette commande hors du MongoDB Shell, pour sortir de la console nous utilisons la commande exit.
exit
JSON (JavaScript Object Notation) :
BSON (Binary JSON) :
MongoDB stocke en BSON mais affiche en JSON
# On est dans le dossier D\mongodb (cd d:\mongodb) qui contient notre fichier docker-compose.yml mkdir sample_data
# 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
Votre container tourne et vous pouvez le voir dans votre Docker Desktop :
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 :
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>
findOne()
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.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" }
{ "_id": ObjectId("5c8eccc1caa187d17ca6ed17"), "city": "BESSEMER", "zip": "35020", "loc": { "y": 33.409002, "x": 86.947547 }, "pop": 40549, "state": "AL" }
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).
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"})
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 à 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”}).
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 :
_id
Il est du Type : ObjectId
ObjectId
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 :
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 !
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.
Il existe 4 méthodes expliquées ci-dessous.
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" }
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 ! )
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à !"); }
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 })
db.zips.aggregate([ { $group: { _id: "$city", count: { $sum: 1 }, ids: { $push: "$_id" } }}, { $match: { count: { $gt: 1 } }} ])
[ { "_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') ] },
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.
// 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 :
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 } ]
$set pour remplacer une valeur : {$set: {"clef": 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 }
acknowledged
true
insertedId
null
upsert: true
matchedCount
1
{ "city": "Paris" }
modifiedCount
upsertedCount
0
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"}}
{$push: {"tags": "nouveauTag"}}
db.zips.updateOne( { "city": "Paris" }, // critère de recherche { $push: { "tags": "nouveauTag" } } // on ajoute "nouveauTag" au tableau `tags` )
Explications :
{ $push: { "tags": "nouveau" } }
$push
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 :
"tags": ["capitale", "touristique", "grande_ville", "nouveauTag"]
find({ "tags": "touristique" })
find({ "tags": { $all: ["touristique", "grande_ville"] } })
$unwind
aggregate([{ $unwind: "$tags" }, { $group: { _id: "$tags", count: { $sum: 1 } } }])
updateOne({ "city": "Paris" },{ $set: { "tags.0": "nouvelleValeur" } })
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" } } :
{ $push: { "tags": "nouveauTag" } }
# premier tag db.zips.updateOne( { "city": "Paris" }, { $push: { "tags": "nouveauTag" } } ) # second tag db.zips.updateOne( { "city": "Paris" }, { $push: { "tags": "secondTag" } } )
$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" })).
db.zips.find({ "tags": "nouveauTag" })
find() pour trouver les villes avec un tag spécifique find({ "tags": "nom du tag" }) :
find({ "tags": "nom du tag" })
$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.
[ { _id: 'secondTag', count: 1 } ]
On pourrait se poser la question de savoir : quand utiliser un champ simple ?.
Voici les 2 raisons légitimes :
$inc pour incrémenter une valeur : {$inc: {"pop": 50000}}
{$inc: {"pop": 50000}}
db.zips.updateOne( {"city": "Paris"}, {$inc: {"pop": 50000}} )
db.zips.findOne({"city": "Paris"}) { _id: ObjectId('69aa8dc9cdf55740398563b1'), city: 'Paris', pop: 2250000, state: 'France', tags: [ 'secondTag' ] }
//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.
Soit, vous supprimez par l’id :
db.zips.deleteOne({"_id": ObjectId("...")})
Exemple : db.zips.deleteOne({"_id": ObjectId("69aa8dc9cdf55740398563b1")})
db.zips.deleteOne({"_id": ObjectId("69aa8dc9cdf55740398563b1")})
Soit, vous supprimez pas un critère :
db.zips.deleteOne({"city": "Paris"})
// 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"})
db.zips.drop()
Attention : La suppression est définitive !
$eq
{"pop": {$eq: 1000}}
$ne
{"state": {$ne: "AL"}}
$gt
{"pop": {$gt: 10000}}
$lt
{"pop": {$lt: 5000}}
$gte
{"pop": {$gte: 12678}}
$lte
{"pop": {$lte: 20000}}
Je suppose que vous connaissez le sens des opérateurs ci-dessus.
// 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.
La structure est particulière car les opérateurs $and et $or sont placés avant les critères.
$and
$or
$nor
$not
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) :
// 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})
// 5 premiers documents db.zips.find().limit(5) // combinaison sort + limit db.zips.find().sort({"city": 1}).limit(5)
// Nombre total de documents (29469) db.zips.find().count() // Nombre de documents filtrés (1523) db.zips.find({"state": "CA"}).count()
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})
// 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 }
{ "acquisitions.0.acquired_year": { $exists: true }, "acquisitions.0.acquired_year": 1 }
Voici un extrait d’un document de 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 !
// itérer sur un tableau db.companies.find({ "acquisitions": { "$elemMatch": { "acquired_year": 2010, "acquired_month": 3, "acquired_day": 2 } } }, {"name": 1})
Un pipeline est une série d’étapes (stages) qui transforment progressivement les données.
db.collection.aggregate([ {$stage1: {...}}, {$stage2: {...}}, {$stage3: {...}} ])
db.companies.aggregate([ {$match: {"offices.city": "Seattle"}} ])
db.companies.aggregate([ {$match: {"offices.city": "Seattle"}}, {$project: {"_id": 1, "name": 1}} ]) // renommer un champ db.companies.aggregate([ {$project: {"society": "$name"}} ])
// 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}} ])
$sum
$avg
$min
$max
$first
$last
Les index améliorent les performances des requêtes en permettant une recherche plus rapide.
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})
// index sur "state" puis "city" db.zips.createIndex({"state": 1, "city": 1})
// 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")
db.zips.getIndexes()
db.zips.dropIndex({"city": 1})
pom.xml :
<dependencies> <!-- MongoDB Java Driver --> <dependency> <groupId>org.mongodb</groupId> <artifactId>mongodb-driver-sync</artifactId> <version>4.11.1</version> </dependency> </dependencies>
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(); } }
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é !");
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);
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()); } }
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 collection.deleteOne(Filters.eq("city", "Paris")); // Supprimer plusieurs documents collection.deleteMany(Filters.eq("state", "France"));
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") ) );
import static com.mongodb.client.model.Projections.*; // Afficher uniquement city et pop FindIterable<Document> docs = collection.find() .projection(fields(include("city", "pop"), excludeId()));
import static com.mongodb.client.model.Sorts.*; // Trier par city (ordre croissant), limiter à 5 FindIterable<Document> docs = collection.find() .sort(ascending("city")) .limit(5);
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()));
composer require mongodb/mongodb
Windows :
php_mongodb.dll
ext
php.ini
extension=mongodb
<?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; ?>
<?php // Insérer un document $result = $collection->insertOne([ 'city' => 'Paris', 'state' => 'France', 'pop' => 2000000 ]); echo "Document inséré avec l'ID : " . $result->getInsertedId(); ?>
<?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(); ?>
<?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"; } ?>
<?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]] ); ?>
<?php // Supprimer un document $result = $collection->deleteOne(['city' => 'Paris']); echo "Documents supprimés : " . $result->getDeletedCount(); // Supprimer plusieurs documents $result = $collection->deleteMany(['state' => 'France']); ?>
<?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]] ] ]); ?>
<?php // Afficher uniquement city et pop $cursor = $collection->find( [], ['projection' => ['city' => 1, 'pop' => 1, '_id' => 0]] ); ?>
<?php $cursor = $collection->find( [], [ 'sort' => ['city' => 1], 'limit' => 5 ] ); ?>
<?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"; } ?>
Ces étapes 3 et 4 sont itératives (approche CI/CD)
Exemple : Un utilisateur a un profil
Document embarqué :
{ "_id": 1, "username": "johnny_depp", "profile": { "firstname": "Johnny", "lastname": "Depp", "age": 30 } }
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 }
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] }
Avantages :
Inconvénients :
Quand utiliser :
// 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
$exists
$type
$in
$nin
$all
$size
$elemMatch
$set
$unset
$inc
$pull
MongoDB est une base de données NoSQL puissante et flexible, particulièrement adaptée pour :
Points clés à retenir :
Pour aller plus loin :