MongoDB
 sql >> Base de données >  >> NoSQL >> MongoDB

Faire correspondre ObjectId à String pour $ graphLookup

Vous utilisez actuellement une version de développement de MongoDB qui a activé certaines fonctionnalités qui devraient être publiées avec MongoDB 4.0 en tant que version officielle. Notez que certaines fonctionnalités peuvent être sujettes à modification avant la version finale. Le code de production doit donc en être conscient avant de vous y engager.

Pourquoi $convert échoue ici

La meilleure façon d'expliquer cela est probablement de regarder votre échantillon modifié mais de le remplacer par ObjectId valeurs pour _id et "strings" pour ceux sous les tableaux :

{
  "_id" : ObjectId("5afe5763419503c46544e272"),
   "name" : "cinco",
   "children" : [ { "_id" : "5afe5763419503c46544e273" } ]
},
{
  "_id" : ObjectId("5afe5763419503c46544e273"),
  "name" : "quatro",
  "ancestors" : [ { "_id" : "5afe5763419503c46544e272" } ],
  "children" : [ { "_id" : "5afe5763419503c46544e277" } ]
},
{ 
  "_id" : ObjectId("5afe5763419503c46544e274"),
  "name" : "seis",
  "children" : [ { "_id" : "5afe5763419503c46544e277" } ]
},
{ 
  "_id" : ObjectId("5afe5763419503c46544e275"),
  "name" : "um",
  "children" : [ { "_id" : "5afe5763419503c46544e276" } ]
}
{
  "_id" : ObjectId("5afe5763419503c46544e276"),
  "name" : "dois",
  "ancestors" : [ { "_id" : "5afe5763419503c46544e275" } ],
  "children" : [ { "_id" : "5afe5763419503c46544e277" } ]
},
{ 
  "_id" : ObjectId("5afe5763419503c46544e277"),
  "name" : "três",
  "ancestors" : [
    { "_id" : "5afe5763419503c46544e273" },
    { "_id" : "5afe5763419503c46544e274" },
    { "_id" : "5afe5763419503c46544e276" }
  ]
},
{ 
  "_id" : ObjectId("5afe5764419503c46544e278"),
  "name" : "sete",
  "children" : [ { "_id" : "5afe5763419503c46544e272" } ]
}

Cela devrait donner une simulation générale de ce avec quoi vous essayiez de travailler.

Ce que vous avez tenté était de convertir le _id valeur dans une "chaîne" via $project avant de saisir le $graphLookup organiser. La raison pour laquelle cela échoue est que vous avez fait un premier $project "dans" ce pipeline, le problème est que la source de $graphLookup dans le "from" L'option est toujours la collection non modifiée et, par conséquent, vous n'obtenez pas les détails corrects lors des itérations de "recherche" suivantes.

db.strcoll.aggregate([
  { "$match": { "name": "três" } },
  { "$addFields": {
    "_id": { "$toString": "$_id" }
  }},
  { "$graphLookup": {
    "from": "strcoll",
    "startWith": "$ancestors._id",
    "connectFromField": "ancestors._id",
    "connectToField": "_id",
    "as": "ANCESTORS_FROM_BEGINNING"
  }},
  { "$project": {
    "name": 1,
    "ANCESTORS_FROM_BEGINNING": "$ANCESTORS_FROM_BEGINNING._id"
  }}
])

Ne correspond pas sur la "recherche" donc :

{
        "_id" : "5afe5763419503c46544e277",
        "name" : "três",
        "ANCESTORS_FROM_BEGINNING" : [ ]
}

"Corriger" le problème

Cependant, c'est le problème principal et non un échec de $convert ou c'est des alias lui-même. Pour que cela fonctionne réellement, nous pouvons à la place créer une "vue" qui se présente comme une collection à des fins de saisie.

Je vais faire cela dans l'autre sens et convertir les "chaînes" en ObjectId via $toObjectId :

db.createView("idview","strcoll",[
  { "$addFields": {
    "ancestors": {
      "$ifNull": [ 
        { "$map": {
          "input": "$ancestors",
          "in": { "_id": { "$toObjectId": "$$this._id" } }
        }},
        "$$REMOVE"
      ]
    },
    "children": {
      "$ifNull": [
        { "$map": {
          "input": "$children",
          "in": { "_id": { "$toObjectId": "$$this._id" } }
        }},
        "$$REMOVE"
      ]
    }
  }}
])

L'utilisation de la "vue" signifie cependant que les données sont toujours vues avec les valeurs converties. Donc l'agrégation suivante utilisant la vue :

db.idview.aggregate([
  { "$match": { "name": "três" } },
  { "$graphLookup": {
    "from": "idview",
    "startWith": "$ancestors._id",
    "connectFromField": "ancestors._id",
    "connectToField": "_id",
    "as": "ANCESTORS_FROM_BEGINNING"
  }},
  { "$project": {
    "name": 1,
    "ANCESTORS_FROM_BEGINNING": "$ANCESTORS_FROM_BEGINNING._id"
  }}
])

