Mot clé C# dynamic

Permet d’introduire la notion de type dynamique en C#. Contrairement aux variables fortement typées, le type n’est pas connu à la compilation, il est déterminé à l’exécution en fonction de l’initialisation.

On peut utiliser dynamic pour des variables locales, dans la signature de fonctions et pour les membres de classe, en revanche on ne peut pas l’utiliser pour une classe ou pour un type d’opérateur.

L’intérêt de dynamic est de permettre d’utiliser un type qu’on ne connait pas à la compilation.

Comparaison avec "var"

Dynamic est différent de var car var sert juste à laisser le compilateur déterminer le type de la variable en fonction des initialisations mais le type reste déterminé à la compilation (et non à l’exécution comme pour dynamic).

Comparaison avec System.Object

Dynamic permet d’effectuer des opérations plus fines que system.object puisque on peut, par exemple, permettre d’utiliser des opérateurs en fonction du type de la variable:

class Program 
{ 
  static void Main(string[] args) 
  {
    // The dynamic variable gets the return  
    // value of a function call and outputs it. 

    dynamic x = DoubleIt(2); 
    Console.WriteLine(x); 

    // Stop and wait 
    Console.WriteLine(“Press any key”); 
    Console.ReadLine(); 
  } 

  // The function receives and returns a dynamic object  
  private static dynamic DoubleIt(dynamic p) 
  { 
    // Attempt to "double" the argument whatever  
    // that happens to produce 

    return p + p; 
  } 
}

Mot clé C# Volatile

Le mot clé volatile indique qu’un champ peut être modifié par plusieurs threads qui s’exécutent simultanément. Les champs qui sont déclarés volatile ne sont pas soumis aux optimisations du compilateur qui supposent l’accès par un seul thread. Cela garantit que la valeur la plus à jour est présente dans le champ à tout moment.

Rien ne garantit l’ordre d’exécution d’une instruction par rapport à une autre lorsqu’on effectue 2 opérations de lecture ou 2 opérations d’écriture sur des variables différentes. Après optimisation du compilateur, les instructions peuvent très bien être inversées. En effet une instruction dans le code équivaut à plusieurs instructions en langage machine. Ainsi dans le cadre d’une exécution du code par 2 threads séparées, l’exécution peut mener à des erreurs si on utilise les variables dans les 2 threads.

Pour palier à ce problème, on peut utiliser le mot clé VOLATILE qui permet de garantir l’ordre d’exécution d’une variable par rapport à une autre:
Dans le cas d’écritures, vous avez une écriture ordinaire suivie d’une écriture volatile, et une écriture volatile ne peut pas être réorganisée lorsqu’elle est précédée d’une opération de mémoire.
Dans le cas de lectures, vous avez une lecture volatile suivie d’une lecture ordinaire, et une lecture volatile ne peut pas être réorganisée lorsqu’elle est suivie d’une opération de mémoire.

L’exemple suivant permet d’illustrer une bonne utilisation du mot clé:

public class DataInit 
{ 
   private int _data = 0; 
   private volatile bool _initialized = false; 

   void Init() 
   { 
     _data = 42; // Write 1 
     _initialized = true; // Write 2 
   } 

   void Print() 
   { 
     if (_initialized) { // Read 1 
       Console.WriteLine(_data); // Read 2 
     } 
     else { 
       Console.WriteLine("Not initialized"); 
     } 
   } 
} 

D’une façon générale, il est fortement conseillé d’utiliser des classes du framework pour manipuler des mêmes variables dans des threads séparées:
Valeurs initialisées tardivement: Lazy<T>; LazyInitializer
Collections thread-safe: BlockingCollection<T>; ConcurrentBag<T>; ConcurrentDictionary<TKey,TValue>; ConcurrentQueue<T>; ConcurrentStack<T>
Primitives permettant de coordonner l’exécution de différents threads: AutoResetEvent; Barrier; CountdownEvent; ManualResetEventSlim; Monitor; SemaphoreSlim
Conteneur conservant une valeur séparée pour chaque thread: ThreadLocal<T>

D’une façon générale, les bonnes pratiques sont:
Évitez d’utiliser inutilement des champs volatiles: le plus souvent, des verrous ou des collections simultanées (System.Collections.Concurrent.*) conviennent mieux pour l’échange de données entre threads. Dans certains cas, des champs volatiles peuvent être utilisés pour optimiser du code simultané mais vous devriez utiliser des mesures de performance afin de valider le fait que l’avantage l’emporte sur le surcroît de complexité.
– Au lieu d’implémenter le modèle d’initialisation tardive vous-même en utilisant un champ volatile, utilisez les types System.Lazy<T> et System.Threading.LazyInitializer.
Évitez les boucles d’interrogation: vous pouvez souvent utiliser BlockingCollection<T>, Monitor.Wait/Pulse, des événements ou une programmation asynchrone à la place d’une boucle d’interrogation.
– Dès que cela est possible, utilisez les primitives de simultanéité .NET standards au lieu d’implémenter une fonctionnalité équivalente vous-même.

Formation MongoDB M101N: semaine 6 – Conception des applications

Durabilité des écritures

Dans un contexte classique, on a:

Application ⇔ driver ⇔ mongod ⇔ mongo Shell

Des opérations d’écritures sont faites par l’application et par le mongo shell. Pour obtenir les erreurs:
– dans le cas du mongo Shell, on les a tout de suite,
– dans le cas de l’application, ces erreurs sont gérées par le driver.

On peut aussi obtenir les erreurs en appelant "getLastError".

"getLastError" comporte 2 paramètres:
w: permet d’indiquer qu’on souhaite recevoir un acknowladgement quand on a écrit en base. Cette acknowladgement ne certifie pas que l’écriture a été effectuée sur le disque mais qu’elle se trouve en mémoire. En cas de coupure d’alimentation l’écriture peut être perdue.
j: indique que l’écriture a été rajoutée dans le journal de log. Dans le cas de cette valeur, on est sûr que l’écriture s’est bien passée et qu’elle se trouve en base.

Différents cas sont possibles:

w=0 et j=0: mode “fire and forget”: on a aucune assurance que l’écriture s’est bien passée. C’était la valeur par défaut jusqu’à la version 3.0.
w=1 et j=0: mode “safe”, dans la plupart des cas l’écriture s’est bien passée. C’est la mode par défaut à partir de la version 3.
w=1 et j=1: mode “commit to journal”: l’écriture est garantie.
w=0 et j=1: même comportement que mode précédent.

Tous ces modes s’appellent le “Write concern” et sont gérés par le driver.

Problèmes réseau

Même dans le mode “commit to journal”, on peut ne pas recevoir l’indication de l’écriture dans le journal en cas de problème réseau. Dans ce cas, l’écriture peut avoir été faite en base mais on ne reçoit pas le signal indiquant l’écriture dans le journal.

Réplication

L’architecture classique de mongo DB est le “replica set”. Il est composé de minimum 3 bases de données autonomes. L’un des nœuds est le primaire “primary” et les autres sont secondaires “secondary”. L’application via le driver effectue les opérations d’écriture systématiquement sur le primaire.

Si le primaire n’est plus accessible, les 2 autres nœuds élisent un primaire qui recevra les nouvelles opérations d’écriture. Si la nœud défaillant redevient opérationnel, il deviendra un secondaire.

Il faut un minimum de 3 nœuds pour permettre l’élection d’un nouveau primaire.

Types de nœud dans un “replica set”

4 types différents:
Regular: nœud normal pouvant être primaire ou secondaire et ayant un droit de vote.
Arbitrer: il ne contient pas de données mais dans le cas où on a que 2 machines, il permet d’avoir un nombre impair pour effectuer les votes. Il peut donc être hébergé sur la même machine qu’un “regular”.
Delayed/regular: il s’agit d’une machine n’ayant pas les mêmes performances qu’un “regular”. Il peut servir à dépanner un “regular” défaillant. Il contient des données et a un droit de vote mais il ne peut pas devenir primaire. Sa priorité est nulle.
Hidden: il contient des données aussi mais il ne peut pas devenir primaire. Il possède un droit de vote. Sa priorité est nulle.

Consistance en écriture

Par défaut l’application effectue des opérations d’écriture et de lecture sur le nœud primaire seulement. Ceci permet d’avoir une forte consistance des données après l’écriture. Ainsi si on lit les données tout de suite après l’écriture, les valeurs correspondront à l’écriture.

Il est possible de permettre les lectures sur les nœuds secondaires mais sachant que la réplication des données est asynchrone, une lecture immédiate après l’écriture ne certifie pas d’avoir des données consistantes.

Création d’un “replica set”

A partir du mongo Shell:

mongod --replSet rs1 --logPath "1.log" --dbPath /data/rs1 --port 27017 --fork

"rs1" est juste le nom du “replica set”.

On indique un port dans ce cas pour différencier les serveurs dans le cas où le “replica set” se trouve sur la même machine.

"fork" permet d’afficher certaines informations sur le mongo Shell.

Pour créer les autres serveurs, il suffit de renouveler la commande avec des ports différents:

mongod --replSet rs1 --logPath "2.log" --dbPath /data/rs1 --port 27018 --fork 
mongod --replSet rs1 --logPath "3.log" --dbPath /data/rs1 --port 27019 --fork

A ce moment les serveurs ne sont pas connectés entre eux.

Pour connecter les serveurs entre eux:

config = { _id : "rs1", members : [ 
{ _id : 0, host : "machine:27017", priority: 0 } 
{ _id : 1, host : "machine:27018" } 
{ _id : 2, host : "machine:27019" } 
]}

rs.initiate(config) 
rs.status()

Le nœud "0" ne pourra pas devenir primaire à cause de "priority:0".

"rs.status()" n’est pas nécessaire pour la création du “replica set” mais affiche seulement le statut.

Pour passer d’un serveur à l’autre, il suffit d’écrire:

mongo --port 27017

Pour autoriser les opérations de lecture sur un secondaire:

rs.slaveOk()

