Loading

ESIR 2020 Mongo

Benoît Chanclou

This is a live streamed presentation. You will automatically follow the presenter and see the slide they're currently on.

Mongo

Qui suis-je ?

Benoît Chanclou

Le complément

de NodeJS

Mongo

Base de données

NoSQL

MongoDB

Base orienté documents

Mongo

Base de données 

  • NoSQL avec
    • replication
    • sharding
  • Orientée documents
    • sans schéma
    • sans relationnel

Théorème CAP (ou de Brewer)

  • Conjecture puis démonstration en 2002
  • On ne peut avoir plus de deux
    • Consistency: toute requête reçoit la valeur la plus récente ou une erreur
    • Availability: toute requête reçoit une réponse sans garanti qu'il contient la valeur la plus récente
    • Partition tolerance: Le système continue de fonctionner même si la communication entre noeuds est défaillante

Bases SQL & NoSQL

Replication

  • Redondance des nœuds d'instance

En cas de panne de l'un des nœuds

la redondance assure la disponibilité des données

Sharding

  • La base est tronçonnée

Répartition des documents
selon une de leur caractéristique

Orientée document

Document <=> objet JSON avec un champ "_id"

{
    "_id" : ObjectId("5811bac48b0cf22ea39e16ec"),
    ...
}
{
    "_id" : "toto",
    ...
}
{
    "_id" : 42,
    ...
}

rappel sur l'auto-incrémentation

  • L'auto-incrémentation c'est le mal
    • espionnage via API
    • ids identiques pour des ressources différentes
       
  • L'UUID s'est le bien

Il faut privilégier des UUIDs

UUID

Un UUID est

  • un nombre de 128 bits (ou ~3.4E38)
  • construit (différentes versions)
  • aléatoire
  • unique (probabilité extrême de duplicata)

en pratique

Un UUID est constitué de :

  • string de 32 chiffres héxadécimaux
  • regroupés en 5 groupes séparés par des tirets

Version du générateur

503d6158-aa6d-490f-91dc-6659ea126fb2

time low

time med

time high

clock

node

en pratique

Bibliothèque npm uuid

https://www.npmjs.com/package/uuid

$ npm install uuid

NoSQL Orientée document

=>

  • Pas de relationnel
  • Pas de schéma
    (en option)
  • Pas de transactionnel
    (sauf dans certaines conditions)

Outils

Manipuler et administrer

MongoDB Compass

  • l'outil officiel
  • beaucoup plus complet (management et monitoring)
  • payant pour avoir la version complète

RoboMongo

  • le plus connu
  • permet d'éditer les requêtes directement
  • version plus complète payante

MongoDB vs SQL

MongoDB

Base NOSQL

non relationnelle

orientée document

Vocabulaire

SQL vs Mongo

  • Base <=> Base
  • Collection <=> Table
  • Document / Objet <=> Ligne / Enregistrement
  • Champ <=> Colonne
  • Sous document ou Lien <=> Jointure
{
    _id: ObjectId("000280000000000000000042"),
    ...
}
Id ...
42 ...

table  #28

mongo

on peut trouver une équivalence entre SQL et mongo

sql

Requêtes

Find & Aggregate

Requêtes simples

Les fonctions CRUD
de bases

No SQL

Langage de requête
en ecmascript

(alias javascript)

C : Création

insert

Insert

  INSERT INTO Tiers (First_Name, Last_Name)
  VALUES ('John','Doe');
  db.Tiers.insert(
    { 
      First_Name: "John", 
      Last_Name : "Doe" 
    }
  )

R : Lecture

find & findone

Find

  SELECT *
  FROM Tiers 
  WHERE First_Name = 'John';
  db.Tiers.find(
    { First_Name: "John" }
  )
{ 
  "_id": ObjectId("58776b8643ab721e65d744ba"), 
  "First_Name": "John",
  "Last_Name": "Doe"
}

Findone

  SELECT *
  FROM Tiers 
  WHERE First_Name = 'John';
  db.Tiers.findone(
    { First_Name: "John" }
  )
{ 
  "_id": ObjectId("58776b8643ab721e65d744ba"), 
  "First_Name": "John",
  "Last_Name": "Doe"
}