Renvoie la sortie attendue :

{
    "_id" : ObjectId("5afe5763419503c46544e277"),
    "name" : "três",
    "ANCESTORS_FROM_BEGINNING" : [
        ObjectId("5afe5763419503c46544e275"),
        ObjectId("5afe5763419503c46544e273"),
        ObjectId("5afe5763419503c46544e274"),
        ObjectId("5afe5763419503c46544e276"),
        ObjectId("5afe5763419503c46544e272")
    ]
}

Résoudre le problème

Cela dit, le vrai problème ici est que vous avez des données qui "ressemblent" à un ObjectId valeur et est en fait valide en tant que ObjectId , mais il a été enregistré en tant que "chaîne". Le problème fondamental pour que tout fonctionne comme il se doit est que les deux "types" ne sont pas les mêmes, ce qui entraîne une inadéquation de l'égalité lorsque les "jointures" sont tentées.

Ainsi, le vrai correctif est toujours le même qu'il l'a toujours été, qui consiste à parcourir les données et à les corriger afin que les "chaînes" soient également ObjectId valeurs. Ceux-ci correspondront alors au _id clés auxquelles ils sont censés se référer, et vous économisez une quantité considérable d'espace de stockage depuis un ObjectId prend beaucoup moins d'espace à stocker que sa représentation sous forme de chaîne en caractères hexadécimaux.

En utilisant les méthodes MongoDB 4.0, vous "pourriez" utilisez réellement le "$toObjectId" afin d'écrire une nouvelle collection, à peu près de la même manière que nous avons créé la "vue" plus tôt :

db.strcoll.aggregate([
  { "$addFields": {
    "ancestors": {
      "$ifNull": [ 
        { "$map": {
          "input": "$ancestors",
          "in": { "_id": { "$toObjectId": "$$this._id" } }
        }},
        "$$REMOVE"
      ]
    },
    "children": {
      "$ifNull": [
        { "$map": {
          "input": "$children",
          "in": { "_id": { "$toObjectId": "$$this._id" } }
        }},
        "$$REMOVE"
      ]
    }
  }}
  { "$out": "fixedcol" }
])

Ou bien sûr, si vous "avez besoin" de conserver la même collection, alors la traditionnelle "boucle et mise à jour" reste la même que celle qui a toujours été requise :

var updates = [];

db.strcoll.find().forEach(doc => {
  var update = { '$set': {} };

  if ( doc.hasOwnProperty('children') )
    update.$set.children = doc.children.map(e => ({ _id: new ObjectId(e._id) }));
  if ( doc.hasOwnProperty('ancestors') )
    update.$set.ancestors = doc.ancestors.map(e => ({ _id: new ObjectId(e._id) }));

  updates.push({
    "updateOne": {
      "filter": { "_id": doc._id },
      update
    }
  });

  if ( updates.length > 1000 ) {
    db.strcoll.bulkWrite(updates);
    updates = [];
  }

})

if ( updates.length > 0 ) {
  db.strcoll.bulkWrite(updates);
  updates = [];
}

Ce qui est en fait un peu un "marteau de forgeron" en raison de l'écrasement de l'ensemble du tableau en une seule fois. Ce n'est pas une bonne idée pour un environnement de production, mais c'est suffisant comme démonstration pour les besoins de cet exercice.

Conclusion

Ainsi, alors que MongoDB 4.0 ajoutera ces fonctionnalités de "casting" qui peuvent en effet être très utiles, leur intention réelle n'est pas vraiment pour des cas comme celui-ci. Ils sont en fait beaucoup plus utiles, comme le démontre la "conversion" en une nouvelle collection à l'aide d'un pipeline d'agrégation, que la plupart des autres utilisations possibles.

Alors que nous "pouvons" créer une "vue" qui transforme les types de données pour activer des choses comme $lookup et $graphLookup pour travailler là où les données de collecte réelles diffèrent, ce n'est vraiment qu'un "pansement" sur le vrai problème car les types de données ne devraient vraiment pas différer et devraient en fait être convertis en permanence.

L'utilisation d'une "vue" signifie en fait que le pipeline d'agrégation pour la construction doit s'exécuter efficacement chaque fois que la "collection" (en fait une "vue") est accédée, ce qui crée un réel surcoût.

Éviter les frais généraux est généralement un objectif de conception. Il est donc impératif de corriger ces erreurs de stockage de données pour obtenir de réelles performances de votre application, plutôt que de simplement travailler avec une "force brute" qui ne fera que ralentir les choses.

Un script de "conversion" beaucoup plus sûr qui applique des mises à jour "correspondantes" à chaque élément du tableau. Le code ici nécessite NodeJS v10.x et une dernière version du pilote de nœud MongoDB 3.1.x :

const { MongoClient, ObjectID: ObjectId } = require('mongodb');
const EJSON = require('mongodb-extended-json');

const uri = 'mongodb://localhost/';

const log = data => console.log(EJSON.stringify(data, undefined, 2));

