MongoDB

Published: 20-12-2014

Updated: 22-05-2016

By: Maxime de Roucy

tags: database mongodb

Mes notes concernant la formation d’initiation à MongoDB donnée par Matthieu Baechler le 16/12/2014.

Environnent de test

Nous avons utilisé docker pour mettre en place l’environnement de test.

Installation et lancement de docker :

max@laptop % yaourt -S docker
max@laptop % usermod -a -G docker max
max@laptop % systemctl start docker

Lancement de l’image correspondant à mongodb 2.6. Comme elle n’est pas encore installée elle est téléchargée directement sur le registry de docker. On en profite pour téléchargé et partagé un fichiers de test dont nous aurons besoins.

max@laptop % mkdir temp
max@laptop % cd temp
max@laptop % wget "https://ftp.craoc.fr/pelican/movies.json"
max@laptop % docker run -d --name mongodb -v `pwd`:/test mongo:2.6
max@laptop % docker ps
CONTAINER ID        IMAGE               COMMAND                CREATED             STATUS              PORTS               NAMES
a3e5032c9d8b        mongo:2.6           "/entrypoint.sh mong   3 hours ago         Up 41 seconds       27017/tcp           mongodb

Obtention d’un shell mongo sur dans l’environnement de test :

max@laptop % docker exec -ti mongodb mongo
MongoDB shell version: 2.6.6
connecting to: test
Welcome to the MongoDB shell.
For interactive help, type "help".
For more comprehensive documentation, see
	http://docs.mongodb.com/
Questions? Try the support group
	http://groups.google.com/group/mongodb-user
>

Généralités

MongoDB est un base de donnée NoSQL. Les bases de données NoSQL contrairement au SGBD relationnel (MariaDB, PostgreSQL…) ne maintiennent pas de « relations » (e.g. impossible de faire des jointures) entres les données qu’elles contiennent. Les performances en sont grandement améliorées. Le traitement des données et les relations sont laissé au soin de l’application utilisant le SGBD.

Cassandra est une autre base de donnée NoSQL. Elle est très performante mais peu flexible.

MongoDB intègre un maximum de fonctionnalités et de flexibilité mais est moins performante. Son développement a commencé en 2009. Le moteur de MongoDB est écrit en C++.

MongoDB a la particularité d’intègrer le moteur JavaScript (JS) de Chrome « V8 ». De ce fait la plupart des fonctions (si ce n’est toutes ?) utilisé pour contrôler MongoDB sont écrite en JS.

Les données sont manipulées au format JSON. En interne, elle sont au format BSON (JSON binaire).

Le processus correspondant au SGBD est mongod. Le shell est mongo.

MongoDB possède des drivers permettant de l’interfacer avec presque tous les langages de programmations (certains langages possède même plusieurs drivers).

C’est une base orientée document. C’est à dire que tous les élément qu’elle contient doivent être des document JSON. Ces document doivent avoir une taille inférieur à 16Mo (dans leur représentation BSON). On doit bien avoir cette limitation en tête lorsqu’on créé l’architecture de notre base donnée.

Un document JSON se représente de la façon suivante :

{ "clé1" : "string",
  "clé2" : 123,
  "clé3"  : true,
  "clé4"  : false,
  "clé5"  : null,
  "clé6" : { "clé61" : "valeur", "clé62": 456 },
  "clé7" : ["aa", "bb"]
}

Un élément de la forme {…} est appelé un document dans la norme JSON.

Il n’est pas possible de faire des transaction au sens relationnel du terme (ie begin, commit…).

Propriété ACID d’une base : atomicité, cohérence, isolation, durabilité Concernant MongoDB :

Concernant l’atomicité, il est possible de demander une modification d’un document si sa version stockée en base correspond à un certain paterne. Et ce de façon atomique au niveau du document.

Concernant le théorème CAP, en général les base de donnée NoSQL supporte A et P (Availability & Partition Tolerance). MongoDB supporte C et P (Consistency & Partition Tolerance)

Dans un cluster MongoDB seul le maitre prend les requêtes de lecture et d’écriture. La « clusterisation » n’est utile que si le nœud maitre tombe, elle ne permet pas de passer à l’échelle (scalability).

Pour passer à l’échelle il faut utiliser la technique du « charding ». Une instance mongos permet de répartir les requêtes sur les différents « shard ». mongos a aussi besoins d’un serveur de configuration pour savoir sur quel « shard » router les différentes requêtes.

Une infrastructure complète devient donc vite très lourde étant données le nombre de serveur qu’elle requière.

Vocabulaire

« Base de donnée » (database, BDD) à la même signification que dans les SGBD relationnels.