Find (avec projection)

  SELECT Last_Name 
  FROM Tiers 
  WHERE First_Name = 'John';
  db.Tiers.find(
    { First_Name: "John" }, 
    { Last_Name: 1 }
  )
{ 
  "_id": ObjectId("58776b8643ab721e65d744ba"), 
  "Last_Name": "Doe"
}

Find (dans un sous objet)

  SELECT Tiers.Name
  FROM Tiers
  LEFT JOIN Address
  ON Address.OwnerId = Tiers.ID
  WHERE City REGEXP 'Saint.*';
  db.Tiers.find(
    { "address.city": { $regex: /Saint.*/g } }, 
    { name: 1 }
  )
{ 
  "_id": ObjectId("58776b8643ab721e65d744ba"), 
  "name": "Tactilia"
}

Find (opérateurs)

db.Tiers.find(
    { First_Name: { $in: ["John", "Jane"] } }
)
db.Equipments.find(
    { $or: [ { nextControlDate: "none" }, { nextControlDate: { $gt: new Date() } } ] }
)
db.Equipments.find(
    { nbSeats: { $exists: true } }
)

U : Mettre à jour

update

Update

    UPDATE Tiers
    SET First_Name = 'Jane'
    WHERE Last_Name = 'Doe';
    db.Tiers.update(
        { Last_Name: "Doe" }, 
        { $set: { First_Name: "Jane" } }
    )
    db.Tiers.update(
        { Last_Name: "Doe" }, 
        { First_Name: "Jane" }
    )

Update (opérateurs)

  • Champs :  $set, $inc, $rename, $currentDate...
  • Tableaux : $addToSet, $push, $pop, $each, $sort,...
  • Bits : $bit​
db.Tiers.update(
    {},
    { $rename: { First_Name: "firstName", Last_Name: "lastName" } },
    { multi: true }
)
db.Tiers.update(
    { "_id": ObjectId("58776b8643ab721e65d744ba") },
    { $push: { products: "configurateur" } },
    { upsert: true }
)
db.Scores.update(
    { "_id": ObjectId("55d744ba76b864387ab721e6") },
    { 
        $push: { score: 42, game: "slot" },
        $currentDate: { "last.play": true },
        $set: { "last.result": "win" },
        $inc: { credits: -1 }
    }
)

D : Supprimer

delete

Delete

    DELETE FROM Tiers
    WHERE Last_Name = 'Doe';
    db.Tiers.delete(
        { Last_Name: "Doe" }
    )

Requêtes complexes

principe

Enchaîner les opérations

Requête composée

aggregate

Exemple

{
    "_id" : ObjectId("584159ba741d85f3df9147d4"),
    "mod_id" : ObjectId("584158d4e4b07592382ba74b"),
    "modVersion" : 0,
    "startDate" : ISODate("2016-12-02T11:12:54.837Z"),
    "submitDate" : ISODate("2016-12-02T11:14:36.643Z"),
    "data" : {
        "CITY" : {
            "date" : NumberLong(1480677182830),
            "value" : {
                "zip" : "35830",
                "city" : "Betton"
            }
        },
        ...
    },
    ...
}
x 2468

Etape #1 : Récupérer les données

db.formData_582b2b5ae4b0a55f3d0f436f.aggregate( [
    { $match: { 
        mod_id: ObjectId("5840597be4b07592382acc4c"), 
        modVersion: 1, 
        submitDate: { $exists: 1 } 
} } ] )
{
    "_id" : ObjectId("584159ba741d85f3df9147d4"),
    "mod_id" : ObjectId("584158d4e4b07592382ba74b"),
    "modVersion" : 0,
    "startDate" : ISODate("2016-12-02T11:12:54.837Z"),
    "submitDate" : ISODate("2016-12-02T11:14:36.643Z"),
    "data" : {
        "CITY" : {
            "date" : NumberLong(1480677182830),
            "value" : {
                "zip" : "35830",
                "city" : "Betton"
            }
        },
        ...
    },
    ...
}
x 2468
{
    "result" : [ 
        {
            "_id" : ObjectId("58418252741d85f3df914805"),
            "mod_id" : ObjectId("5840597be4b07592382acc4c"),
            "modVersion" : 1,
            "startDate" : ISODate("2016-12-02T14:06:49.758Z"),
            "submitDate" : ISODate("2016-12-02T14:07:47.871Z"),
            "data" : {
                ...
                "CITY" : {
                    "date" : NumberLong(1480687625106),
                    "value" : {
                        "zip" : "35000",
                        "city" : "Rennes"
                    }
                },
                ...
            },
        },
        ... x 1655
    ],
    "ok" : 1
}

