В момента използвате версия за разработка на MongoDB, която има активирани някои функции, които се очаква да бъдат пуснати с MongoDB 4.0 като официална версия. Имайте предвид, че някои функции може да подлежат на промяна преди окончателното издание, така че производственият код трябва да е наясно с това, преди да се ангажирате с него.
Защо $convert не успява тук
Вероятно най-добрият начин да обясните това е да разгледате променената си извадка, но замените с ObjectId
стойности за _id
и "низове" за тези под масивите:
{
"_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" } ]
}
Това трябва да даде обща симулация на това, с което се опитвате да работите.
Това, което се опитахте да конвертирате _id
стойност в "низ" чрез $project
преди да въведете $graphLookup
сцена. Причината за неуспех е докато сте правили първоначален $project
"в" този конвейер, проблемът е, че източникът за $graphLookup
в "from"
опцията все още е непроменената колекция и следователно не получавате правилните подробности за последващите итерации за „търсене“.
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"
}}
])
Не съвпада при "търсене", следователно:
{
"_id" : "5afe5763419503c46544e277",
"name" : "três",
"ANCESTORS_FROM_BEGINNING" : [ ]
}
„Поправяне“ на проблема
Това обаче е основният проблем, а не грешка на $convert
или самият псевдоним. За да накараме това действително да работи, можем вместо това да създадем „изглед“, който се представя като колекция с цел въвеждане.
Ще направя това обратното и ще преобразувам "низовете" в ObjectId
чрез $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"
]
}
}}
])
Използването на „изглед“ обаче означава, че данните се виждат последователно с преобразуваните стойности. Така че следното агрегиране с помощта на изгледа:
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"
}}
])
Връща очаквания изход:
{
"_id" : ObjectId("5afe5763419503c46544e277"),
"name" : "três",
"ANCESTORS_FROM_BEGINNING" : [
ObjectId("5afe5763419503c46544e275"),
ObjectId("5afe5763419503c46544e273"),
ObjectId("5afe5763419503c46544e274"),
ObjectId("5afe5763419503c46544e276"),
ObjectId("5afe5763419503c46544e272")
]
}
Отстраняване на проблема
С всичко казано, истинският проблем тук е, че имате някои данни, които "приличат" на ObjectId
стойност и всъщност е валиден като ObjectId
, но е записан като "низ". Основният проблем за всичко, което работи както трябва, е, че двата „типа“ не са еднакви и това води до несъответствие на равенството при опит за „съединяване“.
Така че истинската корекция все още е същата, както винаги е била, а именно вместо това да преминете през данните и да ги коригирате, така че "низовете" всъщност също са ObjectId
стойности. След това те ще съвпадат с _id
ключове, за които те трябва да се отнасят, и спестявате значително количество място за съхранение, тъй като ObjectId
заема много по-малко място за съхранение, отколкото представянето на низове в шестнадесетични знаци.
Използвайки методите на MongoDB 4.0, вие „можете“ всъщност използвайте "$toObjectId"
за да напишем нова колекция, почти по същия въпрос, който създадохме "изгледа" по-рано:
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" }
])
Или, разбира се, когато „трябва“ да запазите една и съща колекция, тогава традиционният „цикл и актуализиране“ остава същият като това, което винаги е било изисквано:
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 = [];
}
Което всъщност е малко "ковал" поради фактическото презаписване на целия масив с едно движение. Не е страхотна идея за производствена среда, но достатъчна като демонстрация за целите на това упражнение.
Заключение
Така че, докато MongoDB 4.0 ще добави тези "кастинг" функции, които наистина могат да бъдат много полезни, тяхното действително намерение всъщност не е за случаи като този. Те всъщност са много по-полезни, както е показано в „преобразуването“ в нова колекция с помощта на конвейер за агрегация, отколкото повечето други възможни употреби.
Докато ние „можем“ създайте "изглед", който трансформира типовете данни, за да активира неща като $lookup
и $graphLookup
за да работи там, където действителните данни за събиране се различават, това наистина е само "превръзка" относно реалния проблем, тъй като типовете данни наистина не трябва да се различават и всъщност трябва да бъдат постоянно преобразувани.
Използването на „изглед“ всъщност означава, че тръбопроводът за агрегация за изграждане трябва да работи ефективно всички когато се осъществи достъп до „колекцията“ (всъщност „изглед“), което създава реални разходи.
Избягването на излишни разходи обикновено е цел на дизайна, затова коригирането на такива грешки при съхранение на данни е наложително за постигане на реална производителност от вашето приложение, а не просто да работите с „груба сила“, която само ще забави нещата.
Много по-безопасен скрипт за "преобразуване", който прилага "съвпадащи" актуализации към всеки елемент от масива. Кодът тук изисква NodeJS v10.x и най-новата версия на драйвер за възел 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()
}
})()
Произвежда и изпълнява групови операции като тези за седемте документа:
{
"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"
}
]
}
}