U gebruikt momenteel een ontwikkelingsversie van MongoDB waarin enkele functies zijn ingeschakeld die naar verwachting met MongoDB 4.0 als officiële release zullen worden uitgebracht. Houd er rekening mee dat sommige functies vóór de definitieve release kunnen worden gewijzigd, dus de productiecode moet hiervan op de hoogte zijn voordat u zich ertoe verbindt.
Waarom $convert hier mislukt
Waarschijnlijk de beste manier om dit uit te leggen, is door naar uw gewijzigde voorbeeld te kijken, maar te vervangen door ObjectId
waarden voor _id
en "strings" voor degenen onder de arrays:
{
"_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" } ]
}
Dat zou een algemene simulatie moeten geven van waarmee u probeerde te werken.
Wat je probeerde was om de _id
. te converteren waarde in een "string" via $project
voordat u de $graphLookup
. invoert fase. De reden dat dit niet lukt, is terwijl je een initieel $project
deed "binnen" deze pijplijn, is het probleem dat de bron voor $graphLookup
in de "from"
optie is nog steeds de ongewijzigde verzameling en daarom krijgt u niet de juiste details bij de daaropvolgende "opzoek"-iteraties.
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"
}}
])
Komt daarom niet overeen met de "lookup":
{
"_id" : "5afe5763419503c46544e277",
"name" : "três",
"ANCESTORS_FROM_BEGINNING" : [ ]
}
Het probleem "patchen"
Dat is echter het kernprobleem en niet het falen van $convert
of het is zelf een alias. Om dit echt te laten werken, kunnen we in plaats daarvan een "weergave" maken die zichzelf presenteert als een verzameling omwille van de invoer.
Ik doe dit andersom en converteer de "strings" naar ObjectId
via $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"
]
}
}}
])
Het gebruik van de "view" betekent echter dat de gegevens consistent worden gezien met de geconverteerde waarden. Dus de volgende aggregatie met behulp van de weergave:
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"
}}
])
Retourneert de verwachte output:
{
"_id" : ObjectId("5afe5763419503c46544e277"),
"name" : "três",
"ANCESTORS_FROM_BEGINNING" : [
ObjectId("5afe5763419503c46544e275"),
ObjectId("5afe5763419503c46544e273"),
ObjectId("5afe5763419503c46544e274"),
ObjectId("5afe5763419503c46544e276"),
ObjectId("5afe5763419503c46544e272")
]
}
Het probleem oplossen
Dat gezegd hebbende, het echte probleem hier is dat je een aantal gegevens hebt die "eruit zien als" een ObjectId
waarde en is in feite geldig als een ObjectId
, maar het is opgenomen als een "string". Het fundamentele probleem dat alles naar behoren werkt, is dat de twee "types" niet hetzelfde zijn en dit resulteert in een gelijkheidsmismatch als de "joins" worden geprobeerd.
Dus de echte oplossing is nog steeds hetzelfde als altijd, namelijk om in plaats daarvan door de gegevens te gaan en deze te repareren zodat de "strings" eigenlijk ook ObjectId
zijn waarden. Deze komen dan overeen met de _id
sleutels waarnaar ze bedoeld zijn, en u bespaart een aanzienlijke hoeveelheid opslagruimte omdat een ObjectId
neemt veel minder ruimte in beslag om op te slaan dan de tekenreeksrepresentatie in hexadecimale tekens.
Als u MongoDB 4.0-methoden gebruikt, kunt u "kon" daadwerkelijk gebruik maken van de "$toObjectId"
om een nieuwe collectie te schrijven, in vrijwel dezelfde kwestie als waarin we eerder de "view" hebben gemaakt:
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" }
])
Of natuurlijk, waar u dezelfde verzameling "moet" houden, blijft de traditionele "loop en update" hetzelfde als wat altijd nodig was:
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 = [];
}
Dat is eigenlijk een beetje een "voorhamer" omdat de hele array in één keer wordt overschreven. Geen geweldig idee voor een productieomgeving, maar genoeg als demonstratie voor deze oefening.
Conclusie
Dus hoewel MongoDB 4.0 deze "casting"-functies zal toevoegen, die inderdaad erg handig kunnen zijn, is hun eigenlijke bedoeling niet echt voor gevallen als deze. Ze zijn in feite veel nuttiger, zoals aangetoond in de "conversie" naar een nieuwe collectie met behulp van een aggregatiepijplijn dan de meeste andere mogelijke toepassingen.
Terwijl we "kunnen" maak een "weergave" die de gegevenstypen transformeert om dingen als $lookup
mogelijk te maken en $graphLookup
om te werken waar de feitelijke verzamelingsgegevens verschillen, is dit echt slechts een "pleister" over het echte probleem, aangezien de gegevenstypen eigenlijk niet zouden moeten verschillen en in feite permanent moeten worden geconverteerd.
Het gebruik van een "view" betekent in feite dat de aggregatiepijplijn voor de bouw effectief elke moet uitvoeren keer dat de "verzameling" (eigenlijk een "weergave") wordt geopend, wat een echte overhead creëert.
Het vermijden van overhead is meestal een ontwerpdoel, daarom is het corrigeren van dergelijke fouten in de gegevensopslag absoluut noodzakelijk om echte prestaties uit uw toepassing te halen, in plaats van alleen met "brute kracht" te werken die de zaken alleen maar zal vertragen.
Een veel veiliger "conversie"-script dat "overeenkomende" updates toepast op elk array-element. De code hier vereist NodeJS v10.x en een MongoDB-knooppuntstuurprogramma 3.1.x van de nieuwste release:
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()
}
})()
Produceert en voert dergelijke bulkbewerkingen uit voor de zeven documenten:
{
"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"
}
]
}
}