Etape #2 : Élaguer les données

db.formData_582b2b5ae4b0a55f3d0f436f.aggregate( [ 
  { $match: { ... } },
  { $project: { "data.CITY": 1 } }
] )
{
    "_id" : ObjectId("584159ba741d85f3df9147d4"),
    "mod_id" : ObjectId("584158d4e4b07592382ba74b"),
    "modVersion" : 0,
    "startDate" : ISODate("2016-12-02T11:12:54.837Z"),
    "submitDate" : ISODate("2016-12-02T11:14:36.643Z"),
    "data" : {
        "CITY" : {
            "date" : NumberLong(1480677182830),
            "value" : {
                "zip" : "35830",
                "city" : "Betton"
            }
        },
        ...
    },
    ...
}
x 2468
{
    "result" : [ 
        {
            "_id" : ObjectId("58418252741d85f3df914805"),
            "data" : {
                "CITY" : {
                    "date" : NumberLong(1480687625106),
                    "value" : {
                        "zip" : "35000",
                        "city" : "Rennes"
                    }
                }
            }
        }, 
        ... x 1655
    ],
    "ok" : 1
}

Etape #3 : Grouper les résultats

db.formData_582b2b5ae4b0a55f3d0f436f.aggregate( [ 
  { $match: { ... } },
  { $project: { "data.CITY": 1 } },
  { $group: { _id: "$data.CITY.value.city", count: { $sum:1 } } }
] )
{
    "_id" : ObjectId("584159ba741d85f3df9147d4"),
    "mod_id" : ObjectId("584158d4e4b07592382ba74b"),
    "modVersion" : 0,
    "startDate" : ISODate("2016-12-02T11:12:54.837Z"),
    "submitDate" : ISODate("2016-12-02T11:14:36.643Z"),
    "data" : {
        "CITY" : {
            "date" : NumberLong(1480677182830),
            "value" : {
                "zip" : "35830",
                "city" : "Betton"
            }
        },
        ...
    },
    ...
}
x 2468
{
    "result" : [ 
        {
            "_id" : "Bourges",
            "count" : 12
        }, 
        {
            "_id" : "Angers",
            "count" : 2
        }, 
        ... x 1655
    ],
    "ok" : 1
}

Etape #4 : Trier les résultats

db.formData_582b2b5ae4b0a55f3d0f436f.aggregate( [ 
  { $match: { ... } },
  { $project: { "data.CITY": 1 } },
  { $group: { _id: "$data.CITY.value.city", count: { $sum:1 } } },
  { $sort: { "count" : -1 } }
] )
{
    "_id" : ObjectId("584159ba741d85f3df9147d4"),
    "mod_id" : ObjectId("584158d4e4b07592382ba74b"),
    "modVersion" : 0,
    "startDate" : ISODate("2016-12-02T11:12:54.837Z"),
    "submitDate" : ISODate("2016-12-02T11:14:36.643Z"),
    "data" : {
        "CITY" : {
            "date" : NumberLong(1480677182830),
            "value" : {
                "zip" : "35830",
                "city" : "Betton"
            }
        },
        ...
    },
    ...
}
x 2468
{
    "result" : [ 
        {
            "_id" : "Vannes",
            "count" : 707
        }, 
        {
            "_id" : "Theix",
            "count" : 89
        }, 
        ... x 1655
    ],
    "ok" : 1
}
        

Etape #5 : Limiter le nombre

db.formData_582b2b5ae4b0a55f3d0f436f.aggregate( [ 
  { $match: { ... } },
  { $project: { "data.CITY": 1 } },
  { $group: { _id: "$data.CITY.value.city", count: { $sum:1 } } },
  { $sort: { "count" : -1 } },
  { $limit: 3 }
] )
{
    "_id" : ObjectId("584159ba741d85f3df9147d4"),
    "mod_id" : ObjectId("584158d4e4b07592382ba74b"),
    "modVersion" : 0,
    "startDate" : ISODate("2016-12-02T11:12:54.837Z"),
    "submitDate" : ISODate("2016-12-02T11:14:36.643Z"),
    "data" : {
        "CITY" : {
            "date" : NumberLong(1480677182830),
            "value" : {
                "zip" : "35830",
                "city" : "Betton"
            }
        },
        ...
    },
    ...
}
x 2468
{
    "result" : [ 
        {
            "_id" : "Vannes",
            "count" : 707
        }, 
        {
            "_id" : "Theix",
            "count" : 89
        }, 
        {
            "_id" : "Saint-Avé",
            "count" : 67
        }
    ],
    "ok" : 1
}