Pour savoir si le serveur sur lequel on est connecté est le primaire:

rs.isMaster()

La réplication des modifications du primaire sur les secondaires se fait par l’intermédiaire de la table "oplog.rs" qui contient un timestamp. Ainsi les secondaires interrogent le primaire régulièrement pour savoir quelles sont les dernières modifications en fonction du timestamp, il peuvent ensuite répliquer les modifications.

Remarque

La réplication supporte des moteurs de stockage différents (storage engine) entre primaire et secondaire.

Failover et rollback

Si un primaire devient défaillant dans un “replica set”, un secondaire sera élu en tant que primaire. Durant le laps de l’élection et de la défaillance, des écritures ont pu être effectué dans le primaire initial.

Si ce primaire redevient opérationnel, il restera secondaire mais il peut contenir des opérations d’écriture que le nouveau primaire n’a pas. Dans ce cas, le primaire va effectuer un rollback et écrire ces opérations dans un fichier pour qu’elles soient éventuellement exécutées manuellement.

Durant le laps de temps du failover, les écritures méneront à des erreurs.

Driver et failover

Lorsqu’une application est connectée à un primaire, si ce primaire devient défaillant puis redevient opérationnel, il aura perdu son statut de primaire. L’application peut entraîner une exception si elle effectue une écriture sur cet ancien primaire.

Par la suite il faudra que l’application change de serveur pour continuer ces opérations d’écriture. Ces mécanismes sont gérés par le driver mais les blocs de code doivent contenir des Try...Catch pour prévenir de ces exceptions.

“Write concern” dans le cas d’un “replica set”

Il est possible de paramétrer la valeur de "w" différemment dans le cas d’un “replica set”:
w=1: on attends l’acknowledgment du primaire,
w=2: on attends l’acknowledgment d’au moins un secondaire,
w=3: on attends l’acknowledgment de tous les secondaires.

En paramétrant une valeur de "w" correspondant à la majorité, on évite la plupart des cas de rollback puisque si la valeur est écrite au moins sur 2 serveurs, on peut imaginer qu’elle ne sera pas rollbacker.

Il est possible de paramétrer une valeur de "w" au niveau de la connexion, de la collection ou du “replica set”.

Il n’est pas possible de paramétrer une valeur différente de 0 ou 1 pour "j".

Remarque

Si w=1 et j=1, on a pas la garantie que des rollbacks ne vont pas se produire car une écriture peut ne pas avoir été faite sur le secondaire lors de la défaillance du primaire.

Préférence en lecture

Au niveau du driver d’une application, il est possible d’indiquer des préférences pour définir la façon dont les données seront lues:
PRIMARY: les lectures ne se font que sur le primaire,
SECONDARY: on effectue les lectures seulement sur un secondary. Si il est indisponible, on ne pourra plus lire.
SECONDARY_PREFERRED: la lecture sera effectuée de préférence sur des secondaires.
PRIMARY_ PREFERRED: la lecture sera effectuée de préférence sur des primaires.
NEAREST: on effectuera les lectures sur le secondaire qui est le plus proche (i.e. Pong le plus court).
TAGGING: on peut ajouter des tags pour indiquer sur quel serveur primaire ou secondaire, on va effectuer les lectures.

Implication de la réplication

– Liste des nœuds: le plus souvent géré par le driver.
– Write concern: valeurs de "w", "j" et du timeout de "w".
– Préférences en lecture
– Des erreurs peuvent survenir, il faut protéger la lecture et l’écriture des données par des blocs TRY...CATCH pour anticiper les erreurs.

Remarque

Si on ne précise pas de timeout de "w", dans le cas où w > 1, on peut attendre une très longue période avant d’avoir une réponse.

Sharding

Pour augmenter les performances et la scalabilité d’une base, il est possible de diviser un environnement Mongo en plusieurs éléments: les “shards”. Chaque “shard” est un “replica set” (composé donc d’au moins 3 serveurs).

Il sera donc possible de diviser une collection en plusieurs parties et chaque partie sera hébergée sur un “shard” différent. Les parties de la collection s’appelle les “chunks”.

Lorsqu’une requête est effectuée sur un environnement composé de “shards”:
– l’application doit se connecter un serveur Mongos au lieu de se connecter à un serveur Mongod. C’est Mongos qui va savoir quel “shard” doit être requêté.
– si on utilise le mongo Shell, il devra être connecté directement au Mongos.
– les documents sont rangés dans les “shards” en fonction de la valeur du “shard key” qui correspond à la clé primaire de la collection. Mongos va se charger lui-même de repartir les documents équitablement sur chaque “shard”.
– sachant que les “shards” sont interrogés en fonction de la valeur du “shard key”, toutes les requêtes doivent comporter le “shard key” lorsqu’une collection est ainsi divisée.
– le “shard key” doit posséder un index.

Mise en œuvre d’un environnement comportant des “shards”

La configuration basique d’un environnement avec des “shards” doit comporter au minimum:
– 2 “shards” composé chacun d’un “replica set”
– chaque “replica set” doit être composé d’au moins 3 serveurs mongod.
– il doit y avoir au moins 3 instances de mongod pour effectuer de la configuration. Ces instances peuvent être exécutées sur la machine qu’un des “replica sets”.

Ainsi une configuration minimum comporte au moins 9 instances de mongod qui s’exécutent: 3 par “replica set” et 3 pour les instances de configuration.

Pour mettre en place un “shard”, il faut créer un “replica set” et préciser qu’il s’agit d’un “shard”:

mongod --replSet rs1 --logPath "1.log" --dbPath /data/shard0/rs0 --port 27017 --fork --shardsvr 
mongod --replSet rs1 --logPath "2.log" --dbPath /data/shard0/rs1 --port 27018 --fork --shardsvr 
mongod --replSet rs1 --logPath "3.log" --dbPath /data/shard0/rs2 --port 27019 --fork --shardsvr

config = { _id : "s0", members : [ 
{ _id : 0, host : "machine:27017" } 
{ _id : 1, host : "machine:27018" } 
{ _id : 2, host : "machine:27019" } 
]} 

rs.initiate(config)

"--shardsvr" permet d’indiquer que le replica set doit faire partie d’un “shard”.

On renouvelle l’opération pour le 2ème “shard”.

On crée ensuite les 3 serveurs de configuration:

mongod --logpath "cfg-a.log" --dbpath /data/config/config-a --port 57017 --fork --configsvr 
mongod --logpath "cfg-b.log" --dbpath /data/config/config-b --port 57018 --fork --configsvr 
mongod --logpath "cfg-c.log" --dbpath /data/config/config-c --port 57019 --fork --configsvr

"--configsvr" permet d’indiquer qu’on souhaite créer des serveurs de configuration.

On démarre le serveur mongos en incluant les serveurs de configuration:

mongos --logpath "mongos.log" --configdb machine:57017,machine:57018,machine:57019 --fork

On ajoute les “shards” et on ajoute la collection au “shards”:

db.adminCommand( { addshard: "s0/machine:37017" } ); 
db.adminCommand( { addshard: "s1/machine:47017" } );
db.adminCommand( { enableSharding: "test" } );

"test" est le nom de la base de données.

db.adminCommand( { shardCollection: "test.grades", key: { student_id : 1 }} );

"test.grades" est le nom de la collection,
"student_id" est le nom de la shard key.

Si on utilise le mongo shell, il faut ensuite se connecter au mongos et non à un serveur mongod.

Quand on effectue une requête, en utilisant ".explain()" on peut voir sur quel “shard” la requête a réellement été exécutée.

sh.status() permet d’indiquer le statut de l’environnement de “shards”.

Implications du “sharding”

– Tous les documents doivent inclure la “shard key”.
– La “shard key” est non modifiable.
– Tous les index doivent commencer par la “shard key”. Ils peuvent comporter d’autres champs mais le premier champ doit forcément être la “shard key”.
– Les requêtes d’update doivent comporter la “shard key” ou doivent être “multi”.
– Si dans une requête, on ne précise pas la “shard key”, tous les “shards” seront requêtés, ce qui est très coûteux.
– Pas d’index unique à moins qu’il comporte la “shard key” car il n’est pas possible de requêter tous les “shards” pour vérifier l’unicité.
– La “shard key” ne peut être un index “multi-key” (portant sur des tableaux par exemple).

Remarque

La “shard key” ne doit pas forcément être unique.

Sharding et réplication

Tout ce qui a été énoncé auparavant concernant les “replica sets” est valable dans un environnement comportant des “shards”:
– Par défaut, mongos effectue les requêtes sur les serveurs primaires de chaque “shard”. Il est possible d’autoriser les requêtes en lecture vers les serveurs secondaires.
– Les éléments de “write concern”: "w", "j" et le timeout "w" définissable au niveau de l’application sont aussi appliqués à chaque “shard”. Ainsi l’obtention d’un acknowladgement au niveau d’un “shard” se fera si on a l’acknowledgment au niveau du “replica set” du “shard”.
– Enfin il est possible d’utiliser plusieurs instances de mongos pour éviter les interruptions de service dans le cas de la défaillance d’une instance.

Sélection d’une “shard key”

– Sachant que la “shard key” n’est pas modifiable, il faut la choisir correctement à la crearion de l’environnement.
– La “shard key” doit comporter suffisamment de cardinalité pour permettre une bonne répartition des documents sur les différents “shards”. Si la cardinalité est insuffisante, un “chunck” pourrait comporter plus de documents qu’un autre.
Hotspotting: intervalle de valeur de la “shard key” doit rester le plus stable possible. Si cet intervalle augmente trop souvent, le “shard” contenant les valeurs maximales sera utilisé plus souvent que les autres. La réparation de charge ne sera donc pas équitable entre tous les “shards”. Ce problème peut se produire si on choisit la date par exemple.

Formation MongoDB M101N: semaine 4 – Performance

Fonctionnement d’un index

