MongoDB
 sql >> Database >  >> NoSQL >> MongoDB

Corrispondenza ObjectId a String per $graphLookup

Attualmente stai utilizzando una versione di sviluppo di MongoDB che ha alcune funzionalità abilitate che dovrebbero essere rilasciate con MongoDB 4.0 come versione ufficiale. Tieni presente che alcune funzionalità potrebbero essere soggette a modifiche prima del rilascio finale, quindi il codice di produzione dovrebbe esserne a conoscenza prima di impegnarti.

Perché $convert qui non riesce

Probabilmente il modo migliore per spiegare questo è guardare il tuo campione alterato ma sostituirlo con ObjectId valori per _id e "stringhe" per quelle sotto gli array:

{
  "_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" } ]
}

Questo dovrebbe fornire una simulazione generale di ciò con cui stavi cercando di lavorare.

Hai tentato di convertire il _id valore in una "stringa" tramite $project prima di inserire il $graphLookup fase. Il motivo per cui questo fallisce è mentre hai eseguito un $project iniziale "all'interno" di questa pipeline, il problema è che il sorgente per $graphLookup nel "from" l'opzione è ancora la raccolta inalterata e quindi non ottieni i dettagli corretti sulle successive iterazioni di "ricerca".

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

Non corrisponde alla "ricerca" quindi:

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

"Correggere" il problema

Tuttavia questo è il problema principale e non un errore di $convert o è alias stesso. Per fare in modo che funzioni effettivamente, possiamo invece creare una "vista" che si presenta come una raccolta per motivi di input.

Lo farò al contrario e convertirò le "stringhe" in ObjectId tramite $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'utilizzo della "vista" significa tuttavia che i dati vengono visualizzati in modo coerente con i valori convertiti. Quindi la seguente aggregazione usando la vista:

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

Restituisce l'output previsto:

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

Risolvere il problema

Detto questo, il vero problema qui è che hai alcuni dati che "sembrano" un ObjectId value ed è infatti valido come ObjectId , tuttavia è stato registrato come "stringa". Il problema fondamentale per far funzionare tutto come dovrebbe è che i due "tipi" non sono gli stessi e ciò si traduce in una mancata corrispondenza di uguaglianza quando si tenta di "unire".

Quindi la vera soluzione è sempre la stessa di sempre, ovvero esaminare i dati e risolverli in modo che le "stringhe" siano effettivamente anche ObjectId valori. Questi corrisponderanno quindi a _id chiavi a cui dovrebbero fare riferimento e stai risparmiando una notevole quantità di spazio di archiviazione poiché un ObjectId occupa molto meno spazio per l'archiviazione rispetto alla rappresentazione di stringa in caratteri esadecimali.

Usando i metodi MongoDB 4.0, "potresti" effettivamente usa il "$toObjectId" per scrivere una nuova raccolta, più o meno nella stessa materia in cui abbiamo creato la "vista" in precedenza:

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

O, naturalmente, dove è "necessario" mantenere la stessa collezione, allora il tradizionale "loop and update" rimane lo stesso di sempre:

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 = [];
}

Che in realtà è un po' una "mazza" a causa della sovrascrittura dell'intero array in una sola volta. Non una grande idea per un ambiente di produzione, ma sufficiente come dimostrazione ai fini di questo esercizio.

Conclusione

Quindi, mentre MongoDB 4.0 aggiungerà queste funzionalità di "casting" che possono essere davvero molto utili, il loro vero intento non è proprio per casi come questo. Sono infatti molto più utili, come dimostrato nella "conversione" in una nuova raccolta utilizzando una pipeline di aggregazione rispetto alla maggior parte degli altri usi possibili.

Mentre noi "possiamo" crea una "vista" che trasforma i tipi di dati per abilitare cose come $lookup e $graphLookup lavorare dove i dati di raccolta effettivi differiscono, questo è in realtà solo un "cerotto" sul vero problema in quanto i tipi di dati in realtà non dovrebbero differire e dovrebbero infatti essere convertiti in modo permanente.

L'utilizzo di una "vista" significa in realtà che la pipeline di aggregazione per la costruzione deve essere eseguita in modo efficace ogni volta che si accede alla "raccolta" (in realtà una "vista"), il che crea un vero sovraccarico.

Evitare il sovraccarico è di solito un obiettivo di progettazione, quindi correggere tali errori di archiviazione dei dati è fondamentale per ottenere prestazioni reali dalla tua applicazione, piuttosto che lavorare solo con la "forza bruta" che rallenterà solo le cose.

Uno script di "conversione" molto più sicuro che applicava aggiornamenti "corrispondenti" a ciascun elemento dell'array. Il codice qui richiede NodeJS v10.x e una versione più recente del driver del nodo 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()
  }

})()

Produce ed esegue operazioni in blocco come queste per i sette documenti:

{
  "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"
      }
    ]
  }
}