von Marc-David Militz
Forum: Neuigkeiten
> db.exp.insertOne({ _id:"x", val1: 100, val2: 200 });
{ "acknowledged" : true, "insertedId" : "x" }
Bisher mussten wir, wenn wir "val1" und "val2" addieren wollten, die beiden Felder addieren und das Dokument updaten, da man im "update" die Felder nicht referenzieren konnte.
> db.exp.update({ _id:"x" }, { $set: { total: 300 } })
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> db.exp.findOne()
{ "_id" : "x", "val1" : 100, "val2" : 200, "total" : 300 }
Das beinhaltet natürlich einen Roundtrip und in der realen Welt, außerhalb dieser Beispiele, wäre es wohl Teil anderer Updates.
Clevere MongoDB Nutzer hätten stattdessen auch die Aggregation Pipeline verwendet, da diese einen "$sum" Operator besitzt, den man dafür nutzen kann.
> db.exp.aggregate( [ { $set: { total: { $sum:[ "$val1","$val2" ] } } } ])
{ "_id" : "x", "val1" : 100, "val2" : 200, "total" : 300 }
Das Ergebnis wird allerdings nicht persistiert.
> db.exp.findOne()
{ "_id" : "x", "val1" : 100, "val2" : 200 }
So wie dieses Minimalbeispiel gebaut wurde würde es auch auf alle Dokumente einer Collection abziehlen.
Wir müssten natürlich einen "$match" Abschnitt einfügen, um den Scope der Anweisung nur auf unser eines Dokument zu setzen.
In MongoDB 4.2 können wir nun die Möglichkeiten der Aggregation Pipeline auch im "update" Befehl nutzen:
> db.exp.update({"_id" : "x" }, [ { $set: { total: { $sum:[ "$val1","$val2" ] } } } ] )
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> db.exp.findOne();
{ "_id" : "x", "val1" : 100, "val2" : 200, "total" : 300 }
Wir verschieben einfach die Aggregation Pipeline in unser Update und die Änderungen werden an dem Dokument durchgeführt.
Der Feldwert, den wir setzen, wird in dem Dokument gespeichert.
Und das Beste dabei ist, dass dies alles ohne zusätzliche Server Roundtrips geschieht.
Das beinhaltet auch die Fähigkeit des Aggregation Frameworks, bedingte Anweisungen wie diese, auszuführen:
> db.exp.update({ _id:"x" }, [
{
"$set": {
"status": {
"$cond": {
"if": { "$gt": ["$total", 200] },
"then": "Over Limit",
"else": "Clear"
}
}
}
}
])
Diese neue Funktionalität ist Teil der Bemühungen der MongoDB Entwickler, die Abfragesprache in den Queries und Aggregationen zu vereinheitlichen und überall die gleichen Möglichkeiten zu bieten.
Wann immer wir künftig also über Verbesserungen im Aggregation Framework sprechen, dann gelten diese ebenso für den "update" und den "findAndModify" Befehl, sofern diese die Aggregation Pipeline nutzen.
Wer sich mit dem Aggregation Framework bereits auskennt, der hat sich vielleicht gewundert wo der "$set" Schritt herkam.
Dieser ist ebenso ein neues Feature in MongoDB 4.2, allerdings nicht ganz so neu.
Es handelt sich um ein Alias für "$addFields" und wurde eingeführt, um Vereinheitlichung der Abfragesprache nahtloser zu gestalten.
Insgesamt gibt es drei aggregationsstufen, die im "update" genutzt werden können, jede davon hat ihren bisherigen Namen sowie einen Aliasnamen.
"$set"/"$addFields", "$unset"/"$project", und "$replaceWith"/"$replaceRoot".
Diese drei Stufen ermöglichen es neue Felder hinzuzufügen, Felder zu entfernen und das komplette Dokument zu ersetzen.
Das ermöglicht alle Aktionen, die man auf ein Dokument anwenden können wollte.
>db.exp.update({"_id" : "x" }, [ { $set: { sin: { $round: [ { $sin:"$val1" } , 5 ] } } } ] )
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> db.exp.findOne();
{
"_id" : "x",
"val1" : 100,
"val2" : 200,
"total" : 300,
"sin" : -0.50637
}
Wir holen uns den Sinuswert unseres "val1" Feldes und runden diesen auf 5 Nachkommastellen, danach speichern wir das Ergebnis wieder im Dokument in das Feld "sin".
> db.reg.insertOne( { text:"Looking for 100 numbers" } )
> db.reg.insertOne( { text:"Digging around in 256 digits" } )
> db.reg.insertOne( { text:"Filtering through 65 characters" } )
Suchen wir nun nach der Zahl, die in den Texten vorkommt, allerdings nur dann, wenn es von einer Referenz zu "numbers" oder "digits" begleitet wird.
Wir benutzen dafür den Ausdruck "([0-9]+) (numbers|digits)".
Das erfasst sowohl die Zahl auch das darauffolgende Wort, sofern es in den Klammern angegeben wurde.
Führen wir die Abfrage aus und sehen uns das Ergebnis an:
> db.reg.aggregate( { $set: { found: { $regexFind: { regex: "([0-9]+) (numbers|digits)", input:"$text" } } } } )
{ "_id" : ObjectId("5d3f059c7baafd6bbd862d47"), "text" : "Looking for 100 numbers", "found" : { "match" : "100 numbers", "idx" : 12, "captures" : [ "100", "numbers" ] } }
{ "_id" : ObjectId("5d3f059c7baafd6bbd862d48"), "text" : "Digging around in 256 digits", "found" : { "match" : "256 digits", "idx" : 18, "captures" : [ "256", "digits" ] } }
{ "_id" : ObjectId("5d3f076f7baafd6bbd862d49"), "text" : "Filtering through 65 characters", "found" : null }
Wenn wir uns das Feld mit den Ergebnissen des regulären Ausdrucks ansehen, dann finden wir mehr als ein einfaches true/false:
"found" : { "match" : "100 numbers", "idx" : 12, "captures" : [ "100", "numbers" ] }
In dem zurückgegebenen Feld "match" bekommen wir den exakten String, der auf unser Suchmuster passt.
Das "idx" Feld zeigt an wie weit im durchsuchten String das Ergebnis gefunden wurde.
Das Feld "captures" enthält schließlich die einzelnen erfassten Teile des übereinstimmenden Strings, als erstes Element die gefundene Zahl und als zweites das Wort "digit" oder "number".
Das ist ideal für komplexes Parsen von Strings.
Wenn es kein Ergebnis gibt, dann gibt "$regexFind" einfach ein "NULL" zurück.
Mit "$regexFind" erhält man lediglich den ersten Treffer zurück.
Wenn man mehrere Treffer zurückbekommen möchte, dann werden mit dem "$regexFindAll" alle übereinstimmenden Treffer ermittelt und das Ergebnis entsprechend als Array zurückgegeben.
Gibt es keine Treffer wird ein leeres Array zurückgegeben.
Wenn man tatsächlich lediglich ein "true" oder "false" als Antwort bekommen möchte, dann kann man dafür "$regexMatch" verwenden.