Une table est rangée comme une liste de valeurs rangée les unes à la suite des autres. Lorsqu’on effectue une requête sur cette liste, si il n’y a pas d’index, on va parcourir toute la liste pour trouver les valeurs. Le parcours de toute la liste correspond à un “table scan” pour une base relationnelle et à une “collection scan” pour une base MongoDB.

Pour remédier à ce problème, on utilise un index qui est un annuaire vers les valeurs en base. Si on crée un index sur une valeur triable d’une collection alors on pourra construire un arbre (B tree) de valeurs classées par ordre alphabétiques qui vont pointer vers la valeur dans la collection. Le parcours de l’arbre est rapide et donc on peut trouver plus rapidement les valeurs dans la collection.

Si on crée un index composite sur 3 valeurs de la collection alors on crée un annuaire pour la valeur 1. Pour chaque valeur 1, on crée un 2ème annuaire de valeurs 2. Et pour chaque valeur 2, on crée un 3ème annuaire de valeurs 3. Ainsi si on fait une recherche sur la valeur 1 seulement, il suffit de consulter le 1er annuaire. En revanche si on fait une recherche sur la valeur 2 seulement, on ne pourra pas utiliser l’index puisque les annuaires de valeurs 2 dépendent de celui de la valeur 1.

Ainsi si on considère un index sur les valeurs "a", "b" et "c" d’une collection et si on effectue une requête sur:
"a" seul alors l’index sera utilisé
"b" seul ou "c" seul, l’index ne sera pas utilisé.
"a" et "b", l’index sera utilisé.
"a", "b" et "c" dans cet ordre alors l’index sera utilisé.
"a" et "c" on ne pourra utiliser l’index que pour "a".

Remarques

– Les requêtes "find", "findOne", "update", "remove" et "sort" utilisent un index.
– Par défaut, pour toutes les tables un index est créé sur "_id".
– Si on crée un index sur des valeurs dans le sens ascendant, dans le cas d’une requête dans le sens descendant, l’index ne sera pas utilisé.

Remarques:

Pluggable storage engines

Il est possible de paramétrer un “storage engine” différent de celui par défaut (à partir de la version 3.0):

Driver ⇔ MongoDB server ⇔ Storage engine ⇔ Disks

Plusieurs “storage engine” sont supportés:
– MMAP
– WiredTiger

Les “storage engines” affectent le format du fichier de données et le format des index.

Le storage engine n’affecte pas la communication entre plusieurs serveurs MongoDB ni l’API à disposition des programmeurs.

MMAPv1

MMAP permet de gérer les données qui sont chargées dans la mémoire virtuelle. Il se base sur le système de fichier MMAP qui mappe les fichiers en mémoire.

Ainsi un fichier de données de grande taille (supérieure à 100GB) sur le disque sera chargé de façon paginée. Les pages seront chargées en mémoire virtuelle en fonction de leur utilisation: à la première utilisation, ils sont chargés.

Collection Level locking

MMAP permet de gérer la concurrence des collections en faisant du “collection level locking”. Si 2 opérations d’écriture sont effectuées au même moment dans une même collection, l’une d’entre elles doit attendre que l’autre a terminée, il peut y avoir plusieurs lecteurs mais un seul écrivain à la fois. Si des collections différentes sont affectées alors l’écriture peut se faire au même moment.

Inplace Updates

Une autre fonctionnalité de MMAP est de bouger les collections d’une page à l’autre en mémoire virtuelle en fonction de leur taille. Ainsi si une page est trop petite pour une collection alors la collection sera déplacée vers une page plus grande.

Power of two sized allocations

Pour éviter de déplacer les collections quand elles grossissent, on place les collections dans des pages plus grandes.

On utilise des puissances de 2 pour savoir la taille des pages:

Par exemple une collection de 3gb sera stockée dans une page de 4gb; une collection 19gb sera stockée dans une page de 32gb.

Plan d’exécution

Si il existe 3 index sur une collection, quand on va effectuer une requête sur cette collection et qu’il est possible d’utiliser les 3 index, MongoDB va exécuter utiliser séparément les 3 index pour effectuer la requête. MongoDB gardera ensuite en memoire l’index qui a permis l’exécution la plus rapide de la requête. Ainsi pour les exécutions suivantes du même type de requête, MongoDB choisira systématiquement l’index qui permet l’exécution la plus rapide.

Manipulations des index

Création d’un index sur une table

db.table.createIndex({ a: 1, b:1,c:1})

Permet de créer un index sur 3 colonnes d’une table.

Le "1" indique que l’on veut construire l’index dans le sens ascendant.

Obtenir le plan d’exécution d’une requête

QueryPlanner

[requete].explain()

Les valeurs affichées sont:
cursor: permet d’indiquer le type de curseur utilisée pour afficher les résultats. "BasicCursor" signifie qu’aucun curseur n’a été utilisé pour afficher le résultat. Dans le cas où un curseur est utilisé, cette donnée indique le nom du curseur utilisé.
millis: nombre de millisecondes nécessaires pour exécuter la requête.
n: indique le nombre de documents retournés.
nScannesObjects: nombre de documents scannés pour afficher le résultat
nScanned: nombre de lignes de l’index scannées et le nombre de documents scannés pour afficher le résultat.
indexBounds: intervalles de l’index qui ont été parcourus.
indexOnly: booléen indiquant si l’index seul a suffit pour afficher les résultats. Si faux alors il a été nécessaire de parcourir les documents pour trouver le résultat.
winningPlan: permet d’indiquer quel est le plan qui a permis de retourner les résultats.

Il existe d’autres modes: "executionStats" et "allPlansExecution".

ExecutionStats

[requete].explain("executionStats")

On peut voir les options du "queryPlanner" mode.
On a en plus quelques statistiques “executionStats”:
"nReturned": nombre de documents retournés.
– Différentes étapes d’exécution de la requète “stage”.
"executionTimeMillis": temps d’exécution.

Les étapes permettent de savoir précisemment les index qui sont utilisés.

AllPlansExecution

[requete].explain("allPlansExecution")

Permet d’exécuter tous les plans d’exécutions en utilisant les différents index pour savoir quel est celui qui est le plus rapide.

Les informations spécifiques à ce mode sont dans “allPlansExecution”.

Avec ce mode, certains plans peuvent ne pas retourner de résultat en fonction de l’index qui est utilisé.

Lister les indexes sur un document

db.table.getIndexes()

Lister tous les indexes d’une base

db.system.indexes.find()

Supprimer un index sur une table