À l’intérieur d’une BDD on trouve des collections, ce sont l’équivalent des « tables » des SGBD relationnels. Dans les collections se trouve des documents, ces documents peuvent ne rien avoir en commun, il peuvent contenir des données complètement différentes. Une requête porte au maximum sur une seul collection.

Les documents contiennent des attributs.

Tous les documents on au moins un attribut _id. Cette attribut est auto-généré leur de l’insertion du document dans une collection.

Cas pratiques

Utilisation de la BDD demo. Celle ci n’est pas encore créée. Elle le sera lorsque le premier document de la première collection sera créé.

> show databases
admin  (empty)
local  0.078GB
> use demo
switched to db demo
> show databases
admin  (empty)
local  0.078GB

Utilisation de la variable people pour désigner la collection db.people. De la même manière, cette collection ne sera créée que lors de la création de sont premier document. db est le point d’entré de tous les éléments des la BDD sur laquelle on travail (ici demo).

> people = db.people
demo.people

Pour l’instant la collection est vide.

> people.findOne()
null

Maintenant la base demo apparait dans la liste des BDD, ça ne veut pas dire qu’elle a été créée.

> show databases
admin  (empty)
demo  (empty)
local  0.078GB

Pour avoir plus d’information sur n’importe quelle fonction il est possible d’afficher son code JS en l’appelant sans argument et sans ().

> people.insert
function ( obj , options, _allow_dot ){
    if ( ! obj )
        throw "no object passed to insert!";

    …
    return result;
}

Insertion d’un document

> people.insert({firstname: "john"})
WriteResult({ "nInserted" : 1 })

La fonction find est l’équivalent du SELECT * dans les SGBD SQL.

> people.find()
{ "_id" : ObjectId("54903deaf8f96813c948199a"), "firstname" : "john" }

La fonction findOne récupère un seul élément et l’affiche au « format pretty ». L’élément afficher est sélectionné de façon non déterminé parmi les éléments correspondant à la requête. Il est aussi possible d’afficher les élément au « format pretty » et les passant à la fonction pretty().

> people.insert({firstname: "john", "lastname" : "do"})
WriteResult({ "nInserted" : 1 })
> people.find().pretty()
{ "_id" : ObjectId("54903deaf8f96813c948199a"), "firstname" : "john" }
{
	"_id" : ObjectId("54903ed4f8f96813c948199b"),
	"firstname" : "john",
	"lastname" : "do"
}
> people.insert({firstname: "david", "roles" : [ {"title": "leadDev", "xp" : 5}, { "title" : "manager" , "xp" : 1 } ]})
WriteResult({ "nInserted" : 1 })
> people.find().pretty()
{ "_id" : ObjectId("54903deaf8f96813c948199a"), "firstname" : "john" }
{
	"_id" : ObjectId("54903ed4f8f96813c948199b"),
	"firstname" : "john",
	"lastname" : "do"
}
{
	"_id" : ObjectId("54903fc2f8f96813c948199d"),
	"firstname" : "david",
	"roles" : [
		{
			"title" : "leadDev",
			"xp" : 5
		},
		{
			"title" : "manager",
			"xp" : 1
		}
	]
}

La fonction find retourne un curseur. La fonction pretty s’applique sur ce curseur. D’autre fonctions peuvent êtres chainées.

> people.find().limit(2)
{ "_id" : ObjectId("54903deaf8f96813c948199a"), "firstname" : "john" }
{ "_id" : ObjectId("54903ed4f8f96813c948199b"), "firstname" : "john", "lastname" : "do" }
> people.find().skip(2).limit(2)
{ "_id" : ObjectId("54903fc2f8f96813c948199d"), "firstname" : "david", "roles" : [ { "title" : "leadDev", "xp" : 5 }, { "title" : "manager", "xp" : 1 } ] }

Le premier argument de la fonction find est un document décrivant le filtre de recherche. Chaque attribut du document correspond à une condition. Les documents retournées par la fonction respect TOUTES ces conditions (fonction logique « ET » entre les attributs).

> people.find({"firstname":"john"})
{ "_id" : ObjectId("54903deaf8f96813c948199a"), "firstname" : "john" }
{ "_id" : ObjectId("54903ed4f8f96813c948199b"), "firstname" : "john", "lastname" : "do" }
> people.find({firstname:"john", lastname : "do"})
{ "_id" : ObjectId("54903ed4f8f96813c948199b"), "firstname" : "john", "lastname" : "do" }

Le deuxième paramètre de la fonction find correspond au filtre d’affichage.