Pas de Schéma

des objets

Pas de schéma

On peut y mettre n'importe quel JSON (max 16Mo)

=> mongo n'impose aucun schéma

=> c'est à l'utilisateur (programme appelant) de gérer

Pas de schéma

Du coup on peut avoir plusieurs types d'objets qui cohabitent dans la même collection !

{
    "name": "Voiture",
    "plate": "AR-456-YI",
    "nextControlDate": ISODate("2017-10-01T00:00:00.000Z"),
    "nbSeats": 5
}
{
    "name": "Camion",
    "plate": "AD-123-PS",
    "nextControlDate": "none",
    "capacity": 20
}

Stockage d'objets

  • C'est fait pour !
  • Pas besoin des complications de SQL

Quid du polymorphisme ?

Règles de bon sens

  • On met un seul type d'objet dans une collection
    => On sait a priori ce qui est stocké
     
  • On met une famille d'objets dans la collection
    & on identifie le type de chaque objet
    => Pour savoir précisément ce qu'on manipule

Quel est le type de mon document ?

on ajoute un champ "_type" ou "_class"

=> on sait quel type on manipule

=> si absent = type générique de la collection

Comment on identifie le type ?

Quel est le type de mon document ?

Exemple

{
    "_type": "car",
    "name": "Voiture",
    "plate": "AR-456-YI",
    "nextControlDate": ISODate("2017-10-01T...Z"),
    "nbSeats": 5
}
{
    "_type": "truck",
    "name": "Camion",
    "plate": "AD-123-PS",
    "nextControlDate": "none",
    "capacity": 20
}

Relations entre objets

composition et agrégation

Pas de relationnel

Pourtant l’agrégation et l'association sont des concepts de base des relations entre objets

Pas de relationnel

Mais on peut avoir des objets composés

{
    _id: "564f36c87dcbd96cf6f3e2ea",
    name: "Tactilia",
    address: {
        city: "Saint Grégoire"
        ...
    },
    products: [
        "fidbe", ...
    ],
    ...
}

Pas de relationnel

Mais on peut mettre des références vers un objet

{
    _id: "564f36c87dcbd96cf6f3e2ea",
    name: "Tactilia",
    employees: [
        "/employees/564f36c87dcbd07cf6f3e2ea",
        ...
    ],
    logo: "564f36c87dcbd97cf6f3e2ea",
    ...
}

L'aggrégation est à

gérer à la main

Agrégation & relation

{
    _id: "5b50f8968656472a58970167",
    password: "xxx",
    userId: "bchanclo",
    ??? lien vers le tiers ???
}
{
    _id: "5b50f8968656472a58970167",
    firstname: "Benoît",
    lastname: "Chanclou",
    birthdate: ...
}

Account

Tiers

Exemple un compte d'accès en lien avec un tiers

Un simple id

  • suppose qu'on sache où aller chercher la ressource
  • on sait faire 
    • un accès direct à la base
       
    • ou via l'api du référentiel Tiers
{
    _id: "5b50f8968656472a58970167",
    password: "xxx",
    userId: "bchanclo",
    user: "564f36c87dcbd97cf6f3e2ea",
}
GET http://api.myapp.com/tiers/1/contacts/564f36c87dcbd97cf6f3e2ea
db.tiers.findOne({_id: "564f36c87dcbd97cf6f3e2ea"})