db.table.dropIndex({ "student_id": 1}

Le nom de l’index doit exactement celui affiché avec la valeur "key" quand on écrit db.table.getIndexes().

Unique index

Permet de rajouter une contrainte d’unicité sur les valeurs de l’index. Dans le cas où un unique index est appliqué, toute insertion de valeurs déjà présentes dans la base conduira à une erreur correspondant à une clé dupliquée.

Création d’un unique index

db.table.ensureIndex( { name: 1}, { unique: true })

Afficher les index de la table

db.table.getIndexes()

On verra que l’unique index comporte une valeur "unique" : true. Cette valeur permet d’indiquer que l’index contraint l’unicité.

Remarque

Pour le champ "_id" l’index n’est pas affiché comme étant unique pourtant il contraint bien le champ "_id" à être unique.

Index multiclé (multikey indexes)

Les index composites (c’est-à-dire composés de plusieurs champs) ne peuvent pas contenir plusieurs champs contenant des valeurs sous forme de tableaux. Il est seulement possible d’avoir un champ qui comporte des valeurs sous forme de tableau.

Ainsi une collection possède un seul document avec les champs:

Person: 
    Names [andrew, benson] 
    Hobby: [photo, hiking, golf] 
    Location: NY 
    Favorite_Color:

Il sera possible de créer un index sur Names et Location ou Hobby et Favorite_Color mais il sera impossible de créer un index sur Names et Hobby.

Si on crée un index sur hobby et location, les valeurs dans l’index seront:

    photo et NY 
    hiking et NY  
    golf et NY 

Ainsi on fait un produit cartésien entre les valeurs (c’est la raison pour laquelle les index multiclé avec des tableaux pour toutes les clés sont interdits puisqu’il y aurait trop de valeurs possibles dans le produit cartésion).

Si un index composite existe déjà pour names et location et si on ajoute un document avec location sous forme de tableau, on aura un message d’erreur indiquant qu’il n’est pas possible d’ajouter la valeur à cause de l’index.

On peut voir qu’un index est multiclé en utilisant "explain". Le champ utilisé est "isMultiKey".

Index background

2 façons de créer des index "foreground" et "background":

Création "foreground":
– Méthode par défaut,
– Rapide,
– Bloque les lecteurs et les écrivains qui essaient d’accéder à la base,
– Ne pas utiliser en production

Création "background":
– Lente,
– Ne bloque pas les lecteurs et écrivains lors de la création.

Dans le cas d’un “replica set” composé de plusieurs serveurs, il est possible de créer l’index successivement sur tous les serveurs. Par exemple, sur le serveur sur lequel on veut créer l’index, on l’enlève du “replica set”; on répartit la charge sur les autres serveurs; on crée l’index; on réintégre le serveur dans le “replica set”. Et ainsi de suite pour les autres serveurs.

Création d’un index “background”

db.students.createIndex({ name:1 }, { background : true })

Covered query

Requête dont le résultat utilise exclusivement un index.

Ainsi si on regarde le plan d’exécution d’une requête et qu’on s’aperçoit que le nombre de documents parcourus "totalDocsExamined" > 0 alors il ne s’agit pas d’une “covered query”.

Remarque

L’utilisation totale d’un index dépend des champs qui sont affichés par la requête. Si on affiche des champs non utilisés par un index alors MongoDB est obligé d’aller scanner la collection pour obtenir les autres champs.

Taille des index

Il est possible de voir la taille des index utilisés pour une collection en faisant:

db.table.stats()

Les champs "totalIndexSize" et "indexSize" permettent de donner la mémoire utilisée pour les index.

Il est important que les index puissent être chargés dans le “memory set” (partie chargée dans la mémoire plutôt que sur le disque) pour éviter trop d’aller-retour avec le disque.

Remarque

“WiredTiger” compresse les données et ainsi il est possible de réduire la taille des index en mémoire.

Cardinalité

Comparaison entre le nombre de valeurs dans l’index et le nombre de valeurs des champs:
– Regular index: 1:1
– Sparse index: ≤ documents
– Index multikey: > documents

Hint

Permet de forcer l’utilisation d’un index en particulier. On va l’utiliser à ajoutant ".hint" à la suite d’une requête et en indiquant le nom de l’index:

Par exemple:

db.table.find({ a :500, b:500}).hint({a:1})
Attention

Dans le "hint" on indique bien le nom de l’index tel qu’il apparaît avec "getIndexes".

Si on ne veut pas utiliser d’index particulier et qu’on souhaite effectuer un scan de la collection:
On utilisera la fonction "$natural":

db.table.find({ a :500, b:500}).hint({$natural:1})

Sparse index

A la création d’un index, il est possible d’indiquer que l’on souhaite que cette index porte exclusivement sur les documents ou la clé de l’index existe. Ainsi tous les documents ne contenant pas la valeur, ne seront pas indexés.

La création de ce type d’index se fait comme suit:

db.table.createIndex( { name: 1}, { sparse: true })

Ainsi si on utilise “hint” sur cet index, alors les résultats ne comporteront que des documents où le champ "name" existe. Évidement, la même requête sans la fonction "hint" retournerait des résultats.

Full text search index

Permet d’indexer un document contenant un champ contenant avec une chaine de caractères de grande taille.

Création d’un index “full text search”:

db.table.createIndex({ "words": "text"})

"words" est le nom du champ.

Chercher dans une chaine en utilisant un index “full text search”:

db.table.find({ $text: { $search: "chaine de caractères" } })

Même si on écrit plusieurs mots dans la chaine de caractères, la requête va retourner un résultat dès qu’il y a au moins un mot comment entre la chaine de caractères et la valeur d’un champ dans un document.

Performance des index

Pour certains opérateurs, les index se révèlent particulièrement inefficaces. Par exemple pour les opérateurs: $gt, $lt, $ne et $regex. Lorsque ces opérateurs sont utilisés dans les premières conditions d’une requête "find", il est préférable d’imposer l’utilisation d’un index sur les autres champs de la requête n’utilisant pas ces opérateurs (en utilisant "hint").

Par exemple:

db.students.find({ student_id: { $gt: 500}}, { class_id: 20})

Par défaut l’index sur "student_id" sera utilisé. Pour être plus efficace, il faudrait imposer l’utilisation de l’index sur "class_id".

L’élément le plus important dans l’efficacité d’un index est sa sélectivité c’est-à-dire sa capacité à minimiser la quantité de documents ou de clés qui ont été scannés.

D’une façon générale, il faut privilégier la sélectivité des valeurs dans cet ordre: égalité > ordonnancement (utilisation de "sort") > sélection d’un intervalle.

Profiler

Il existe 3 niveaux de profiling:
0: la fonctionnalité est désactivée,
1: seules les requêtes lentes sont logguées.
2: toutes les requêtes sont logguées.

Pour lancer le serveur dans un mode particulier de profiling:

mongod --profile 1 --slowms 2

Ce paramétrage signifie que le niveau utilisé sera "1" et que les requêtes seront logguées lorsqu’elles durent plus de 2 millisecondes.

Pour consulter les logs de profiling:

db.system.profile.find()

Les champs du résultat sont:
"ts": timespamp c’est l’heure à laquelle la requête a été effectuée.
"query": la requête qui été effectuée.
"nscanned": nombre d’objets scannés pour afficher le résultat.
"nreturned": nombre de documents retournés
"millis": temps en millisecondes de la requête.
"ns": namespace, c’est la table sur laquelle la requête a été exécutée: base et nom de la table.

Par exemple:
Pour obtenir les logs comportant un namespace particulier ordonné par timespamp:

db.system.profile.find({ ns: /test.foo/}).sort({ts:1}).pretty()

Pour obtenir les requêtes qui ont duré plus d’un certain temps:

db.system.profile.find({ millis: { $gt: 1 }}).sort({ts:1}).pretty()

Pour obtenir le niveau de profiling courant:

db.getProfilingLevel()
db.getProfilingStatus()

Permet d’obtenir un document comportant les champs:
"was": niveau de profiling
"slowms": durée à partir de laquelle les logs sont faits.

Pour paramétrer un nouveau niveau de profiling ou changer la durée:

db.setProfilingLevel(1,4)

pour niveau 1 et une durée de 4 millisecondes.

Index geospatial

C’est un type d’index qui permet de référencer des points sur un plan (espace à 2 dimensions).

Pour qu’il fonctionne, il faut:
– un champ "location" dans le document avec des coordonnées (x,y) dans un tableau: location : [ x, y]
– de créer l’index: createIndex({ "location" : '2d', type: 1 }). "2d" permet de spécifier le type d’index geospatial et type 1 permet de préciser le sens ascendant.
– d’effectuer des requêtes en utilisant l’opérateur $near:
find({ location: { $near: [x,y]}}) ce qui permettra de trouver les autres points dans la base situé à côté du point (x,y) du plus proche au plus éloigné. On pourra rajouter ".limit(20)" pour limiter le nombre de points à 20 par exemple.

Sharding

C’est une architecture qui permet de multiplier les serveurs sur lesquels une requête sera effectuée pour augmenter les performances. Ainsi l’application va communiquer avec un serveur “mongos” qui va communiquer avec plusieurs “replica sets”:

Chaque “replica set” comprend plusieurs bases qui sont répliquées cependant il s’agit d’un seul serveur “mongod”. Les “replica sets” contiennent des données différentes en fonction de la “shard key”.

Ainsi une même table peut être divisée sur plusieurs “replica sets”. Cette table comprend un champ “shard key” qui va permettre de savoir sur quel “replica set” le document sera réellement stocké, le “shard key” pourrait être la clé primaire ou le champ “_id” de la table. Le mécanisme est transparent pour l’application qui ne sait pas sur quel “replica set” une requête sera effectuée. Le serveur “mongos” exécutera la requête en fonction de la valeur du “shard key”. Si la requête ne comporte pas de “shard key” alors le serveur “mongos” effectuera la requête sur tous les “replica sets”.

Mongotop

Permet de faire du profiling pour une application. Il suffit d’écrire à la ligne de commandes:

mongotop [nombre de secondes d'exécution]

Mongostat

Permet d’avoir le nombre de "insert", "query", "update" et "delete" sur une base mongoDB pendant 1 seconde. Il suffit de taper à la ligne de commandes:

mongostat

"flushes": est le nombre d’écritures sur le disque,
"mapped" est la quantité de mémoire mappée,
"ar/aw" est le nombre de lecture/écriture,
"% used" pourcentage de mémoire utilisée,
"res": resident size.

Formation MongoDB M101N: semaine 5 – Aggregation framework

Permet de faire des requêtes proches de $group.

La fonction "aggregate" fonctionne en utilisant des “pipes” comme une ligne unix. Chaque étape du pipeline correspond à des étapes ou “stage”. Il n’y a pas d’ordre imposé pour les étapes, elles peuvent se succéder dans n’importe quel ordre.

Par exemple, les étapes peuvent être $match, $sort, $group etc. Ainsi le pipeline est indiqué par [ ] et les étapes sont séparées par des “,”:

db,collection.aggregate( [  
    { $match : { ... } }, 
    { $group : { ... } }, .... 
] )

Quelques exemples d’étapes:
$project: permet de sélectionner des clés en particulier et de modifier l'aspect d'un champ. La cardinalité est 1:1
$match<: permet de filtrer. Cardinalité n:1
$group: permet d’effectuer des agrégations. Cardinalité n:1
$sort: permet d’ordonner, cardinalité 1:1
$skip: pour sauter plusieurs documents dans les résultats, cardinalité n:1
$limit: pour limiter le nombre de documents dans le résultat, cardinalité n:1
$unwind: permet de convertir un tableau en plusieurs documents, cardinalité 1:n.

Étape $group

Explication du fonctionnement d’une requête d’agrégation:

Par exemple:
Si on prend un exemple avec $group:

db.products.aggregate( [  
{
    $group : 
    { 
        _id: "$manufacturer", 
        num_products: { $sum: 1} 
    } 
} 
] )

Cette requête permet de grouper des produits puis de compter en utilisant la fonction "$sum" par manufacturier.
Pour le champ: _id on utilise un "$" avant le nom du champ “manufacturer” parce qu’on veut que le champ soit valué quand tous les documents seront parcourus.
Le résultat de la requête sera un autre document avec les deux champs "_id" et "num_products".

Pour construire les résultats, la fonction va parcourir tous les documents et construire de nouveaux documents en y ajoutant les champs et en effectuant des "upsert" successifs pour additionner tous les produits.

$group avec _id:null

Il est possible d’effectuer une étape “$group” sans éléments à grouper en utilisant "_id:null".

Ainsi si on veut compter le nombre de documents dans une collection:

db.tables.aggregate([ 
    { $group: { _id: null, count: { $sum: 1 } }} 
])

Si on veut sommer un champ dans des documents:

db.tables.aggregate([ 
    { $group: { _id: null, total: { $sum: "$value" } }} 
])

Agrégation avec plusieurs clés

Pour avoir un clé composée de plusieurs champs c'est-à-dire grouper par plusieurs champs:

db.products.aggregate( [  
{ 
    $group : 
    { 
        _id: { "maker": "$manufacturer", "cat": "$category" }, 
        num_products: { $sum: 1} 
    } 
} 
] )

Dans les documents du résultat, le clé sera formée de 2 valeurs "maker" et "cat". Ces 2 valeurs ne sont pas présentes dans la collection d’origine, ils sont justes utilisés pour former le document contenant les résultats. En revanche, "$manufacturer" et "$category" sont bien des champs valués de collection d’origine.

Attention

Comme pour toutes les collections dans MongoDB, la collection contenant les résultats doit comporter une clé "_id" qui est unique à chaque document.

Autres fonctions possibles de l’étape $group:
$sum: pour additionner ou compter tous les documents,
$avg: pour effectuer une moyenne de valeurs dans les documents,
$min: pour obtenir le minimum,
$max: pour obtenir le maximum,
$push: permet de créer un tableau dans lequel les valeurs seront rajoutées systématiquement à la fin,
$addToSet: permet de créer un tableau dont les valeurs seront uniques,
$first: permet d’indiquer la première valeur parmi une liste de valeurs. Il faut utiliser cette fonction avec “$sort” pour que le résultat soit pertinent.
$last: permet d’indiquer la dernière valeur parmi une liste de valeurs. Il faut utiliser cette fonction avec “$sort” pour que le résultat soit pertinent.

Précision sur la fonction $sum:

Si on souhaite compter les différentes valeurs, on utilisera { $sum: 1 }, par exemple:

db.products.aggregate( [  
{ 
    $group : 
    { 
        _id: { "maker": "$manufacturer", "cat": "$category" }, num_products: { $sum: 1} 
    } 
} 
] )

En revanche, si on veut sommer les valeurs, on utilisera: { $sum: "$price" }, "price" étant le champ à sommer dans la collection d’origine. Par exemple:

db.products.aggregate( [  
{ 
    $group : 
    { 
        _id: { "maker": "$manufacturer", "cat": "$category" }, total: { $sum: "$price" } 
    } 
} 
] )

Précision sur la fonction $avg

La fonction $avg possède une syntaxe similaire à la fonction $sum:

Par exemple pour avoir la moyenne des prix:

db.products.aggregate( [  
{ 
    $group : 
    { 
        _id: { "maker": "$manufacturer", "cat": "$category" }, avg_price: { $avg: "$price" } 
    } 
} 
] )

Précision sur la fonction $addToSet

La syntaxe est similaire aux fonctions précédentes mais le champ dans les documents résultats sera un tableau au lieu d’être une valeur.

Double grouping

On peut effectuer 2 agrégations l’une à la suite de l’autre. Par exemple: si on a une liste de notes indiquant l’élève, la note, la classe et le type de devoir. Si on veut la moyenne par classe, il faut:
– effectuer la moyenne par élève et par classe
– effectuer la moyenne par classe
Pour effectuer la moyenne par élève et par classe:

db.grades.aggregate( [ 
{  
    $group: { _id : { "studentId" : "$student_id", "classId": "$class_id" }, "average" : { $avg : "$score" }} 
} 
])

Pour effectuer la 2ème moyenne en utilisant les résultats de la première requête:

db.grades.aggregate( [ 
{  
    $group: { _id : { "studentId" : "$student_id", "classId": "$class_id" }, "average" : { $avg : "$score" }} 
}, 
{ 
    $group: { _id : "$_id.class_id" , "average" : { $avg : "$average" }} 
} 
] )

Précision sur les fonctions "$first" et "$last"

Exemple:

db.cities.aggregate( [  
{ 
    $group : { _id : { state: "$state", city : "$city" } } 
    { population : { $sum: "$pop" } } 
}
{ 
    $sort : { "_id.state" : 1, "population" : -1 } 
} 
{ 
    $group : { 
        _id : "$_id.state", 
        city : { $first : "$_id.city"}, 
        population : { $first : "$population" } 
} 
} 
] )

Étape $project

Cette fonction permet de :
– supprimer des clés,
– ajouter de nouvelles clés,
– utiliser des fonctions simples comme $toLower, $toUpper, $add ou $multiply.
La cardinalité de cette fonction est 1:1.

Par exemple:

db.prices.aggregate( [ 
{ 
    $project : { 
        _id: 0, 
        "maker" : { $toLower : "$manufacturer" }, 
        "details" : { "category" : "$category" , "price" : { $multiply : [ "$price" : 10 ] }, 
        "item" : "$name", 
        "name" : 1 
    } 
} 
] )

"_id: 0" permet d’indiquer qu’on ne souhaite pas afficher le champ "_id".
Les champs "maker", "details" et “item” définissent les champs qui seront présents dans les documents du résultat mais ils ne sont pas présents dans les documents d’origine.

"name" :1 permet d’indiquer qu’on veut le champ "name" sans modifications.

Étape "$match"

Cette fonction permet de filtrer une collection dans la fonction d’agrégation. La cardinalité est n:1.

Par exemple, si on souhaite filtrer une collection contenant des adresses en ne gardant que les adresses dans l’état de New York:

db.addresses.aggregate( [  
    { $match : { state : "NY" } } 
] )

Pour avoir les codes postaux dont la population est supérieure à 100000:

db.zips.aggregate( [  
    { $match : { pop : { $gt:100000} } } 
] )

Étape "$sort"

Permet d’effectuer un tri. On peut l’utiliser avant ou après l’utilisation de "$group".

ATTENTION

Le tri s’effectue intégralement en mémoire

Exemple:

db.cities.aggregate( [  
{ 
    $group : 
    { _id : { state: "$state", city : "$city" } } 
    { population : { $sum: "$pop" } } 
}, 
{ 
    $sort : { "_id.state" : 1, "population" : -1 } } 
] )

"_id.state" ne comporte pas de "$" puisqu’on ne veut pas une valuation du champ.
"1" permet d’indiquer qu’on souhaite le tri dans le sens croissant. Pour avoir le sens décroissant, on utilise "-1".

Étapes "$skip" et "$limit"

On peut utiliser les 2 fonctions séparément mais la plupart du temps elles sont utilisées toutes les 2 avec la fonction "$sort" et dans l’ordre "$skip" puis "$limit". Sachant que ce sont des fonctions séparées, ce sont des étapes distinctes dans le pipeline.

Par exemple:

db.addresses.aggregate( [  
    { $sort : { state : 1} } 
    { $skip: 10 } 
    { $limit: 5 } 
] )

"$skip" permet de sauter 10 valeurs et "$limit" permet de limiter le nombre de résultat à 5.

Étape "$unwind"

Permet de convertir les éléments dans un tableau en plusieurs documents en reprenant les mêmes valeurs pour les autres champs.
Ainsi si on a un document:

{ a:1, b:1, c: [ "c1", "c2", "c3" ] }

Le résultat sera:

{ a:1, b:1, c: "c1" } 
{ a:1, b:1, c: "c2" } 
{ a:1, b:1, c: "c3" } 

Exemple:

db.posts.aggregate( [ 
    { $unwind: "tags" } 
] )

Double unwind

Si on utilise 2 étapes "$unwind", on effectue le produit cartésien entre toutes les valeurs des 2 tableaux.

Ainsi si on a: { a:1, b:1, c: [ "c1", "c2" ], d: [ "d1", "d2" ] }

Alors:

db.posts.aggregate( [ 
    { $unwind: "c" } 
    { $unwind: "d" } 
] )

On aura:

{ a:1, b:1, c: "c1", d: "d1" } 
{ a:1, b:1, c: "c1", d: "d2" } 
{ a:1, b:1, c: "c2", d: "d1" } 
{ a:1, b:1, c: "c2", d: "d2" } 

Limitations de la fonction "aggregate"

– La limite par étape est de 100 MB. il est possible d’utiliser une option "allowDiskUse" par dépasser cette limite.
– Il existe une limite de 16 MB par document si on renvoie tous les résultats dans un document.
– Dans le cas du "sharding", si on effectue une requête utilisant "group by" ou "sort", tous les documents sont récupérés de tous les “shard” vers le premier “shard” pour être traités. Il existe une fonction “map/reduce” (non recommandé) pour aider dans ce cas.
Il est aussi possible d’utiliser "hadoop".

Agrégations avec le driver .NET

Si on souhaite effectuer la requète:

db.zips.aggregate( [ 
    { $group: { _id: "state", totalPop: { $sum: "$pop" } } }, 
    { $match: { totalPop: { $gte: 10*1000*1000 } } } 
]}

En .NET:
Avec des BsonDocument:

var list = await col.aggregate() 
    .Group(new BsonDocument("_id", "$state").Add("totalPop", new BsonDocument("$sum", "$pop"))) 
    .Match (new BsonDocument("totalPop", new BsonDocument("$gte", 10 * 1000 * 1000))) 
    .ToListAsync(); 

Avec des chaines de caractères directement:

var list = await col.Aggregate() 
    .Group("{ _id: '$state', totalPop: { $sum: '$pop'} }") 
    .Match("{totalPop: {$gte: 10000000}}") 
    .ToListAsync(); 

Avec POCO:

var list = col.Aggregate() 
    .Group(x => x.State, g => new { State = g.Key, TotalPop = g.Sum(x => x.Population) }) 
    .Match(x => x.TotalPop > 10*1000*1000);

Formation MongoDB M101N: semaine 3 – Conception

Quelques caractéristiques:
– “Rich documents”: on peut stocker davantages que de simples valeurs,
– “Prejoin” en utilisant des documents intégrés (embedded documents),
– Pas de jointures en MongoDB,
– Pas de contraintes,
– Pas de transactions,
– Pas de schéma conceptuel.

Le principe général d’une base MongoDB est d’avoir des schémas de données proches de l’application.

Les bases relationnelles sont conçues pour:
– Interdire de modifier la base en introduisant des anomalies,
– Minimise le redesign de la base quand le schéma conceptuel grossit,
– Il permet d’éviter d’être influencer par certains patterns d’accès.

MongoDB ne pousse pas à utiliser un pattern par rapport à un autre. Ensuite il faut éviter d’introduire des anomalies à la création de données.

Exemple d’articles d’un blog

Par exemple, pour afficher les articles d’un blog, on peut stocker tous les éléments de l’article:
– titre,
– auteur,
– contenu,
– date,
– mots clé.

Tous les commentaires:
– auteur du commentaire,
– email,
– contenu du commentaire.

Tous ces éléments peuvent être stockés dans la même collection en utilisant des documents intégrés (embedded document).

Si on utilise un schéma proche de celui qu’on aurait utilisé dans une base relationnelle, on aurait:
– une collection pour les articles avec un ID, titre, corps, auteur et date,
– une collection pour les commentaires avec un ID, un ID d’article (clé étrangère), auteur, adresse email et l’ordre d’affichage du commentaire,
– une collection pour les tags avec un ID, un tag et un ID d’article (clé étrangère).

Ce type de schéma impose de
– maintenir une relation entre les 3 collections en utilisant des clés étrangères,
– maintenir l’ordre dans la collection des commentaires,
– d’utiliser 3 fichiers sur le disque, 1 fichier par collection.
– d’accèder à 3 collections pour afficher un article.

Cette organisation est plus lourde que de considérer une collection pour stocker tous les éléments.

Contraintes

Un des avantages de la base relationnelle est de garantir d’avoir des données consistantes en utilisant des clés étrangères. MongoDB ne garantit pas d’avoir des clés étrangères consistantes.
Le fait d’utiliser des documents intégrés garantit que les documents sont liés entre eux: si tous les commentaires sont directement dans la collection d’articles, par construction ils appartiennent à l’article.

Transactions

Pas de transactions en MongoDB mais les opérations sont atomiques.
Quand on insère des documents dans une collection, ils ne seront visibles que si le document est ajouté entièrement. En revanche, l’ajout de plusieurs documents n’est pas atomiques.
Le fait d’utiliser des documents intégrés permet de garantir la consistance des données.

En utilisant MongoDB, il faut:
– être restrictif en utilisant des documents intégrés ce qui permet d’insérer des données de façon consistante.
– implémenter des “locks” au niveau software
– tolérer une inconsistance dans les données.

Relations entre les objets logiques

Relations un vers un

Par exemple un employé et un CV, un employé n’a qu’un CV.
On peut stocker les infos dans 2 collections:

Employee:
- _ID,
- Name,
- Resume_ID
Resume 
- _ID,
- Jobs: [] (array)
- Education: []

Ou alors on inclut le CV directement dans la collection "Employee" sous forme de document intégré.

Pas de restrictions pour les relations un vers un, il faut juste avoir en tête:
La fréquence d’accès aux données: si on accède très souvent au CV d’un employé ou si on accède seulement aux informations de l’employé.
La taille des éléments: les informations du CV seront peut être plus volumineuse que celles de l’employé lui-même.
La limite de 16mb d’un document intégré: un document intégré ne peut dépasser 16mb.

Relations un vers plusieurs

Par exemple, une ville et une personne. Une ville contient plusieurs personnes.

Plusieurs schémas sont possibles:
1ere solution:

City:
- name,
- area;
- people: [] (array)

Ce schéma n’est pas possible car il y a beaucoup trop de personnes pour une ville, le tableau serait trop grand.

2e solution:

People:
- name,
- city : { name; area; }

Le problème avec ce schéma est de multiplier les données relatives à la ville dans tous les documents des personnes.

3e solution: 2 collections:

People:
- name;
- city: "NYC"
City:
- _ID: "NYC" 
- area

La 3e solution est la meilleure puisqu’on ne duplique pas les informations.

La 1ère solution aurait pu être choisi si il n’y avait pas beaucoup de personnes.

Pour les relations un vers plusieurs, l’important est de savoir l’ordre de grandeur du plusieurs:
dans le cas d’un grand nombre: il faut considérer une collection différente
dans le cas d’un nombre pas très élevé: il est préférable de privilégier un document intégré.

Plusieurs vers plusieurs

Par exemple, des livres et des auteurs.
Dans ce cas, il y a peu de livres pour un auteur et inversement, il a peu d’auteurs pour un livre. Donc il peut considérer 2 collections:

Books:
- _ID, 
- title, 
- authors: [ author_ID ]
Author: 
- _ID 
- Author_name, 
- books: [ books_ID ]

On peut inclure des références vers les auteurs dans la collection livre dans un tableau.

On peut aussi inclure directement les livres dans la collection auteur mais la contrainte est pour les livres qui ont plusieurs auteurs.

Dans le cas d’étudiants et de professeurs:

Un professeur possède beaucoup d’étudiants, on peut utiliser des documents intégrés en incluant un tableau de professeur dans la collection d’étudiants. Ce schéma peut poser problème si des étudiants sont aussi des professeurs.

Index multi-clé

Permet d’utiliser des index pour des relations plusieurs vers plusieurs.

Si on considère 2 collections telles que:

Students: 
- _ID, 
- name, 
- teachers: [ teacher_ID ] (tableau de teacher ID)
Teachers: 
- _ID 
- name,

Si on veut lister les professeurs d’un étudiant, on peut le faire directement. En revanche, si on veut les étudiants d’un professeur, on aura besoin d’un index multi-clé.

db.students.ensureIndex({ 'teachers':1 })

Pour avoir tous les étudiants d’un professeur:

db.students.find({ 'teachers': { $all: [ 0, 1 ] } })

Pour savoir comment l’index est utilisé:

db.students.ensureIndex({ 'teachers':1 }).explain()

Avantages d’utiliser des documents intégrés

– Permet d’améliorer les performances en lecture
– Effectuer un seul aller-retour avec la base de données
La base utilise des disques durs dont le temps d’accès est long mais qui ont une bande passante élevée: on met du temps à attendre l’information mais on peut charger rapidement beaucoup d’information en même temps. Il est donc préférable d’avoir des informations rapprochées pour les charger d’un coup.

Arbres

Par exemple, si on considère des produits et des catégories. Les catégories appartiennent à d’autres categories.
On peut proposer ce schéma:

Products: 
- Category, 
- Product_name
Category: 
- _ID 
- Category_name 
- ancestors: [ category_ID ]

Ainsi tous les ascendants de la categorie se trouvent dans le tableau “ancestors”.

Si on veut les descendants d’un categorie, il suffit de faire:

db.category.find({ ancestors: 35 })

Formation MongoDB M101N: semaine 2 – Driver .NET

Inclure le package .NuGet “MongoDB.Driver” dans son projet.
Plusieurs packages sont installés:
MongoDB.Drive, MongoDB.BSON et MongoDB.Driver.Core.
L’objet principal pour ouvrir une connection est MongoClient.
Pour entrer certains paramètres de connexion, il faut utiliser une connectionString.

var connectionString = "mongodb://localhost:27017";
var client = new MongoClient(connectionString);
var db = client.GetDatabase("test");
var col = db.GetCollection("person");

Représentation des documents

Pour représenter des documents en .NET, on utilise l’objet BsonDocument;

var doc = new BsonDocument
{
    { "name", "Jones" }
};

Pour ajouter une valeur:

doc.Add("age", 30);

Pour affecter une valeur:

doc["profression"] = "hacker";

Pour définir un tableau:

var nestedArray = new BsonArray();
nestedArray.Add(new BsonDocument("color", "red));
doc.Add("array", nestedArray);

Représentation POCO

Permet de mapper du POCO vers du Bson.
On peut utiliser directement des structures et préciser des attributs pour préciser des éléments sur des membres:
Par exemple:

class Person
{
    public ObjectId Id {get; set;}

    [BsonElement("name")]
    public string Name {get; set;}

    [BsonRepresentation(BsonType.String)]
    public int Age {get; set;}
}

Il est possible de préciser des données par code:

Par exemple:

BsonClassMap.RegisterClassMap<Person>(cm => *
{
    cm.AutoMap();
    cm.MapMember(x => x.Name).SetElementName("name");
});

Pour ajouter une convention qui permet d’indiquer que "name" doit être mappé vers le membre
Person.Name (avec une majuscule):

var conventionPack = new ConventionPack();
conventionPack.Add(new CamelCaseElementNameConvention());
ConventionRegistry.Register("camelcase", conventionPack, t => true);

insertOne

Pour insérer une valeur dans une collection:

var client = new MongoClient();
var db = client.GetDatabase("test");
var col = db.GetCollection<BsonDocument>("people");
var doc = new BsonDocument
{
    { "Name": "Smith" },
    { "Age": 30 },
};

Pour insérer un seul document:

await col.InsertOneAsync(doc);

Pour insérer plusieurs documents.

await col.InsertManyAsync();

Avec un classe mappée:

var person = class Person
{
    public ObjectId Id {get; set;}
    public string Name {get; set;}
    public int Age {get; set;}
};
var doc = new Person{ };
await col.InsertOneAsync(doc);

ATTENTION: on peut pas insérer 2 fois le même objet à cause de "_id" qui est valué.

Il faut que "_id" n’est pas de valeur.

find

find() permet de retourner plusieurs lignes comme le Shell:
Il faut passer par l’intermédiaire d’un curseur, on entoure l’utilisation du curseur avec un try...finally pour exécuter le close même en cas d’exception.

var col = db.GetCollection<BsonDocument>("people");
using (var cursor = qawait col.Find(new BsonDocument()).ToCursorAsync())
{
    while (await cursor.MoveNextAsync())
    {
        foreach (var doc in cursor.Current)
        {
            Console.WriteLine(doc);
        }
    }
}

Pour traiter une liste directement:

var list = await col.Find(new BsonDocument()).ToListAsync();

Avec un ForEach:

var list = await col.Find(new BsonDocument()).ForEachAsync(doc => ConsoleWriteLine(doc));

Avec des critères de sélection:

var filter = new BsonDocument("Name", "Smith");
var list = await col.Find(filter).ToListAsync();

Il est possible de passer du code Bson directement:

var list = await col.Find({ "Name" : "Smith" }).ToListAsync();

Pour entrer plus de conditions:

var filter = new BsonDocument("$and", new BsonArray
{
    new BsonDocument("Age", new BsonDocument("$lt", 30)), 
    new BsonDocument("Name", "Smith"),
});

var filter = builder.And(builder.Lt("Age", 30) & builder.Eq("Name", "Smith"));

Il est possible d’utiliser directement des POCO:

var col = db.GetCollection<Person>("people");

En utilisant des lambda expressions:

var list = await col.Find(x => x.Age < 30 && x.Name != "Smith").ToListAsync();

Skip

Permet de sauter des valeurs:

var list = await col.Find(new BsonDocument())
    .Limit(1)
    .Skip(1)
    .ToListAsync();

Sort

Pour trier des éléments:

var list = await col.Find(new BsonDocument())
    .Sort(new BsonDocument("{ Age : 1 }")
    .ToListAsync();

ou

var list = await col.Find(new BsonDocument())
    .Sort(new BsonDocument("Age", 1)
    .ToListAsync();

ou

var col = db.GetCollection<BsonDocument>("people");
var list = await col.Find(new BsonDocument())
    .Sort(Builders<BsonDocument>.Sort.Ascending("Age"))
    .ToListAsync();

En utilisant POCO;

var col = db.GetCollection<Person>("people");
var list = await col.Find(new BsonDocument())
    .Sort(Builders<Person>.Sort.Ascending("Age").Descending("Name"))
    .ToListAsync();

ou

var list = await col.Find(new BsonDocument())
    .SortBy(x => x.Age)
    .ToListAsync();

ou

var list = await col.Find(new BsonDocument())
    .Sort(Builders<Person>.Sort.Ascending(x => x.Age))
    .ToListAsync();

Projections

var list = await col.Find(new BsonDocument())
    .Project("{ Name : 1, _Id: 0}")
    .ToListAsync();

ou

var col = db.GetCollection<Person>("people");
var list = await col.Find(new BsonDocument())
    .Project<Person>(Builders<Person>.Projection.Include("Name").Exclude("_Id"))
    .ToListAsync();

ou

var col = db.GetCollection<Person>("people");
var list = await col.Find(new BsonDocument())
    .Project(x => x.Name)
    .ToListAsync();

Replace

var result = await col.ReplaceOneAsync(
    new BsonDocument("_Id", 5)
    new BsonDocument("_Id", 5).Add("x", 30)
);

Si on utilise les "_Id", il faut répéter le séquence.

var result = await col.ReplaceOneAsync(
    new BsonDocument("x", 5)
    new BsonDocument("x", 30)
);

Pour savoir si l’update a fait quelque chose, il faut regarder le résultat dans "result".
En ajoutant une option:

var result = await col.ReplaceOneAsync(
    new BsonDocument("x", 5)
    new BsonDocument("x", 30)
    new UpdateOptions { IsUpsert = true }
);

Update

Même type de syntaxe:

var result = await col.UpdateOneAsync(
    Builders<BsonDocument>.Filter.Eq("x", 5),
    new BsonDocument("$inc", new BsonDocument("x", 10))
);

ou

var result = await col.UpdateOneAsync(
    Builders<BsonDocument>.Filter.Eq("x", 5),
    Builders<BsonDocument>.Update.Inc("x", 10)
);

ou

var result = await col.UpdateOneAsync(
    x => x.X > 5,
    Builders<BsonDocument>.Update.Inc("x", 10)
);

UpdateMany permet de modifier plusieurs lignes.

Delete

Pour supprimer seulement 1 élément:

var result = await col.DeleteOneAsync(x => x.X > 5);

Ou pour supprimer plusieurs documents:

var result = await col.DeleteManyAsync(x => x.X > 5);

FindOneAndUpdate

Permet de faire une mise à jour si on trouve l’élément.

var result = await col.FindOneAndUpdateOneAsync(
    x => x.X > 5,
    Builders<BsonDocument>.Update.Inc("x", 10)
);

Le résultat contient le document qui a été modifié.
On peut rajouter des options:

var result = await col.FindOneAndUpdateOneAsync(
    x => x.X > 5,
    Builders<BsonDocument>.Update.Inc("x", 10),
    new FindOneAndUpdateOptions<Widget, Widget>
    {
         ReturnDocument = ReturnDocument.After
    }
);

Par défaut, on renvoie le document avant modification. L’option "ReturnDocument" permet de renvoyer le document après la modification.

Sort

Pour trier les éléments avant de les modifier:

var result = await col.FindOneAndUpdateOneAsync(
    x => x.X > 5,
    Builders<BsonDocument>.Update.Inc("x", 10),
    new FindOneAndUpdateOptions<Widget, Widget>
    {
        ReturnDocument = ReturnDocument.After,
        Sort = Builders<Widget>.Sort.Descending(x => x.X),
    }
);

Formation MongoDB M101N: semaine 2 – CRUD

Opérations CRUD et mongo Shell

Les mots clé en mongo équivalent à:
– Create: insert
– Read: find
– Update: update
– Delete: remove

Pour effectuer des opérations CRUD en mongodb, on utilise pas une string comme en SQL mais directement des fonctions de l’API.
Le mongo Shell est un shell Javascript ce qui signifie qu’il est possible d’écrire du Javascript directement dans le Shell.
Les raccourcis sont compatibles avec les raccourcis EMACS (Ctrl + A, Ctrl + E etc…).
Pour avoir de l’aide:
taper “help”

Assignation de variable:

x = 1; y = "abc";

z = { "a" : 1 } permet de créer un dictionaire. Pour accéder à une valeur, il suffit de faire z.a ou z["a"]. Il faut privilégier z["a"] qui permet d’accéder au type de la valeur.

Par exemple:

x = { "a" : 1 };
y = "a";
x[y]++;
print(x.a);

Le résultat est 2.

Afficher une valeur

print(x.a)

BSON

Les données stockées dans une base Mongo sont sous forme de données binaires: BSON pour binary JSON.
Il est possible de faire des affectations de dictionnaires et de documents intégrés (“embedded document”):

obj = { "a" : 1, "b" : "hello", "c" : [ "apples", "tomatoes" ] }

Mais il est aussi possible d’affecter des données plus fortement typées en utilisant des constructeurs:
NumberInt(1) pour des entiers codés en 16bits ou NumberLong(3) pour des entiers codés en 64 bits.
new Date() est de type ISODate().

Par exemple:

obj = { "a" : 1, "b" : ISODate("2012-10"), "c" : NumberLong(42) }

Fonction "insert"

Permet d’insérer un document dans une collection:

db.people.insert([ document ])

Dans le syntaxe db.people.insert():
"db" est le base de données,
"people" est une collection et
"insert()" est une fonction sur la collection.

Une collection comporte toujours un champ "_id" qui est inséré par défaut. La valeur est censé être unique à l’échelle de toutes les bases (espèce de GUID).

Fonction "findOne"

Permet de retourner la 1ère valeur de la collection:

db.people.findOne()

Il est possible de rajouter des arguments:

db.people.findOne( { "name" : "jones" } )

On peut préciser plusieurs conditions dans le premier argument de la fonction:
Par exemple:

db.score.find( { "student" : 19 , "type" : "essay" } );

Permet de remonter tout le document avec des valeurs avec des champs tels que "student" 19 ET les "types" “essay”.

Le 2ème argument de la fonction FIND indique les champs à remonter et les champs à exclure:
Par exemple:

db.score.find( { "student" : 19 , "type" : "essay" }, { "score" : true } );

Dans ce cas on va remonter seulement les champs “score” et "_id" (qui est automatiquement remonté).
Pour exclure le champ "_id", il faut préciser:

db.score.find( { "student" : 19 , "type" : "essay" }, { "score" : true, "_id" : false } );

Fonction "find"

Fonctionne comme la fonction "findOne".
db.people.find() permet d’afficher tout le contenu de la collection.

On peut améliorer l’affichage en écrivant:

db.people.find().pretty()

Opérateurs "$gt" et "$lt"

Permet d’avoir des conditions telles que:
"$gt": strictly greater than
"$lt": strictly less than
"$lte": less or equal than
"$gte": greater or equal than
Par exemple:

db.people.find( { score : { $gt : "95" } } );

Avec plusieurs conditions:

db.people.find( { score : { $gt : "95", "$lt" : 100 }, type : "essay" } );

Utilisation des opérateurs de comparaison sur les chaînes de caractères

Il est possible d’utiliser les opérateurs "$gt" et "$lt" pour les chaînes de caractères:
Par exemple:

db.people.find( { name : { $gt : "B", $lt : "D" } } );

Permet de retourner les noms dont la première lettre est supérieure à "B" et inférieure à "D".
En fait on utilise l’ordre des caractères dans le codage UTF-8 pour savoir moment ordonner les lettres.
Il est possible d’utiliser des types différents dans les champs. Cependant si un champ "name" d’un document est une valeur numérique, il ne sera pas retourné par la requête précédente.

Opérateur $exists

Permet d’indiquer si un champ à l’intérieur d’un document existe ou non.
Par exemple:

db.people.find( { profession : { $exists : true }});

Permet d’indiquer si le champ “profession” existe.
A l’opposé pour chercher les documents où le champ n’existe pas:

db.people.find( { profession : { $exists : false }});

Opérateur $type

Permet de faire un recherche en fonction du type d’un champ.
Par exemple:

db.people.find( { name : { $type : 2}});

Les codes des types sont à retrouver sur http://bsonspec.org/spec.html
Le type 2 correspond au type “string”.

Opérateur $regex

Permet d’indiquer une expression régulière pour faire une recherche.
Par exemple:
db.people.find( { name : { $regex : "a"}}); pour avoir un champ "name" contenant “a”.
db.people.find( { name : { $regex : "e$"}}); pour avoir un champ "name" se terminant avec “e”.
db.people.find( { name : { $regex : "^A"}}); pour avoir un champ "name" commençant par “A”.

Opérateur $or

Opérateur logique:

db.people.find( { $or : [ { name : { $gt : "C" } }, { age : { $exists : true } } ] } );
Remarque:

Faire attention on utilise des crochets pour cet opérateur: $or : [...]

db.scores.find( { $or : [ { score : { $lt : 50 } }, { score : { $gt : 90 } } ] } ) ;

Opérateur $and

Opérateur logique utilisable pour les conditions d’une requête FIND:
Par exemple:

db.people.find( { $and : [ { name : { $gt : "C" } }, { name : { $regex : "a" } } ] } );

Équivalent à:

db.people.find( { name : { $gt : "C" , $regex : "a" } } );
ATTENTION;

db.scores.find( { score : { $gt : 50 }, score : { $lt : 60 } } );

Cette syntaxe est fausse, dans la requète ici, score : { $gt : 50 } est remplacé par score : { $lt : 60 } et la requète exécutée est:

db.scores.find( { score : { $lt : 60 } } );

La syntaxe correcte est:

db.scores.find( { score : { $gt : 50 , $lt : 60 } } );

Recherche à l’intérieur d’un tableau

Si un document est de type:

{ "name" :"George", "favorites": [ "ice cream", "pretzels"]}

Pour chercher à l’intérieur du tableau il suffit de faire:

db.accounts.find( { favorites: "pretzels" })

Ce genre de requêtes ne fonctionne que pour un tableau et seulement pour des valeurs qui se trouvent directement sur le tableau et non dans des tableaux intégrés.

Opérateur $all

Permet d’indiquer que tous les éléments dans un tableau doivent être présents pour que le document soit remonté.
Par exemple:

db.accounts.find( { favorites: { $all : [ "pretzels", "beer" ] } } )

Il faut que le tableau contienne "pretzels" ET "beer", l’ordre n’a pas d’importance.

Opérateur $in

Même chose que l’opérateur $all mais il suffit qu’un seul élément dans le tableau soit présent dans le document pour qu’il soit retourné.
Par exemple:

db.accounts.find( { favorites: { $in : [ "pretzels", "beer" ] } } )

Les documents dont le tableau contient "pretzels" OU "beer" seront remontés.

Notation avec un point "."

Cette notation permet de designer un champ d’un document lorsque celui-ci est composé de document intégré (embedded document).
Un document intégré est de ce type:

name : "richard"
email : { "work" : "richard.lord@work.com",
"personal" : richard.lord@home.com}

Le champ “email” est un document intégré (embedded document).
Si on fait une requête sur ce document, on est obligé d’indiquer tous les champs dans le bon ordre sinon le document contenant se sera pas retourner.
Par exemple:

db.people.find( { email : { "work" : "richard.lord@work.com"} } );

Ne retournera aucun résultat.

db.people.find( { email : { "personal" : "richard.lord@home.com", "work" : "richard.lord@work.com" } } );

Ne retournera aucun résultat.
Pour faire une requête seulement sur le champ "work" il faut utiliser la notation:

db.people.find( { "email.work" : "richard.lord@work.com" } );
ATTENTION:

Il faut utiliser les guillemets "email.work"

Curseur

Permet de parcourir les résultats de la fonction code class=”cs”>”find()”.

cur = db.people.find()

Pour avoir la valeur suivante:

cur.next()

Pour savoir si il existe une valeur suivante:

cur.hasNext()

Ce qui permet de faire une boucle WHILE:

while (cur.hasNext()) printjson(cur.next());

Pour limiter le nombre d’éléments dans le curseur:

cur.limit(5)

Pour ordonner les éléments dans le curseur:
cur.sort( { name: -1 } ); pour afficher dans l’ordre décroissant du champ "name".
Pour sauter 2 documents:

cur.sort( { name: -1 } ).limit(3).skip(2);
ATTENTION:

sort, skip et limit peuvent être utilisé seulement avant que le 1er document soit retourné et avant avoir vérifié que le curseur est vide.

Fonction "count"

code class=”cs”>count() permet de compter le nombre de valeurs:

long count = dbCollection.count();

La syntaxe du Shell pour la fonction "count" est similaire à celle de "find":

db.people.count( { "name" : "john" } );

Fonction "update"

Pour faire des mises à jour.

Par exemple:

dbCollection.update( { name : "Smith" }, { name : "Thompson", salary:50000 })

Cette ligne va remplacer tous les champs du document avec le name ="Smith". Si d’autres champs sont présents, ils seront tous remplacés par le seul champ "salary".
Dans l’exemple: db.people.update( { name : "john" }, { $set : { profession : 1 }} );
Si aucun document ne correspond à "name" = " john", aucune mise à jour ne sera effectuée.

Paramètre $set

Permet de rajouter de nouveaux champs sans effacer les anciens:
Par exemple:

db.people.update( { name : "john" }, { $set : { profession : 1 }} );

Si aucun document ne correspond à "name" = " john", aucune mise à jour ne sera effectuée.

Paramètre $unset

Permet de supprimer un champ d’un document.

La syntaxe est similaire à $set:

db.people.update( { name : "john" }, { $unset : { profession : 1 }} );

Cette ligne va supprimer le champ "profession" du document avec "name" = "john".

ATTENTION

La valeur “1” est ici complêtement arbitraire, elle ne sera pas utilisée par la suite dans le cas du $unset.

Manipulation de tableau

Si on a un tableau d’éléments:

a : [ 1, 2, 3, 4]

Pour modifier la valeur d’un élément du tableau:

db.arrays.update( { _id : 0 }, { $set : { "a.2" : 5 } } )

Permet de modifier la 3e valeur du tableau

a : [ 1, 2, 5, 4]

$push

Permet de rajouter une valeur à la fin du tableau

db.arrays.update( { _id : 0 }, { $push : { a : 6 } } )

Le résultat est:

a : [ 1, 2, 5, 4, 6]

$pop

Permet de supprimer une valeur à gauche avec 1 comme valeur:

db.arrays.update( { _id : 0 }, { $pop : { a : 1 } } )

Le résultat est:

a : [ 1, 2, 5, 4]

Ou supprimer une valeur à droite avec -1:

db.arrays.update( { _id : 0 }, { $pop : { a : -1 } } )

Le résultat est:

a : [ 2, 5, 4]

$pushAll

Permet de rajouter un autre tableau à la suite d’un tableau:

db.arrays.update( { _id : 0 }, { $pushAll : { a : [7, 8, 9] } } )

Le résultat est:

a : [ 2, 5, 4, 7, 8, 9]

$pull

Permet de supprimer une valeur quelque soit sa position dans le tableau

db.arrays.update( { _id : 0 }, { $pull : { a : 5 } } )

Le résultat est:

a : [ 2, 4, 7, 8, 9]

$pullAll

Permet de supprimer une liste de valeurs quelque soit leur position

db.arrays.update( { _id : 0 }, { $pushAll : { a : [2, 4, 8] } } )

Le résultat est:

a : [ 7, 9]

$addToSet

Permet de rajouter un élément dans le tableau s’il n’existe pas déjà. La fonction est idempotente (plusieurs exécutions de la ligne ne rajoutera pas la valeur si elle est déjà présente).

db.arrays.update( { _id : 0 }, { $addToSet : { a : 5 } } )

Le résultat est:

a : [ 7, 9, 5]

Paramètre $upsert

Si la valeur est à false, la commande "update" ne rajoute pas d’entrées si aucun document ne correspond au critère de sélection. Si vraie alors un document est rajoutée même si il n’existe pas à l’origine.

db.people.update( { name : "john" }, { $set : { profession : 1 }, { $upsert : true } } );

Permettra de rajouter le document avec name="john" et profession = 1 même s’il n’existe pas.

Pour utiliser "$upsert" il faut que la valeur du champ soit parfaitement indiquée.

Par exemple:

db.people.update( { age : { $gt : 50 } }, { $set : { name : "john" }, { upsert : true } } );

Cette ligne ne permet pas de déterminer la valeur du champ “age” donc il ne rajoutera pas le champ "age". En revanche, il va rajouter le champ "name".

Paramètre $multi

Si faux, la modification s’effectue seulement sur le premier élément qui correspond aux critères de sélection. Si vraie, alors tous les documents satisfaisant le critère de sélection seront mis à jour.

db.people.update( {}, { $set : { title : "Dr" }, { $multi : true } } );

Permettra de rajouter l’élément "title" à tous les documents de la collection.

ATTENTION

Par défaut "update" ne modifie que la 1ère valeur d’un document satisfaisant le critère. Il faut l’option "$multi" pour la mise à jour se fasse sur plusieurs documents.

Pausing yielding

La mise à jour de plusieurs lignes ne se fait pas de façon atomique. Seul l’écriture des champs est faite de façon atomique. Le pausing yielding consiste, dans le cas où plusieurs opérations de mise à jour sont faites sur les mêmes documents, à permettre la mise à jour de plusieurs documents pour la première opération, puis de plusieurs documents pour la 2ème opération. Ensuite de revenir à la première opération et de permettre la mise à jour d’autres documents et ainsi de suite…

Paramètre $inc

Pour incrémenter une valeur numérique:

db.scores.update( { "score" : { $lt : 70 } }, { $inc : { score : 20} }, { multi : true} )

Permet de rajouter 20 points aux scores inférieurs à 70.

Fonction "remove"

Permet de supprimer des données.
Cette fonction a la même syntaxe que "find".
Contrairement à “update” qui met à jour par défaut seulement le premier document qui correspond aux critères, “remove” supprime tous les documents qui correspondent aux critères.
Par exemple:

db.collection.remove({});

Supprime tous les documents de la table.

db.collection.remove({"id", "alice"});

Permet de supprimer les documents dont le champ "id" est "alice".
La suppression avec "remove" se fait de ligne à ligne contrairement à "drop".
Comme l’update, la suppression de lignes avec "remove" n’est pas atomique.

Fonction "drop"

Permet de supprimer toutes les valeurs d’une collection sans parcourir les lignes une à une.
Plus efficace que "remove" pour supprimer toutes les valeurs.

getLastError

Pour obtenir la dernière erreur survenue:

db.runCommand( { getLastError : 1 } )

Le résultat est un document avec pour élément “err” contenant le message de son erreur.
Cette commande permet aussi de donner un feedback sur certaines commandes sans forcément même si il n’y a pas eu d’erreurs.

Driver .NET

Formation MongoDB M101N: semaine 2 – Driver .NET