> people.find({}, {roles: 1})
{ "_id" : ObjectId("54903deaf8f96813c948199a") }
{ "_id" : ObjectId("54903ed4f8f96813c948199b") }
{ "_id" : ObjectId("54903fc2f8f96813c948199d"), "roles" : [ { "title" : "leadDev", "xp" : 5 }, { "title" : "manager", "xp" : 1 } ] }

> people.find({}, {_id : 0, roles: 1})
{  }
{  }
{ "roles" : [ { "title" : "leadDev", "xp" : 5 }, { "title" : "manager", "xp" : 1 } ] }

> people.find({firstname:"john"}, {_id : 0})
{ "firstname" : "john" }
{ "firstname" : "john", "lastname" : "do" }

La fonction sort permet de trier les éléments en sortie.

> people.find().sort({firstname:1})
{ "_id" : ObjectId("54903fc2f8f96813c948199d"), "firstname" : "david", "roles" : [ { "title" : "leadDev", "xp" : 5 }, { "title" : "manager", "xp" : 1 } ] }
{ "_id" : ObjectId("54903deaf8f96813c948199a"), "firstname" : "john" }
{ "_id" : ObjectId("54903ed4f8f96813c948199b"), "firstname" : "john", "lastname" : "do" }
> people.find().sort({firstname:-1})
{ "_id" : ObjectId("54903deaf8f96813c948199a"), "firstname" : "john" }
{ "_id" : ObjectId("54903ed4f8f96813c948199b"), "firstname" : "john", "lastname" : "do" }
{ "_id" : ObjectId("54903fc2f8f96813c948199d"), "firstname" : "david", "roles" : [ { "title" : "leadDev", "xp" : 5 }, { "title" : "manager", "xp" : 1 } ] }

La fonction count retourne le nombre d’élément dans curseur sur lequel elle s’applique.

> people.find().count()
3
> people.count()
3

On peut supprimer des document d’une collection avec la fonction remove.

> people.insert({firstname: "james", "lastname" : "bond"})
> people.remove({"firstname" : "james"})
WriteResult({ "nRemoved" : 1 })

La fonction update permet de modifier un document. Le deuxième argument permet de spécifier comment modifier les attributs du document. Pour faire en sorte de update modifie plusieurs document (de manière non atomique) il faut lui passé un troisième argument ayant pour attribut multi:1.

> people.update({}, {version: 2})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> people.find()
{ "_id" : ObjectId("549044a1bc60161c3b439e7a"), "version" : 2 }
{ "_id" : ObjectId("549044a9bc60161c3b439e7b"), "firstname" : "john", "lastname" : "do" }
{ "_id" : ObjectId("54903fc2f8f96813c948199d"), "firstname" : "david", "roles" : [ { "title" : "leadDev", "xp" : 5 }, { "title" : "manager", "xp" : 1 } ] }
> people.update({}, {$set: {version: 3}}, {multi:1})
WriteResult({ "nMatched" : 3, "nUpserted" : 0, "nModified" : 3 })
> people.find()
{ "_id" : ObjectId("549044a1bc60161c3b439e7a"), "version" : 3 }
{ "_id" : ObjectId("549044a9bc60161c3b439e7b"), "firstname" : "john", "lastname" : "do", "version" : 3 }
{ "_id" : ObjectId("54903fc2f8f96813c948199d"), "firstname" : "david", "roles" : [ { "title" : "leadDev", "xp" : 5 }, { "title" : "manager", "xp" : 1 } ], "version" : 3 }
> people.update({firstname: "david"}, {$addToSet: {"roles": {"mongo" : "truc"}}}, {multi:1})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> people.find()
…
{ "_id" : ObjectId("54903fc2f8f96813c948199d"), "firstname" : "david", "roles" : [ { "title" : "leadDev", "xp" : 5 }, { "title" : "manager", "xp" : 1 }, { "mongo" : "truc" } ], "version" : 3 }

Pour la suite nous aurons besoins d’importer le fichier movies.json que nous avons inclue au conteneur dans la section « Environnent de test » (les erreurs d’import ne sont pas importante pour ce tuto). Nous allons utiliser une session bash dans le conteneur.

max@laptop % docker exec -ti mongodb bash
root@docker # mongoimport --db imdb --collection movies --file /test/movies.json
connected to: 127.0.0.1
exception:BSON representation of supplied JSON is too large: code FailedToParse: FailedToParse: Expecting '{': offset:0 of:[
exception:BSON representation of supplied JSON is too large: code FailedToParse: FailedToParse: Expecting '{': offset:0 of:]
2014-12-16T15:03:54.475+0000 check 9 350
2014-12-16T15:03:54.475+0000 imported 350 objects
encountered 2 error(s)s

