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