mongodb mongodb-query aggregation-framework

mongodb - Coincidiendo ObjectId con String para $ graphLookup



mongodb-query aggregation-framework (1)

Estoy tratando de ejecutar un $graphLookup como se muestra en la siguiente impresión:

El objetivo es, dado un registro específico (comentado $match allí), recuperar su "ruta" completa a través de immediateAncestors propiedad TimerAncestors. Como puedes ver, no está sucediendo.

Introduje $convert aquí para tratar con _id de la colección como una string , creyendo que podría ser posible "coincidir" con _id de immediateAncestors lista de registros de los _id immediateAncestors (que es una string ).

Entonces, realicé otra prueba con datos diferentes (sin ObjectId s involucrado):

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

Y la consulta:

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

... lo que da como resultado lo que esperaba (los cinco registros se conectaron directa e indirectamente al que tiene la 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 pregunta es: ¿hay una manera de lograr el objetivo que mencioné al principio?

Estoy corriendo Mongo 3.7.9 (de Docker oficial)

¡Gracias por adelantado!


Actualmente está utilizando una versión de desarrollo de MongoDB que tiene algunas funciones habilitadas que se espera que se lancen con MongoDB 4.0 como versión oficial. Tenga en cuenta que algunas características pueden estar sujetas a cambios antes de la versión final, por lo que el código de producción debe tener esto en cuenta antes de comprometerse.

Por qué $ convert falla aquí

Probablemente la mejor manera de explicar esto es mirar su muestra alterada pero reemplazando con los valores de ObjectId para _id y "cadenas" para aquellos que están bajo las matrices:

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

Eso debería dar una simulación general de lo que intentabas trabajar.

Lo que intentaste fue convertir el valor _id en una "cadena" a través de $project antes de ingresar a la etapa $graphLookup . La razón por la que esto falla es que mientras realizaba un $project inicial de $project "dentro" de esta canalización, el problema es que la fuente de $graphLookup en la opción "from" sigue siendo la colección sin modificaciones y, por lo tanto, no obtiene los detalles correctos en la iteraciones posteriores de "búsqueda".

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

No coincide en la "búsqueda" por lo tanto:

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

"Parcheando" el problema

Sin embargo, ese es el problema central y no un error de $convert o son alias en sí. Para hacer que esto realmente funcione, podemos crear una "view" que se presente como una recopilación por el bien de la entrada.

Lo haré al revés y convertiré las "cadenas" a ObjectId través de $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" ] } }} ])

Sin embargo, usar la "view" significa que los datos se ven constantemente con los valores convertidos. Así que la siguiente agregación utilizando 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" }} ])

Devuelve el resultado esperado:

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

Arreglando el problema

Con todo lo dicho, el verdadero problema aquí es que tiene algunos datos que "parecen" un valor ObjectId y de hecho son válidos como ObjectId , sin embargo, se han registrado como una "cadena". El problema básico para que todo funcione como debería es que los dos "tipos" no son los mismos y esto da como resultado un desajuste de igualdad cuando se intentan las "uniones".

Así que la solución real sigue siendo la misma que siempre ha sido, que consiste en ir a través de los datos y corregirlos para que las "cadenas" sean en realidad valores de ObjectId . Luego, coincidirán con las claves _id las que deben referirse, y está ahorrando una cantidad considerable de espacio de almacenamiento, ya que un ObjectId ocupa mucho menos espacio para almacenar que su representación de cadena en caracteres hexadecimales.

Al usar los métodos de MongoDB 4.0, usted podría " usar $toObjectId el $toObjectId para escribir una nueva colección, en la misma cuestión que creamos la "vista" anteriormente:

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, por supuesto, donde "necesita" mantener la misma colección, entonces el tradicional "bucle y actualización" sigue siendo el mismo que siempre se ha requerido:

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

Lo que en realidad es un poco de "martillo" debido a que en realidad sobrescribe todo el array de una sola vez. No es una gran idea para un entorno de producción, pero es suficiente como una demostración para los propósitos de este ejercicio.

Conclusión

Entonces, aunque MongoDB 4.0 agregará estas características de "lanzamiento" que pueden ser muy útiles, su intención real no es realmente para casos como este. De hecho, son mucho más útiles como se demuestra en la "conversión" a una nueva colección que utiliza una canalización de agregación que la mayoría de los otros posibles usos.

Si bien "podemos" crear una "vista" que transforma los tipos de datos para permitir que cosas como $lookup y $project funcionen donde los datos de recopilación reales $project , esto realmente es solo una "curita" en el problema real, ya que los datos Los tipos realmente no deberían diferir, y de hecho deberían convertirse permanentemente.

El uso de una "vista" en realidad significa que el proceso de agregación para la construcción debe ejecutarse de manera efectiva cada vez que se accede a la "colección" (en realidad una "vista"), lo que crea una sobrecarga real.

Evitar gastos generales suele ser un objetivo de diseño, por lo tanto, corregir dichos errores de almacenamiento de datos es imperativo para obtener un rendimiento real de su aplicación, en lugar de simplemente trabajar con "fuerza bruta" que solo ralentizará las cosas.

Un script de "conversión" mucho más seguro que aplicaba actualizaciones "coincidentes" a cada elemento de la matriz. El código aquí requiere NodeJS v10.x y un controlador de nodo MongoDB 3.1.x de última versión:

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 y ejecuta operaciones masivas como estas para los siete documentos:

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