Nous pouvons maintenant accéder à la collection movies dans la BDD imdb.

> show databases
admin  (empty)
local  0.078GB
imdb   0.078GB
> use imdb
switched to db imdb
> show collections
movies
system.indexes
> movies = db.movies
> movies.find()
…
> movies.find({rating_count: {$gt : 5000, $lt: 10000}})
…
> movies.find({$and : [{rating_count: {$gt : 5000, $lt: 10000}}, {year: 2012}]})
…
> movies.find({$and : [{rating_count: {$gt : 5000, $lt: 10000}}, {year: 2012}, {country : { $nin: ["USA"]}}]})
…

La création d’index peut se faire à chaud. La fonction explain permet d’afficher des information sur le déroulement d’une requête, en l’occurrence find.

> movies.find({$and : [{rating_count: {$gt : 5000, $lt: 10000}}, {year: 2012}]}).explain()
{
	"cursor" : "BasicCursor",
	"isMultiKey" : false,
	"n" : 1,
	"nscannedObjects" : 350,
	"nscanned" : 350,
	"nscannedObjectsAllPlans" : 350,
	"nscannedAllPlans" : 350,
	"scanAndOrder" : false,
	"indexOnly" : false,
	"nYields" : 2,
	"nChunkSkips" : 0,
	"millis" : 1,
	"server" : "df1909569ade:27017",
	"filterSet" : false
}
> movies.ensureIndex({rating_count:1})
{
	"createdCollectionAutomatically" : false,
	"numIndexesBefore" : 1,
	"numIndexesAfter" : 2,
	"ok" : 1
}
> movies.find({$and : [{rating_count: {$gt : 5000, $lt: 10000}}, {year: 2012}]}).explain()
{
	"cursor" : "BtreeCursor rating_count_1",
	"isMultiKey" : false,
	"n" : 1,
	"nscannedObjects" : 17,
	"nscanned" : 17,
	"nscannedObjectsAllPlans" : 17,
	"nscannedAllPlans" : 17,
	"scanAndOrder" : false,
	"indexOnly" : false,
	"nYields" : 0,
	"nChunkSkips" : 0,
	"millis" : 0,
	"indexBounds" : {
		"rating_count" : [
			[
				5000,
				10000
			]
		]
	},
	"server" : "df1909569ade:27017",
	"filterSet" : false
}

Il est possible de faire des index composés.

> movies.ensureIndex({rating_count:1, year: 1})
{
	"createdCollectionAutomatically" : false,
	"numIndexesBefore" : 2,
	"numIndexesAfter" : 3,
	"ok" : 1
}
> movies.getIndices()
[
	{
		"v" : 1,
		"key" : {
			"_id" : 1
		},
		"name" : "_id_",
		"ns" : "imdb.movies"
	},
	{
		"v" : 1,
		"key" : {
			"rating_count" : 1
		},
		"name" : "rating_count_1",
		"ns" : "imdb.movies"
	},
	{
		"v" : 1,
		"key" : {
			"rating_count" : 1,
			"year" : 1
		},
		"name" : "rating_count_1_year_1",
		"ns" : "imdb.movies"
	}
]

Il est possible de faire des recherche « full text » mais pour cela il faut explicitement créer les indexes correspondants. On utilise aussi la fonction find pour faire une recherche « full text ».

> movies.find({$text: {$search : "hobbit"}}, {title:1}).pretty()
error: {
	"$err" : "Unable to execute query: error processing query: ns=imdb.movies limit=0 skip=0\nTree: TEXT : query=hobbit, language=, tag=NULL\nSort: {}\nProj: { title: 1.0 }\n planner returned error: need exactly one text index for $text query",
	"code" : 17007
}

On pose l’index de type « text » sur l’attribut « plot_simple ». On utilise l’option « language_override » car certains « plot_simple » contiennent du texte dans une autre langue que l’anglais et pour lesquelles notre environnement de test ne dispose d’aucun parseur. Sans cette option mongo nous retournerai une erreur.

> movies.ensureIndex({plot_simple:"text"}, {language_override:"dummy"})
{
	"createdCollectionAutomatically" : false,
	"numIndexesBefore" : 3,
	"numIndexesAfter" : 4,
	"ok" : 1
}
> movies.find({$text: {$search : "hobbit"}}, {title:1}).pretty()
{
	"_id" : ObjectId("549049da1a26493362bd1b61"),
	"title" : "The Lord of the Rings: The Fellowship of the Ring"
}
{
	"_id" : ObjectId("549049da1a26493362bd1c0f"),
	"title" : "The Hobbit: An Unexpected Journey"
}