Une uri (c'est universel !)

  • ne présuppose rien sur la localisation de la ressource
  • on sait
    • extraire l'id pour une requête directe à la base
    • ou appeler l'url correspondant à l'uri
{
    user: "http://api.myapp.com/tiers/1/contacts/564f36c87dcbd97cf6f3e2ea",
}

Une référence enrichie

  • les objets de type "Tiers" sont dans une collection ou un référentiel connu
  • on sait
    • récupérer la ressource
    • que ce sera un objet de type Tiers
{
    user: {
        type: "Tiers",
        id: "564f36c87dcbd97cf6f3e2ea"
    },
}

Les doublons

C'est le mal !

Ben non, c'est même plutôt utile

Les doublons

un cache

=> il faut une référence

  • Pour les requêtes croisées
    compense l'absence de relationnel
  • Pour les données en lecture seule
  • Il faut un mode de synchronisation

Requêtes croisées

Trouver des livraisons dont le livreur est prénommé "John"

Impossible en une seule étape

{
    ...
    "deliveryMan": xxx,
    ...
}
{
    "_id": xxx,
    "firstname": "John",
    "lastname": "Doe",
    "phone": "+33123456789",
    ...
}
db.Deliveries.find(
  { deliveryMan: xxx }
)
db.Tiers.find(
  { firstname: "John" }
)

Tiers

Deliveries

Requêtes croisées

Impossible sauf en mettant

les champs nécessaires dans le cache

{
    ...
    "deliveryMan": xxx,
    ...
}
{
    "_id": xxx,
    "firstname": "John",
    "lastname": "Doe",
    "phone": "+33123456789",
    ...
}
{
    ...,
    "deliveryMan": {
        "_id": xxx,
        "firstname": "John",
        "lastname": "Doe",
        "phone": "+33123456789"
    },
    ...
}
db.Deliveries.find(
  { deliveryMan.firstname: "John" }
)

Tiers

Deliveries

Deliveries

Une référence avec cache

  • si on veut juste le nom et le prénom
    • inutile d'aller charger la ressource secondaire
    • on utilise le cache
  • on sait récupérer la ressource si besoin
{
    "contact": {
        "_id": xxx,
        "firstname": "John",
        "lastname": "Doe",
        "phone": "+33123456789"
    },
}

Avantages et Contraintes du cache

+ Accès rapide

+ Recherches possible sur les champs mis en cache

+ Lazy loading de la ressource référencée

- Gérer le cycle de vie du cache :

- Invalidation

- Mise à jour

Mise à jour du cache

  • Asynchrone :
    Lorsque la ressource secondaire est chargée (en lazy loading), le cache peut être mis à jour

 

  • Synchrone :
    Lorsque la ressource secondaire est mise à jour
    • En local :
      Le cache est lui aussi mis à jour
    • Dans un autre référentiel :
      Le secondaire appelle un webhook du principal pour mettre à jour le cache

Versionner

Pourquoi !?

Les objets sont vivants

Un objet a un cycle de vie

  • création
  • modification
  • destruction

destruction

  • delete

ou

  • Archivage

=>Ajout d'un champ _archive: true

find({ 
    ..., 
    $or: [ { _archive: { $exists: false } }, { _archive: false } ]
})
find({ 
    ..., 
    _archive: true
})

Les objets ont des champs 

  • en plus
  • en moins
  • renommés
  • qui changent de type

En plus

  • si c'est un champ qui ne peut pas être nul
    => initialiser la valeur par défaut dans le constructeur
  • au prochain enregistrement il apparaîtra

En moins

  • au prochain enregistrement complet il disparaîtra
  • au pire utiliser $unset (cas d'une champs volumineux)

Renommage

  • garder les deux noms
  • ajouter accesseur et mutateur

ou

  • utiliser un système de version de schémas

ou

  • mettre à jour les objets de la base avec $rename

Changement de type

  • ajouter accesseur et mutateur
    pour retourner le bon type
    (si le langage le permet)

ou

  • utiliser un système de version de schémas

ou

  • mettre à jour les objets de la base

Un objet

  • change de nature
  • fusionne avec un autre
  • se scinde en deux

changement de nature

  • le nouveau format est dans une autre collection
  • grâce au UUIDs
    • si l'objet est dans la nouvelle collection => ok
    • sinon l'objet est dans l'ancienne
      => convertir l'objet

ou

  • utiliser un système de version de schémas

ou

  • on met à jour les objets de la base

fusion

  • utiliser un système de version de schéma
  • à la lecture l'objet principal agrège le second objet

ou

  • mettre à jour les objets de la base

scission

  • l'objet principal reste dans sa collection
    • ​à la lecture si encore agrégé scinder l'objet
  • le second objet est transféré dans une autre collection​
    • l'objet est dans la nouvelle collection => ok
    • sinon l'objet est encore agrégé à l'objet principal
      => convertir la partie du second objet
      (accessoirement mettre aussi à jour l'objet principal)
    • problème de génération de l'id du second objet

ou

  • on met à jour les objets de la base

Versionner les schémas

Pourquoi ?

  • Parce qu'il n'y a pas de schéma !
    => plusieurs versions différentes peuvent cohabiter
    • pas la peine de mettre les documents à jour
      => pas de script de mise à jour
      (sauf énorme changement)
    • mais on ne sait pas quelle version on manipule
      => ajouter un champ "_version"
      (si absent = version 0)

Comment ?

{
    "addressStreet": "rue d'Alembert",
    "addressZipCode": "35000",
    "addressCityName": "Rennes",
    ...
}
{
    "_version": 1
    "address": {
        "street": "rue de Gauss",
        "zipCode": "56230",
        "cityName": "Questembert"
    },
    ...
}
{
    "_version": 2
    "address": {
        "street": "rue du Rennes",
        "city" : {
            "zipCode": "35700",
            "name": "Saint Malo"
        }
    },
    ...
}
var CONVERTERS = [
  obj => { 
    obj.address = {
      street: obj.addressStreet,
      zipCode: obj.addressZipCode,
      cityName: obj.addressCityName
    };
    delete obj.addressStreet;
    delete obj.addressZipCode;
    delete obj.addressCityName;
    obj._version = 1;
  },
  obj => { 
    obj.address.city = {
      zipCode: obj.address.zipCode,
      name: obj.address.cityName
    };
    delete obj.address.zipCode;
    delete obj.address.cityName;
    obj._version = 2;
  }
];
while (obj._version < CONVERTERS.length) {
  CONVERTERS[obj._version](obj);
}

Avantages et Contraintes

+ on ne manipule que des objets au dernier format

+ on n'est pas obligé de mettre toute la collection à jour

+ pas de migration =

  • pas d'interruption de service
  • conversion au fil de l'eau

 

- recherche complexifiées

- il faut rechercher dans toutes les versions

 

utile dans une grosse base où peu d'objets sont utilisés
(ex.: liste de commandes clients, au quotidien on ne manipule que les plus récentes)

Gérer l'historique

Cas #1

Un document par version

{  "docId" : 174, "rev" : 1,  "attr1": 165 }   /* version 1 */
{  "docId" : 174, "rev" : 2,  "attr1": 165, "attr2": "A-1" } 
{  "docId" : 174, "rev" : 3,  "attr1": 184, "attr2": "A-1" }
db.docs.find({ "docId": 174 }).sort({ "rev": -1 }).limit(-1);

findone

db.docs.aggregate( [ 
    { "$sort": { "docId" : 1, "rev" : -1 } },
    { "$group" : { "_id" : "$docId",
                   "doc": { "$first" : "$$ROOT" }
    } },
    { "$match": { "doc.attr1": 184 } }
] );

find

Cas #1a

Un document par version avec current

{  "docId" : 174, "v" : 1,  "attr1": 165 }
{  "docId" : 174, "v" : 2,  "attr1": 165, "attr2": "A-1" } 
{  "docId" : 174, "v" : 3,  "attr1": 184, "attr2": "A-1", "current": true }
db.docs.findOne({ "docId": 174, "current": true }));

findone

db.docs.find({ "doc.attr1": 184, "current": true }));

find

Cas #2

un seul document contenant toutes les versions

{  
    "docId" : 174, 
    "current" : { "attr1": 184, "attr2": "A-1" },
    "prev": [ {  "attr1": 165 }, {  "attr1": 165, "attr2": "A-1" } ]
}
db.CurrentCollection.findone({ "docId": 174 }, { current: 1 });

findone

db.CurrentCollection.find(
    { "current.attr1": 184 }, 
    { current: 1 }
);

find

Cas #3

Deux collections

CurrentCollection:    
  { "docId" : 174, "rev": 3, "attr1": 184, "attr2": "A-1" }
PreviousCollection:
  { "docId" : 174, "rev": 1, "attr1": 165 }
  { "docId" : 174, "rev": 2, "attr1": 165, "attr2": "A-1" }
  { "docId" : 174, "rev": 3, "attr1": 184, "attr2": "A-1" }
db.CurrentCollection.findone({ "docId": 174 });

findone

db.CurrentCollection.find({ "attr1": 184 });

find

Cas #4

Un document par version mais on stocke les deltas

sous forme de JSONPatch

{ "_id": ..., "id": 174, "userId": "xxx", "date": ISODate(...), patch: [
    { "op": "add", "path": "/attr1", "value": 165 }
] }
{ "_id": ..., "id": 174, "userId": "yyy", "date": ISODate(...), patch: [
    {"op": "add", "path": "/attr2", "value": "A-1" }
] } 
{ "_id": ..., "id": 174, "userId": "zzz", "date": ISODate(...), patch: [
    { "op": "replace", "path": "/attr1", "value": 184 }
] }

chaque patch contient

  • id du document
  • date de modification
  • patch à appliquer

en option

  • id utilisateur
  • son rôle
  • id du tenant

find

Compliqué on doit procéder en deux étapes

  • créer une collection où on reconstruit les objets
  • puis effectuer la recherche
db.docs.aggregate( [ 
    { $match: { id: "174" } },    // recherche tous les patches sur l'entité
    { $sort: { "date" : 1 } },    // trie les résultats par date
    { $unwind : "$patch" },       // aplati tous les patchs
    { $group: { _id: "$id", patch: { $push:"$patch" } } } 
    // regroupe tous les patchs en un tableau
] );

findone

  • On agrège tous les patchs lié à l'id de l'objet puis on applique la liste de patchs sur un objet vide
  • On peut ajouter un filtre sur la date pour gérer les révisions

Cas #3+4

Deux collections

SnapshotCollection:    
  { "id" : 174,  "v": 3, "attr1": 184, "attr2": "A-1" }
PatchCollection:
  { "_id": ..., "id": 174, "userId": "xxx", "date": ISODate(...), patch: [...] }
  { "_id": ..., "id": 174, "userId": "yyy", "date": ISODate(...), patch: [...] } 
  { "_id": ..., "id": 174, "userId": "zzz", "date": ISODate(...), patch: [...] }
db.SnapshotCollection.findone({ "docId": 174 });

findone

db.SnapshotCollection.find({ "attr1": 184 });

find

Cas #5

oplog

Le but est d'utiliser les logs de mongo pour gérer l'historique des versions du document (voir ce blog)

Au final

Mongo

compléments

Indexation

Accélérer les recherches

Qu'est-ce qu'un index ?

Une structure de recherche

  • but :
    réduire le temps de réponse de requêtes
  • inconvénient :
    prend de la place
    consommateur de ressources

Quand les utiliser ?

  • il faut les utiliser mais pas en abuser
  • les utiliser pour les requêtes identifiées comme
    • lentes
    • fréquentes

champ _id

  • indexé par défaut

Créer un index

  • Différents types d'index :
    • simple, composé, multi-clé, géospatial, texte, hash
  • Propriétés
    • unique, partiel, clairsemé, TTL, 
db.xxx.createIndex( <key & index type specification>, <options> )

Indexation

  • Pour le geospatial c'est top
     
  • Pour le texte c'est bof mais ça s'améliore
    il faut mieux utiliser une base d'indexation basée sur lucene
    (comme solr ou elasticsearch, ou des services tout fait comme algolia)
  • dans le nom de la collection

Les tenants

  • dans les documents
db.data_564f36c87dcbd97cf6f3e2ea.find()

Mettre un id de tenant

db.data.find({ownerId: "564f36c87dcbd97cf6f3e2ea"})
{
    ...,
    ownerId: "564f36c87dcbd97cf6f3e2ea",
    ...
}

Liens

bibliographie

Documentations

  • Documentation mongo (la recherche marche bien)
  • https://fr.wikipedia.org/wiki/Base_de_donn%C3%A9es_orient%C3%A9e_documents
  • Stack mean
  • Mongoose
  • Spring

Blogs & présentations

Utiliser un framework 

certains frameworks savent gérer ce type de références
(au sein d'une même application, même db)

  • java : Spring 
  • js : mongoose (populate)

Unitaires,

d'intégration,

de charge

Made with Slides.com