Obecnie używasz rozwojowej wersji MongoDB, która ma włączone pewne funkcje, które mają zostać wydane wraz z MongoDB 4.0 jako oficjalna wersja. Pamiętaj, że niektóre funkcje mogą ulec zmianie przed ostatecznym wydaniem, więc kod produkcyjny powinien być o tym świadomy, zanim się do tego zaangażujesz.
Dlaczego $convert tutaj się nie udaje
Prawdopodobnie najlepszym sposobem wyjaśnienia tego jest przyjrzenie się zmienionej próbce, ale zastąpienie jej przez ObjectId
wartości dla _id
i "strings" dla tych pod tablicami:
{
"_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 powinno dać ogólną symulację tego, z czym próbowałeś pracować.
Próbowałeś przekonwertować _id
wartość do „ciągu” za pomocą $project
przed wprowadzeniem $graphLookup
scena. Powodem niepowodzenia jest to, że wykonałeś początkowy $project
"w" tym potoku problem polega na tym, że źródło $graphLookup
w "from"
opcja jest nadal niezmienioną kolekcją i dlatego nie otrzymujesz poprawnych szczegółów dotyczących kolejnych iteracji wyszukiwania.
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"
}}
])
Nie pasuje do „wyszukiwania”, dlatego:
{
"_id" : "5afe5763419503c46544e277",
"name" : "três",
"ANCESTORS_FROM_BEGINNING" : [ ]
}
"Załatanie" problemu
Jednak to jest główny problem, a nie porażka $convert
lub sam jest aliasem. Aby to faktycznie zadziałało, możemy zamiast tego utworzyć „widok”, który prezentuje się jako kolekcja ze względu na dane wejściowe.
Zrobię to na odwrót i przekonwertuję "ciągi" na ObjectId
przez $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"
]
}
}}
])
Użycie „widoku” oznacza jednak, że dane są konsekwentnie widoczne z przekonwertowanymi wartościami. Tak więc następująca agregacja przy użyciu widoku:
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"
}}
])
Zwraca oczekiwany wynik:
{
"_id" : ObjectId("5afe5763419503c46544e277"),
"name" : "três",
"ANCESTORS_FROM_BEGINNING" : [
ObjectId("5afe5763419503c46544e275"),
ObjectId("5afe5763419503c46544e273"),
ObjectId("5afe5763419503c46544e274"),
ObjectId("5afe5763419503c46544e276"),
ObjectId("5afe5763419503c46544e272")
]
}
Rozwiązywanie problemu
Biorąc to wszystko pod uwagę, prawdziwym problemem jest to, że masz pewne dane, które „wyglądają” jak ObjectId
wartość i jest w rzeczywistości prawidłowy jako ObjectId
, jednak został nagrany jako „string”. Podstawowym problemem, który sprawia, że wszystko działa tak, jak powinno, jest to, że te dwa „typy” nie są takie same, co skutkuje niezgodnością równości przy próbach „połączeń”.
Tak więc prawdziwa poprawka jest nadal taka sama, jak zawsze, czyli zamiast tego przejrzeć dane i naprawić je tak, aby "łańcuchy" były w rzeczywistości również ObjectId
wartości. Będą one następnie pasować do _id
klucze, do których mają się odwoływać, a oszczędzasz znaczną ilość miejsca w pamięci, ponieważ ObjectId
zajmuje dużo mniej miejsca do przechowywania niż jego reprezentacja w postaci ciągu znaków w postaci szesnastkowej.
Korzystając z metod MongoDB 4.0, „możesz” faktycznie użyj "$toObjectId"
w celu napisania nowej kolekcji, dokładnie w tej samej sprawie, w której wcześniej stworzyliśmy „widok”:
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" }
])
Lub oczywiście tam, gdzie „trzeba” zachować tę samą kolekcję, tradycyjna „pętla i aktualizacja” pozostaje taka sama, jak zawsze była wymagana:
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 w rzeczywistości jest trochę „młotem”, ponieważ faktycznie nadpisuje całą tablicę za jednym razem. Nie jest to świetny pomysł na środowisko produkcyjne, ale wystarczy jako demonstracja na potrzeby tego ćwiczenia.
Wniosek
Tak więc, podczas gdy MongoDB 4.0 doda te funkcje „przesyłania”, które rzeczywiście mogą być bardzo przydatne, ich faktycznym zamiarem nie jest tak naprawdę dla przypadków takich jak ten. W rzeczywistości są one o wiele bardziej przydatne, jak pokazano w „konwersji” na nową kolekcję przy użyciu potoku agregacji, niż większość innych możliwych zastosowań.
Podczas gdy my „możemy” utwórz "widok", który przekształca typy danych, aby umożliwić takie rzeczy jak $lookup
i $graphLookup
do pracy tam, gdzie rzeczywiste dane są różne, to naprawdę jest tylko "opaskę" na prawdziwym problemie, ponieważ typy danych naprawdę nie powinny się różnić, a w rzeczywistości powinny być trwale przekonwertowane.
Korzystanie z „widoku” w rzeczywistości oznacza, że potok agregacji na potrzeby budowy musi skutecznie działać każdy czas, w którym uzyskuje się dostęp do „kolekcji” (w rzeczywistości „widoku”), co powoduje prawdziwy narzut.
Unikanie narzutów jest zwykle celem projektowym, dlatego poprawienie takich błędów przechowywania danych jest konieczne, aby uzyskać rzeczywistą wydajność aplikacji, a nie tylko pracować z „brute force”, która tylko spowolni działanie.
O wiele bezpieczniejszy skrypt „konwersji”, który stosował „dopasowane” aktualizacje do każdego elementu tablicy. Poniższy kod wymaga NodeJS v10.x i najnowszej wersji sterownika węzła 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()
}
})()
Tworzy i wykonuje operacje zbiorcze, takie jak te dla siedmiu dokumentów:
{
"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"
}
]
}
}