V současné době používáte vývojovou verzi MongoDB, která má některé povolené funkce a očekává se, že bude vydána s MongoDB 4.0 jako oficiální vydání. Upozorňujeme, že některé funkce se mohou před konečným vydáním změnit, takže produkční kód by si toho měl být vědom, než se k tomu zavážete.
Proč zde $convert selže
Pravděpodobně nejlepší způsob, jak to vysvětlit, je podívat se na svůj upravený vzorek, ale nahradit jej ObjectId
hodnoty pro _id
a "řetězce" pro ty pod poli:
{
"_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" } ]
}
To by mělo poskytnout obecnou simulaci toho, s čím jste se snažili pracovat.
Pokusili jste se převést _id
hodnotu do "řetězce" prostřednictvím $project
před zadáním $graphLookup
etapa. Důvodem je, že jste provedli počáteční $project
„v rámci“ tohoto kanálu je problém v tom, že zdroj pro $graphLookup
v "from"
volba je stále nezměněná kolekce, a proto nezískáte správné podrobnosti o následných iteracích „vyhledání“.
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"
}}
])
Neodpovídá na "vyhledání", proto:
{
"_id" : "5afe5763419503c46544e277",
"name" : "três",
"ANCESTORS_FROM_BEGINNING" : [ ]
}
„Oprava“ problému
To je však hlavní problém a ne selhání $convert
nebo je to samotný alias. Aby to skutečně fungovalo, můžeme místo toho vytvořit "pohled", který se prezentuje jako sbírka pro vstup.
Udělám to naopak a převedu „řetězce“ na ObjectId
přes $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"
]
}
}}
])
Použití "zobrazení" však znamená, že data jsou konzistentně vidět s převedenými hodnotami. Takže následující agregace pomocí zobrazení:
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"
}}
])
Vrátí očekávaný výstup:
{
"_id" : ObjectId("5afe5763419503c46544e277"),
"name" : "três",
"ANCESTORS_FROM_BEGINNING" : [
ObjectId("5afe5763419503c46544e275"),
ObjectId("5afe5763419503c46544e273"),
ObjectId("5afe5763419503c46544e274"),
ObjectId("5afe5763419503c46544e276"),
ObjectId("5afe5763419503c46544e272")
]
}
Oprava problému
Když už bylo řečeno, skutečný problém je v tom, že máte nějaká data, která „vypadají jako“ ObjectId
a je ve skutečnosti platný jako ObjectId
, nicméně byl zaznamenán jako "řetězec". Základním problémem, aby vše fungovalo tak, jak má, je to, že dva „typy“ nejsou stejné, což vede k nesouladu rovnosti při pokusu o „spojení“.
Takže skutečná oprava je stále stejná jako vždy, což je místo toho projít data a opravit je tak, aby "řetězce" byly ve skutečnosti také ObjectId
hodnoty. Ty pak budou odpovídat _id
klíče, na které mají odkazovat, a ušetříte značné množství úložného prostoru, protože ObjectId
zabírá mnohem méně místa k uložení než jeho řetězcová reprezentace v hexadecimálních znacích.
Pomocí metod MongoDB 4.0 byste "mohli" ve skutečnosti použijte "$toObjectId"
abychom mohli napsat novou sbírku, v podstatě ve stejné věci, v jaké jsme dříve vytvořili "pohled":
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" }
])
Nebo samozřejmě tam, kde „potřebujete“ zachovat stejnou sbírku, pak tradiční „zacyklení a aktualizace“ zůstane stejné jako to, co bylo vždy vyžadováno:
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 = [];
}
Což je ve skutečnosti trochu „práska“ kvůli vlastně přepsání celého pole na jeden zátah. Není to skvělý nápad pro produkční prostředí, ale jako ukázka pro účely tohoto cvičení to stačí.
Závěr
Takže zatímco MongoDB 4.0 přidá tyto „castingové“ funkce, které mohou být skutečně velmi užitečné, jejich skutečný záměr není ve skutečnosti pro případy, jako je tento. Jsou ve skutečnosti mnohem užitečnější, jak se ukázalo při „převodu“ na novou kolekci pomocí agregačního kanálu, než většina jiných možných použití.
Zatímco my "můžeme" vytvořit "view", který transformuje datové typy, aby umožnil věci jako $lookup
a $graphLookup
pracovat tam, kde se skutečná data sběru liší, je to opravdu jen "náplast" na skutečný problém, protože datové typy by se ve skutečnosti neměly lišit a ve skutečnosti by měly být trvale převedeny.
Použití „zobrazení“ ve skutečnosti znamená, že agregační kanál pro výstavbu musí efektivně běžet každý čas přístupu ke „sbírce“ (ve skutečnosti „pohledu“), což vytváří skutečnou režii.
Cílem návrhu je obvykle vyhnout se režii, proto je oprava takových chyb při ukládání dat nezbytná pro získání skutečného výkonu z vaší aplikace, spíše než jen práce s „hrubou silou“, která věci pouze zpomalí.
Mnohem bezpečnější „konverzní“ skript, který aplikoval „odpovídající“ aktualizace na každý prvek pole. Zde uvedený kód vyžaduje NodeJS v10.x a nejnovější verzi ovladače uzlu 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()
}
})()
Vytváří a provádí hromadné operace, jako jsou tyto, pro sedm dokumentů:
{
"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"
}
]
}
}