Principes

et

Bonnes pratiques

Outils

Manipuler et administrer les bases mongo

RoboMongo

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

MongoDB Compass

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

Ligne de commande

  • outil le plus basique
  • outil complet
  • utile pour l'initialisation, la sauvegarde et la restauration de données
    (notamment avec des scripts)
  • permet de tester une requête
    (mais les autres sont plus conviviaux)

MongoDB vs mySQL

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

Mongo

Base de données 

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

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

{
    _id: ObjectId("000280000000000000000042"),
    ...
}
Id ...
42 ...

table  #28

mongo

on peut trouver une équivalence entre SQL et mongo

sql

NoSQL Orientée document

=>

  • Pas de relationnel
  • Pas de schéma
    (en option)
  • Pas de transactionnel
    (mais ça va venir dans certaines conditions)

Requêtes

Find & Aggregate

Requêtes simples

Les fonctions CRUD
de bases

No SQL

Langage de requête
en ecmascript

(alias javascript)

Insert

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

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 } }
)

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 }
    }
)

Delete

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

Requêtes complexes l'agrégation

principe

Enchaîner les opérations

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 (jusqu'à 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 livreur dont le nom est "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

{
    ...
    "contactId": xxx,
    ...
}
{
    "_id": xxx,
    "firstname": "John",
    "lastname": "Doe",
    "phone": "+33123456789",
    ...
}
{
    ...,
    "contact": {
        "_id": xxx,
        "firstname": "John",
        "lastname": "Doe",
        "phone": "+33123456789"
    },
    ...
}
db.Customers.find(
  { contact.firstname: "John" }
)

Tiers

Customers

Customers

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

S'adapter

au besoin

et

à la volumétrie

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)

MongoDB

Compléments

Les champs supp' 

C'est open bar

Pas de schéma

  • Il suffit d'ajouter les champs
  • Les champs peuvent être :
    • un objet
    • un tableau
    • un type simple
    • whatever

Nécessité d'un modèle

  • Connaître le contenu est indispensable
    => Spécifier le type de document

Sous types

=> utiliser le "_type"

{
    "_id": ...,
    "_type": "Sous type 1",
    "champSpecifiqueSousType1" : []
    ...
}

l'héritage c'est le mal,
l'agrégation c'est le bien

Agrégats

=> utiliser un tableau  de "_tags"

{
    "_id": ...,
    "_tags": [
        "Attribut 1", 
        "Attribut 2", 
        ...],
    "champSpecifiqueAttr1" : null,
    "champSpecifiqueAttr2" : ""
    ...
}

Champs dynamiques

=> utiliser un modèle

{
    "_id": ...,
    "_modId": [ 123 ],
    "aaa": 42
    ...
}
{
    "_id": 123,
    "fields": [ { 
        "name": "Champ quelconque", 
        "label": "aaa", 
        "type": "int", 
        .... }, 
    ... ]
}

Modèle

Recommandations

  • Attention aux collisions de noms de champs
    • envisager des sous documents
  • Les requêtes se font de la même manière
    • champs supp' <=> champs normaux
  • 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)

MongoDBx4

By Benoît Chanclou

MongoDBx4

en cours

  • 375