Obiettivo di confronto con stringa per $ graphLookup

Sto provando a lanciare $graphLookup come mostrato nel seguente messaggio:

$ graphLookup sample

L’objective è, dato un record specifico (commentato $match lì), recuperare il suo “path” completo attraverso la proprietà immediateAncestors . Come puoi vedere, non sta succedendo.

Ho introdotto $convert qui per gestire _id dalla raccolta come string , ritenendo che fosse ansible “corrispondere” con _id dall’elenco dei record immediateAncestors (che è una string ).

Quindi, ho eseguito un altro test con dati diversi (nessun object ObjectId coinvolto):

 db.nodos.insert({"id":5,"name":"cinco","children":[{"id":4}]}) db.nodos.insert({"id":4,"name":"quatro","ancestors":[{"id":5}],"children":[{"id":3}]}) db.nodos.insert({"id":6,"name":"seis","children":[{"id":3}]}) db.nodos.insert({"id":1,"name":"um","children":[{"id":2}]}) db.nodos.insert({"id":2,"name":"dois","ancestors":[{"id":1}],"children":[{"id":3}]}) db.nodos.insert({"id":3,"name":"três","ancestors":[{"id":2},{"id":4},{"id":6}]}) db.nodos.insert({"id":7,"name":"sete","children":[{"id":5}]}) 

E la domanda:

 db.nodos.aggregate( [ { $match: { "id": 3 } }, { $graphLookup: { from: "nodos", startWith: "$ancestors.id", connectFromField: "ancestors.id", connectToField: "id", as: "ANCESTORS_FROM_BEGINNING" } }, { $project: { "name": 1, "id": 1, "ANCESTORS_FROM_BEGINNING": "$ANCESTORS_FROM_BEGINNING.id" } } ] ) 

… che restituisce ciò che mi aspettavo (i cinque record collegati direttamente e indirettamente a quello con id 3):

 { "_id" : ObjectId("5afe270fb4719112b613f1b4"), "id" : 3.0, "name" : "três", "ANCESTORS_FROM_BEGINNING" : [ 1.0, 4.0, 6.0, 5.0, 2.0 ] } 

La domanda è: c’è un modo per raggiungere l’objective che ho menzionato all’inizio?

Sto usando Mongo 3.7.9 (da Docker ufficiale)

Grazie in anticipo!

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 della versione finale, pertanto il codice di produzione dovrebbe essere consapevole di ciò prima di eseguirne il commit.

Perché $ convert non riesce qui

Probabilmente il modo migliore per spiegarlo è guardare il campione modificato, ma sostituirlo con i valori ObjectId per _id e “stringhe” per quelli 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 dare una simulazione generale di ciò che stavi cercando di lavorare.

Quello che hai tentato era di convertire il valore _id in una “stringa” tramite $project prima di entrare nella fase $graphLookup . Il motivo per cui questo fallisce è mentre hai fatto un $project iniziale $project “all’interno” di questa pipeline, il problema è che la fonte per $graphLookup "from" è ancora la collezione inalterata e quindi non ottieni i dettagli corretti sul 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 quindi alla “ricerca”:

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

“Patching” del problema

Comunque questo è il problema principale e non un fallimento di $convert o è alias stesso. Per fare in modo che funzioni effettivamente, possiamo invece creare una “vista” che si presenta come una collezione per il gusto dell’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’uso della “vista”, tuttavia, significa che i dati vengono costantemente visualizzati 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 atteso:

 { "_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 avete alcuni dati che “assomigliano” a un valore ObjectId ed è in effetti valido come ObjectId , tuttavia è stato registrato come una “stringa”. Il problema di base a tutto ciò che funziona come dovrebbe è che i due “tipi” non sono gli stessi e questo si traduce in una mancata corrispondenza di uguaglianza mentre i “join” vengono tentati.

Quindi la vera soluzione è sempre la stessa che è sempre stata, ovvero passare attraverso i dati e correggerli in modo che le “stringhe” siano in realtà anche valori ObjectId . Questi corrisponderanno quindi alle chiavi _id cui sono destinati a fare riferimento e si sta risparmiando una notevole quantità di spazio di archiviazione poiché un ObjectId occupa molto meno spazio da memorizzare rispetto alla sua rappresentazione in caratteri esadecimali.

Usando i metodi di MongoDB 4.0, potresti effettivamente usare "$toObjectId" per scrivere una nuova collezione, proprio 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" } ]) 

Ovviamente, dove “hai bisogno” di mantenere la stessa collezione, il tradizionale “ciclo e aggiornamento” rimane lo stesso di quello che è sempre stato richiesto:

 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 ‘un “martello” a causa della sovrascrittura dell’intero array in un colpo solo. Non una grande idea per un ambiente di produzione, ma abbastanza come una dimostrazione ai fini di questo esercizio.

Conclusione

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

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

L’utilizzo di una “vista” in realtà significa 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”), che crea un sovraccarico reale.

Evitare il sovraccarico è in genere un objective di progettazione, quindi correggere errori di memorizzazione dei dati è fondamentale per ottenere prestazioni reali dall’applicazione, piuttosto che lavorare con la “forza bruta” che rallenterà solo le cose.


Uno script di “conversione” molto più sicuro che applicava aggiornamenti “abbinati” a ciascun elemento dell’array. Il codice qui richiede NodeJS v10.x e l’ultima versione 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 di massa 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" } ] } }