(async function() {

  try {

    const client = await MongoClient.connect(uri);
    let db = client.db('test');
    let coll = db.collection('strcoll');

    let fields = ["ancestors", "children"];

    let cursor = coll.find({
      $or: fields.map(f => ({ [`${f}._id`]: { "$type": "string" } }))
    }).project(fields.reduce((o,f) => ({ ...o, [f]: 1 }),{}));

    let batch = [];

    for await ( let { _id, ...doc } of cursor ) {

      let $set = {};
      let arrayFilters = [];

      for ( const f of fields ) {
        if ( doc.hasOwnProperty(f) ) {
          $set = { ...$set,
            ...doc[f].reduce((o,{ _id },i) =>
              ({ ...o, [`${f}.$[${f.substr(0,1)}${i}]._id`]: ObjectId(_id) }),
              {})
          };

          arrayFilters = [ ...arrayFilters,
            ...doc[f].map(({ _id },i) =>
              ({ [`${f.substr(0,1)}${i}._id`]: _id }))
          ];
        }
      }

      if (arrayFilters.length > 0)
        batch = [ ...batch,
          { updateOne: { filter: { _id }, update: { $set }, arrayFilters } }
        ];

      if ( batch.length > 1000 ) {
        let result = await coll.bulkWrite(batch);
        batch = [];
      }

    }

    if ( batch.length > 0 ) {
      log({ batch });
      let result = await coll.bulkWrite(batch);
      log({ result });
    }

    await client.close();

  } catch(e) {
    console.error(e)
  } finally {
    process.exit()
  }

})()

Produit et exécute des opérations en masse comme celles-ci pour les sept documents :

{
  "updateOne": {
    "filter": {
      "_id": {
        "$oid": "5afe5763419503c46544e272"
      }
    },
    "update": {
      "$set": {
        "children.$[c0]._id": {
          "$oid": "5afe5763419503c46544e273"
        }
      }
    },
    "arrayFilters": [
      {
        "c0._id": "5afe5763419503c46544e273"
      }
    ]
  }
},
{
  "updateOne": {
    "filter": {
      "_id": {
        "$oid": "5afe5763419503c46544e273"
      }
    },
    "update": {
      "$set": {
        "ancestors.$[a0]._id": {
          "$oid": "5afe5763419503c46544e272"
        },
        "children.$[c0]._id": {
          "$oid": "5afe5763419503c46544e277"
        }
      }
    },
    "arrayFilters": [
      {
        "a0._id": "5afe5763419503c46544e272"
      },
      {
        "c0._id": "5afe5763419503c46544e277"
      }
    ]
  }
},
{
  "updateOne": {
    "filter": {
      "_id": {
        "$oid": "5afe5763419503c46544e274"
      }
    },
    "update": {
      "$set": {
        "children.$[c0]._id": {
          "$oid": "5afe5763419503c46544e277"
        }
      }
    },
    "arrayFilters": [
      {
        "c0._id": "5afe5763419503c46544e277"
      }
    ]
  }
},
{
  "updateOne": {
    "filter": {
      "_id": {
        "$oid": "5afe5763419503c46544e275"
      }
    },
    "update": {
      "$set": {
        "children.$[c0]._id": {
          "$oid": "5afe5763419503c46544e276"
        }
      }
    },
    "arrayFilters": [
      {
        "c0._id": "5afe5763419503c46544e276"
      }
    ]
  }
},
{
  "updateOne": {
    "filter": {
      "_id": {
        "$oid": "5afe5763419503c46544e276"
      }
    },
    "update": {
      "$set": {
        "ancestors.$[a0]._id": {
          "$oid": "5afe5763419503c46544e275"
        },
        "children.$[c0]._id": {
          "$oid": "5afe5763419503c46544e277"
        }
      }
    },
    "arrayFilters": [
      {
        "a0._id": "5afe5763419503c46544e275"
      },
      {
        "c0._id": "5afe5763419503c46544e277"
      }
    ]
  }
},
{
  "updateOne": {
    "filter": {
      "_id": {
        "$oid": "5afe5763419503c46544e277"
      }
    },
    "update": {
      "$set": {
        "ancestors.$[a0]._id": {
          "$oid": "5afe5763419503c46544e273"
        },
        "ancestors.$[a1]._id": {
          "$oid": "5afe5763419503c46544e274"
        },
        "ancestors.$[a2]._id": {
          "$oid": "5afe5763419503c46544e276"
        }
      }
    },
    "arrayFilters": [
      {
        "a0._id": "5afe5763419503c46544e273"
      },
      {
        "a1._id": "5afe5763419503c46544e274"
      },
      {
        "a2._id": "5afe5763419503c46544e276"
      }
    ]
  }
},
{
  "updateOne": {
    "filter": {
      "_id": {
        "$oid": "5afe5764419503c46544e278"
      }
    },
    "update": {
      "$set": {
        "children.$[c0]._id": {
          "$oid": "5afe5763419503c46544e272"
        }
      }
    },
    "arrayFilters": [
      {
        "c0._id": "5afe5763419503c46544e272"
      }
    ]
  }
}