Kapitel 4. abfragen

Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com

Dieses Kapitel befasst sich ausführlich mit Abfragen. Die wichtigsten Bereiche, die behandelt werden, sind die folgenden:

  • Du kannst Bereiche, Mengeneinschlüsse, Ungleichungen und mehr abfragen, indem du $conditionals verwendest.

  • Abfragen geben einen Datenbank-Cursor zurück, der nach und nach Stapel von Dokumenten zurückgibt, wenn du sie brauchst.

  • Es gibt viele Metaoperationen, die du mit einem Cursor durchführen kannst, z. B. das Überspringen einer bestimmten Anzahl von Ergebnissen, die Begrenzung der Anzahl der zurückgegebenen Ergebnisse und die Sortierung der Ergebnisse.

Einführung in die Suche

Die Methode find wird verwendet, um Abfragen in MongoDB durchzuführen. Die Abfrage gibt eine Teilmenge der Dokumente in einer Sammlung zurück, von gar keinen Dokumenten bis hin zur gesamten Sammlung. Welche Dokumente zurückgegeben werden, wird durch das erste Argument von find bestimmt, das ein Dokument ist, das die Abfragekriterien angibt.

Ein leeres Abfragedokument (z. B. {}) passt auf alles in der Sammlung. Wenn find kein Abfragedokument angegeben wird, wird standardmäßig {} verwendet. Zum Beispiel das Folgende:

> db.c.find()

passt auf jedes Dokument in der Sammlung c (und gibt diese Dokumente in Stapeln zurück).

Wenn wir dem Abfragedokument Schlüssel/Wertpaare hinzufügen, schränken wir unsere Suche ein. Das funktioniert bei den meisten Typen ganz einfach: Zahlen entsprechen Zahlen, Boolesche Werte entsprechen Booleschen Werten und Zeichenketten entsprechen Zeichenketten. Die Suche nach einem einfachen Typ ist so einfach wie die Angabe des gesuchten Wertes. Um zum Beispiel alle Dokumente zu finden, in denen der Wert für "age" 27 ist, können wir dieses Schlüssel/Wert-Paar zum Abfragedokument hinzufügen:

> db.users.find({"age" : 27})

Wenn wir eine Zeichenkette haben, die wir abgleichen wollen, z. B. einen "username" Schlüssel mit dem Wert "joe", verwenden wir stattdessen dieses Schlüssel/Wert-Paar:

> db.users.find({"username" : "joe"})

Mehrere Bedingungen können aneinandergereiht werden, indem weitere Schlüssel/Wertpaare zum Abfragedokument hinzugefügt werden, was als "condition1 AND condition2 AND ... AND conditionN." Um zum Beispiel alle 27-jährigen Benutzer mit dem Benutzernamen "joe" zu finden, können wir Folgendes abfragen:

> db.users.find({"username" : "joe", "age" : 27})

Festlegen, welche Schlüssel zurückgegeben werden sollen

Manchmal du nicht alle Schlüssel/Wertpaare eines Dokuments benötigt. In diesem Fall kannst du ein zweites Argument an find (oder findOne) übergeben, in dem du die gewünschten Schlüssel angibst. Dadurch werden sowohl die Datenmenge, die über die Leitung gesendet wird, als auch der Zeit- und Speicherbedarf für die Dekodierung der Dokumente auf der Client-Seite reduziert.

Wenn du zum Beispiel eine Benutzersammlung hast und nur an den Schlüsseln "username" und "email" interessiert bist, kannst du mit der folgenden Abfrage nur diese Schlüssel zurückgeben:

> db.users.find({}, {"username" : 1, "email" : 1})
{
    "_id" : ObjectId("4ba0f0dfd22aa494fd523620"),
    "username" : "joe",
    "email" : "joe@example.com"
}

Wie du in der vorherigen Ausgabe sehen kannst, wird der Schlüssel "_id" standardmäßig zurückgegeben, auch wenn er nicht ausdrücklich angefordert wurde.

Du kannst diesen zweiten Parameter auch verwenden, um bestimmte Schlüssel/Wertpaare aus den Ergebnissen einer Abfrage auszuschließen. Du hast zum Beispiel Dokumente mit einer Vielzahl von Schlüsseln und weißt nur, dass du niemals den Schlüssel "fatal_weakness" zurückgeben möchtest:

> db.users.find({}, {"fatal_weakness" : 0})

Dadurch kann auch verhindert werden, dass "_id"zurückgeschickt wird:

> db.users.find({}, {"username" : 1, "_id" : 0})
{
    "username" : "joe",
}

Einschränkungen

gibt es einige Einschränkungen für Abfragen. Der Wert eines Abfragedokuments muss eine Konstante sein, soweit es die Datenbank betrifft. (Er kann eine normale Variable in deinem eigenen Code sein.) Das heißt, er kann sich nicht auf den Wert eines anderen Schlüssels im Dokument beziehen. Wenn wir zum Beispiel Inventar führen und sowohl "in_stock"als auch "num_sold" als Schlüssel haben, können wir ihre Werte nicht vergleichen, indem wir Folgendes abfragen:

> db.stock.find({"in_stock" : "this.num_sold"}) // doesn't work

Es gibt verschiedene Möglichkeiten, dies zu tun (siehe "$where-Abfragen"), aber in der Regel erhältst du eine bessere Leistung, wenn du dein Dokument leicht umstrukturierst, so dass eine "normale" Abfrage ausreicht. In diesem Beispiel könnten wir stattdessen die Schlüssel "initial_stock" und "in_stock" verwenden. Jedes Mal, wenn jemand einen Artikel kauft, verringern wir den Wert des Schlüssels "in_stock" um eins. Schließlich können wir mit einer einfachen Abfrage prüfen, welche Artikel nicht mehr vorrätig sind:

> db.stock.find({"in_stock" : 0})

Abfragekriterien

Abfragen können über die im vorigen Abschnitt beschriebene exakte Übereinstimmung hinausgehen; sie können komplexere Kriterien wie Bereiche, ODER-Klauseln und Negation abgleichen.

Abfrage-Bedingungen

"$lt" "$lte", und sind alle Vergleichsoperatoren, die jeweils <, <=, > und >= entsprechen. Sie können kombiniert werden, um nach einer Reihe von Werten zu suchen. Um zum Beispiel nach Nutzern zu suchen, die zwischen 18 und 30 Jahre alt sind, können wir so vorgehen: "$gt" "$gte"

> db.users.find({"age" : {"$gte" : 18, "$lte" : 30}})

Dies würde alle Dokumente finden, bei denen das Feld "age" größer oder gleich 18 UND kleiner oder gleich 30 ist.

Diese Arten von Bereichsabfragen sind oft für Daten nützlich. Um zum Beispiel Personen zu finden, die sich vor dem 1. Januar 2007 angemeldet haben, können wir dies tun:

> start = new Date("01/01/2007")
> db.users.find({"registered" : {"$lt" : start}})

Je nachdem, wie du Daten erstellst und speicherst, ist eine exakte Übereinstimmung weniger nützlich, da Daten mit Millisekundengenauigkeit gespeichert werden. Oft willst du einen ganzen Tag, eine Woche oder einen Monat abfragen, was eine Bereichsabfrage erforderlich macht.

Um nach Dokumenten zu suchen, bei denen der Wert eines Schlüssels nicht gleich einem bestimmten Wert ist, musst du einen anderen Bedingungsoperator verwenden, "$ne", der für "nicht gleich" steht. Wenn du alle Benutzer finden willst, die nicht den Benutzernamen "joe" haben, kannst du sie folgendermaßen abfragen:

> db.users.find({"username" : {"$ne" : "joe"}})

"$ne" kann mit jedem Typ verwendet werden.

OR-Abfragen

Es gibt zwei Möglichkeiten, eine OR-Abfrage in MongoDB durchzuführen. "$in" kann verwendet werden, um eine Vielzahl von Werten für einen einzigen Schlüssel abzufragen. "$or" ist allgemeiner; es kann verwendet werden, um jeden der angegebenen Werte über mehrere Schlüssel abzufragen.

Wenn du mehr als einen möglichen Wert für einen einzelnen Schlüssel hast, verwende ein Array von Kriterien mit "$in". Nehmen wir zum Beispiel an, wir veranstalten eine Tombola und die Gewinnnummern der Lose sind 725, 542 und 390. Um alle drei dieser Dokumente zu finden, können wir die folgende Abfrage erstellen:

> db.raffle.find({"ticket_no" : {"$in" : [725, 542, 390]}})

"$in" ist sehr flexibel und ermöglicht es dir, Kriterien verschiedener Typen und Werte anzugeben. Wenn wir zum Beispiel unser Schema schrittweise auf Benutzernamen statt auf Benutzer-ID-Nummern umstellen, können wir mit dieser Funktion nach beiden Kriterien suchen:

> db.users.find({"user_id" : {"$in" : [12345, "joe"]}})

Dies passt zu Dokumenten, deren "user_id" gleich 12345 ist und zu Dokumenten, deren "user_id" gleich "joe" ist.

Wenn "$in" ein Array mit einem einzelnen Wert erhält, verhält es sich genauso wie ein direkter Abgleich mit dem Wert. Zum Beispiel passt {ticket_no : {$in : [725]}}auf die gleichen Dokumente wie {ticket_no : 725}.

Das Gegenteil von "$in" ist "$nin", das Dokumente zurückgibt, die keinem der Kriterien im Array entsprechen. Wenn wir alle Personen zurückgeben wollen, die bei der Verlosung nichts gewonnen haben, können wir sie mit dieser Abfrage finden:

> db.raffle.find({"ticket_no" : {"$nin" : [725, 542, 390]}})

Diese Abfrage liefert alle, die keine Tickets mit diesen Nummern hatten.

"$in" gibt dir eine ODER-Abfrage für einen einzelnen Schlüssel, aber was ist, wenn wir Dokumente finden müssen, bei denen "ticket_no" gleich 725 oder "winner" gleich true ist? Für diese Art der Abfrage müssen wir die "$or" Bedingung verwenden. "$or" nimmt ein Array möglicher Kriterien auf. Im Fall der Verlosung würde die Verwendung von "$or" wie folgt aussehen:

> db.raffle.find({"$or" : [{"ticket_no" : 725}, {"winner" : true}]})

"$or" kann weitere Bedingungen enthalten. Wenn wir zum Beispiel einen der drei "ticket_no" Werte oder den "winner" Schlüssel abgleichen wollen, können wir dies verwenden:

> db.raffle.find({"$or" : [{"ticket_no" : {"$in" : [725, 542, 390]}},
...                        {"winner" : true}]})

Bei einer normalen UND-Abfrage willst du deine Ergebnisse mit möglichst wenigen Argumenten so weit wie möglich eingrenzen. ODER-Abfragen sind das Gegenteil: Sie sind am effizientesten, wenn die ersten Argumente auf so viele Dokumente wie möglich passen.

Obwohl "$or" immer funktioniert, solltest du wann immer möglich "$in" verwenden, da der Abfrageoptimierer damit effizienter umgehen kann.

$nicht

"$not" ist eine Meta-Bedingung: Sie kann zusätzlich zu allen anderen Kriterien angewendet werden. Betrachten wir als Beispiel den Modulus-Operator, "$mod". "$mod" fragt nach Schlüsseln, deren Werte, wenn sie durch den ersten angegebenen Wert geteilt werden, einen Rest des zweiten Wertes ergeben:

> db.users.find({"id_num" : {"$mod" : [5, 1]}})

Die vorherige Abfrage liefert Nutzer mit "id_num"s von 1, 6, 11, 16, und so weiter. Wenn wir stattdessen Benutzer mit "id_num"s von 2, 3, 4, 5, 7, 8, 9, 10, 12, usw. zurückgeben wollen, können wir "$not" verwenden:

> db.users.find({"id_num" : {"$not" : {"$mod" : [5, 1]}}})

"$not" kann besonders in Verbindung mit regulären Ausdrücken nützlich sein, um alle Dokumente zu finden, die nicht mit einem bestimmten Muster übereinstimmen (die Verwendung regulärer Ausdrücke wird unter im Abschnitt "Reguläre Ausdrücke" beschrieben ).

Typspezifische Abfragen

Wie in Kapitel 2 beschrieben, gibt es in MongoDB eine Vielzahl von Typen, die in einem Dokument verwendet werden können. Einige dieser Typen haben ein besonderes Verhalten bei der Abfrage.

null

null verhält sich ein wenig seltsam. Es stimmt mit sich selbst überein. Wenn wir also eine Sammlung mit den folgenden Dokumenten haben:

> db.c.find()
{ "_id" : ObjectId("4ba0f0dfd22aa494fd523621"), "y" : null }
{ "_id" : ObjectId("4ba0f0dfd22aa494fd523622"), "y" : 1 }
{ "_id" : ObjectId("4ba0f148d22aa494fd523623"), "y" : 2 }

können wir auf die erwartete Weise nach Dokumenten suchen, deren "y" Schlüssel null ist:

> db.c.find({"y" : null})
{ "_id" : ObjectId("4ba0f0dfd22aa494fd523621"), "y" : null }

null entspricht aber auch "existiert nicht". Eine Abfrage nach einem Schlüssel mit dem Wert null liefert also alle Dokumente, die diesen Schlüssel nicht enthalten:

> db.c.find({"z" : null})
{ "_id" : ObjectId("4ba0f0dfd22aa494fd523621"), "y" : null }
{ "_id" : ObjectId("4ba0f0dfd22aa494fd523622"), "y" : 1 }
{ "_id" : ObjectId("4ba0f148d22aa494fd523623"), "y" : 2 }

Wenn wir nur Schlüssel finden wollen, deren Wert null ist, können wir mit der Bedingung "$exists" prüfen, ob der Schlüssel null ist und existiert:

> db.c.find({"z" : {"$eq" : null, "$exists" : true}})

Reguläre Ausdrücke

"$regex" bietet reguläre Ausdrücke für den Mustervergleich von Zeichenketten in Abfragen. Reguläre Ausdrücke sind nützlich für den flexiblen Abgleich von Zeichenketten. Wenn wir zum Beispiel alle Benutzer mit dem Namen "Joe" oder "joe" finden wollen, können wir einen regulären Ausdruck verwenden, um die Groß- und Kleinschreibung zu ignorieren:

> db.users.find( {"name" : {"$regex" : /joe/i } })

Flags für reguläre Ausdrücke (z. B. i) sind erlaubt, aber nicht erforderlich. Wenn wir nicht nur verschiedene Großbuchstaben von "joe", sondern auch "joey" finden wollen, können wir unseren regulären Ausdruck weiter verbessern:

> db.users.find({"name" : /joey?/i})

MongoDB verwendet die Perl Compatible Regular Expression (PCRE)-Bibliothek, um reguläre Ausdrücke abzugleichen; jede von PCRE erlaubte Syntax für reguläre Ausdrücke ist in MongoDB erlaubt. Es ist eine gute Idee, deine Syntax mit der JavaScript-Shell zu überprüfen, bevor du sie in einer Abfrage verwendest, um sicherzustellen, dass sie mit dem übereinstimmt, was du glaubst, dass sie passt.

Hinweis

MongoDB kann einen Index für Abfragen nach regulären Ausdrücken mit Präfix verwenden (z. B. /^joey/). Indizes können nicht für die Suche ohne Berücksichtigung der Groß-/Kleinschreibung verwendet werden (/^joey/i). Ein regulärer Ausdruck ist ein "Präfix-Ausdruck", wenn er entweder mit einem Caret (^) oder einem linken Anker (\A) beginnt. Wenn der reguläre Ausdruck eine Groß-/Kleinschreibungsabfrage verwendet und ein Index für das Feld existiert, können die Übereinstimmungen mit den Werten im Index durchgeführt werden. Handelt es sich außerdem um einen Präfix-Ausdruck, kann die Suche auf die Werte innerhalb des Bereichs beschränkt werden, der durch das Präfix im Index gebildet wird.

Reguläre Ausdrücke können auch mit sich selbst übereinstimmen. Nur sehr wenige Menschen fügen reguläre Ausdrücke in die Datenbank ein, aber wenn du einen einfügst, kannst du ihn mit sich selbst abgleichen:

> db.foo.insertOne({"bar" : /baz/})
> db.foo.find({"bar" : /baz/})
{
    "_id" : ObjectId("4b23c3ca7525f35f94b60a2d"),
    "bar" : /baz/
}

Arrays abfragen

Die Abfrage von nach Elementen eines Arrays ist so konzipiert, dass sie sich genauso verhält wie die Abfrage von Skalaren. Wenn das Array zum Beispiel eine Liste von Früchten ist, wie hier:

> db.food.insertOne({"fruit" : ["apple", "banana", "peach"]})

wird die folgende Abfrage erfolgreich mit dem Dokument übereinstimmen:

> db.food.find({"fruit" : "banana"})

Wir können es genauso abfragen, wie wir es tun würden, wenn wir ein Dokument hätten, das wie das (illegale) Dokument {"fruit" : "apple", "fruit" : "banana", "fruit" : "peach"} aussieht.

"$all"

Wenn du Arrays nach mehr als einem Element abgleichen musst, kannst du "$all" verwenden. Damit kannst du eine Liste von Elementen abgleichen. Nehmen wir zum Beispiel an, wir erstellen eine Sammlung mit drei Elementen:

> db.food.insertOne({"_id" : 1, "fruit" : ["apple", "banana", "peach"]})
> db.food.insertOne({"_id" : 2, "fruit" : ["apple", "kumquat", "orange"]})
> db.food.insertOne({"_id" : 3, "fruit" : ["cherry", "banana", "apple"]})

Dann können wir alle Dokumente finden, die sowohl "apple" als auch "banana" enthalten, indem wir mit "$all" abfragen:

> db.food.find({fruit : {$all : ["apple", "banana"]}})
{"_id" : 1, "fruit" : ["apple", "banana", "peach"]}
{"_id" : 3, "fruit" : ["cherry", "banana", "apple"]}

Die Reihenfolge spielt keine Rolle. Beachte, dass "banana" im zweiten Ergebnis vor "apple" steht. Die Verwendung eines Ein-Element-Arrays mit "$all" ist gleichbedeutend mit der Nichtverwendung von "$all". Zum Beispiel passt {fruit : {$all : ['apple']} auf die gleichen Dokumente wie {fruit : 'apple'}.

Du kannst auch eine Abfrage mit exakter Übereinstimmung durchführen, indem du das gesamte Array verwendest. Die exakte Übereinstimmung wird jedoch nicht mit einem Dokument übereinstimmen, wenn einige Elemente fehlen oder überflüssig sind. So wird zum Beispiel das erste unserer drei Dokumente gefunden:

> db.food.find({"fruit" : ["apple", "banana", "peach"]})

Aber das wird es nicht:

> db.food.find({"fruit" : ["apple", "banana"]})

und das hier auch nicht:

> db.food.find({"fruit" : ["banana", "apple", "peach"]})

Wenn du nach einem bestimmten Element eines Arrays suchen willst, kannst du einen Index mit der Syntax key.index:

> db.food.find({"fruit.2" : "peach"})

Arrays sind immer 0-indiziert, also würde dies das dritte Array-Element mit der Zeichenkette "peach" abgleichen.

"$size"

Ein nützliches Conditional für die Abfrage von Arrays ist "$size", mit dem du nach Arrays einer bestimmten Größe suchen kannst. Hier ist ein Beispiel:

> db.food.find({"fruit" : {"$size" : 3}})

Eine häufige Abfrage ist die nach einem Größenbereich. "$size" kann nicht mit einer anderen $ Bedingung (in diesem Beispiel "$gt") kombiniert werden, aber diese Abfrage kann durch Hinzufügen eines "size" Schlüssels zum Dokument erreicht werden. Jedes Mal, wenn du dem Array ein Element hinzufügst, erhöhst du den Wert von "size". Wenn die ursprüngliche Aktualisierung so aussah:

> db.food.update(criteria, {"$push" : {"fruit" : "strawberry"}})

kann sie einfach so geändert werden:

> db.food.update(criteria,
... {"$push" : {"fruit" : "strawberry"}, "$inc" : {"size" : 1}})

Das Inkrementieren ist extrem schnell, so dass der Leistungsverlust vernachlässigbar ist. Wenn du Dokumente auf diese Weise speicherst, kannst du Abfragen wie die folgende durchführen:

> db.food.find({"size" : {"$gt" : 3}})

Leider funktioniert diese Technik nicht so gut mit dem "$addToSet" Operator.

"$Slice"

Wie bereits in diesem Kapitel erwähnt hat, legt das optionale zweite Argument von find die Schlüssel fest, die zurückgegeben werden sollen. Der spezielle "$slice" Operator kann verwendet werden, um eine Teilmenge von Elementen für einen Array-Schlüssel zurückzugeben.

Nehmen wir zum Beispiel an, wir hätten ein Blogpost-Dokument und wollten die ersten 10 Kommentare zurückgeben:

> db.blog.posts.findOne(criteria, {"comments" : {"$slice" : 10}})

Wenn wir die letzten 10 Kommentare haben wollen, können wir alternativ −10 verwenden:

> db.blog.posts.findOne(criteria, {"comments" : {"$slice" : -10}})

"$slice" kann auch Seiten in der Mitte der Ergebnisse zurückgeben, indem es einen Offset und die Anzahl der zurückzugebenden Elemente angibt:

> db.blog.posts.findOne(criteria, {"comments" : {"$slice" : [23, 10]}})

Dies würde die ersten 23 Elemente überspringen und das 24. bis 33. zurückgeben. Wenn es weniger als 33 Elemente im Array gibt, werden so viele wie möglich zurückgegeben.

Wenn nicht anders angegeben, werden bei der Verwendung von "$slice" alle Schlüssel in einem Dokument zurückgegeben. Im Gegensatz zu den anderen Schlüsselangaben werden nicht erwähnte Schlüssel nicht zurückgegeben. Wenn wir zum Beispiel ein Blogpost-Dokument hätten, das wie folgt aussieht:

{
    "_id" : ObjectId("4b2d75476cc613d5ee930164"),
    "title" : "A blog post",
    "content" : "...",
    "comments" : [
        {
            "name" : "joe",
            "email" : "joe@example.com",
            "content" : "nice post."
        },
        {
            "name" : "bob",
            "email" : "bob@example.com",
            "content" : "good post."
        }
    ]
}

und wir eine "$slice" machen, um den letzten Kommentar zu bekommen, würden wir das hier bekommen:

> db.blog.posts.findOne(criteria, {"comments" : {"$slice" : -1}})
{
    "_id" : ObjectId("4b2d75476cc613d5ee930164"),
    "title" : "A blog post",
    "content" : "...",
    "comments" : [
        {
            "name" : "bob",
            "email" : "bob@example.com",
            "content" : "good post."
        }
    ]
}

Sowohl "title" als auch "content" werden zurückgegeben, auch wenn sie nicht explizit in der Schlüsselangabe enthalten sind.

Rückgabe eines passenden Array-Elements

"$slice" ist hilfreich, wenn du den Index des Elements kennst, aber manchmal willst du das Array-Element, das deinen Kriterien entspricht. Du kannst das passende Element mit dem $Operator zurückgeben. Mit dem vorherigen Blog-Beispiel könntest du Bobs Kommentar mit zurückbekommen:

> db.blog.posts.find({"comments.name" : "bob"}, {"comments.$" : 1})
{
    "_id" : ObjectId("4b2d75476cc613d5ee930164"),
    "comments" : [
        {
            "name" : "bob",
            "email" : "bob@example.com",
            "content" : "good post."
        }
    ]
}

Beachte, dass dies nur den ersten Treffer für jedes Dokument zurückgibt: Wenn Bob mehrere Kommentare zu diesem Beitrag hinterlassen hätte, würde nur der erste im Array "comments" zurückgegeben werden.

Interaktionen bei Array- und Bereichsabfragen

Skalare (Nicht-Array-Elemente) in Dokumenten müssen jeder Klausel der Abfragekriterien entsprechen. Wenn du zum Beispiel nach {"x" : {"$gt" : 10, "$lt" : 20}} fragst, muss "x" sowohl größer als 10 als auch kleiner als 20 sein. Wenn das Feld "x" eines Dokuments jedoch ein Array ist, passt das Dokument, wenn es ein Element von "x" gibt, das jedem Teil der Kriterien entspricht , aber jede Abfrageklausel kann einem anderen Array-Element entsprechen.

Der beste Weg, dieses Verhalten zu verstehen, ist ein Beispiel. Angenommen, wir haben die folgenden Dokumente:

{"x" : 5}
{"x" : 15}
{"x" : 25}
{"x" : [5, 25]}

Wenn wir alle Dokumente finden wollen, bei denen "x" zwischen 10 und 20 liegt, könnten wir die Abfrage naiv als db.test.find({"x" : {"$gt" : 10, "$lt" : 20}}) strukturieren und erwarten, dass wir ein Dokument zurückbekommen: {"x" : 15}. Wenn wir dies jedoch ausführen, erhalten wir zwei:

> db.test.find({"x" : {"$gt" : 10, "$lt" : 20}})
{"x" : 15}
{"x" : [5, 25]}

Weder 5 noch 25 liegen zwischen 10 und 20, aber das Dokument wird zurückgegeben, weil 25 mit der ersten Klausel übereinstimmt (es ist größer als 10) und 5 mit der zweiten Klausel (es ist kleiner als 20).

Diese macht Bereichsabfragen gegen Arrays im Grunde nutzlos: Ein Bereich passt zu jedem mehrgliedrigen Array. Es gibt mehrere Möglichkeiten, um das erwartete Verhalten zu erreichen.

Erstens kannst du mit "$elemMatch" verwenden, um MongoDB zu zwingen, beide Klauseln mit einem einzigen Array-Element zu vergleichen. Der Haken an der Sache ist jedoch, dass "$elemMatch" keine Nicht-Array-Elemente vergleichen kann:

> db.test.find({"x" : {"$elemMatch" : {"$gt" : 10, "$lt" : 20}}})
> // no results

Das Dokument {"x" : 15} stimmt nicht mehr mit der Abfrage überein, da das Feld "x" kein Array ist. Allerdings solltest du einen guten Grund dafür haben, Array- und Einzelwerte in einem Feld zu mischen. Viele Anwendungsfälle erfordern keine Vermischung. Für diese Fälle bietet "$elemMatch" eine gute Lösung für Bereichsabfragen auf Array-Elementen.

Wenn du einen Index über das Feld hast, das du abfragst (siehe Kapitel 5), kannst du min und max verwenden, um den Indexbereich, der von der Abfrage durchlaufen wird, auf deine Werte "$gt" und "$lt" zu beschränken:

> db.test.find({"x" : {"$gt" : 10, "$lt" : 20}}).min({"x" : 10}).max({"x" : 20})
{"x" : 15}

Dadurch wird der Index nur von 10 bis 20 durchlaufen, wobei die Einträge 5 und 25 fehlen. Du kannst min und max allerdings nur verwenden, wenn du einen Index für das Feld hast, das du abfragen willst, und du musst alle Felder des Index an min und max übergeben.

Die Verwendung von min und max bei der Abfrage von Bereichen über Dokumente, die Arrays enthalten können, ist generell eine gute Idee. Die Indexgrenzen für eine "$gt"/"$lt" Abfrage über ein Array sind ineffizient. Sie akzeptiert im Grunde jeden Wert und durchsucht daher alle Indexeinträge, nicht nur die im Bereich.

Abfragen von eingebetteten Dokumenten

Es gibt zwei Möglichkeiten, ein eingebettetes Dokument abzufragen: die Abfrage des gesamten Dokuments oder die Abfrage der einzelnen Schlüssel/Wertpaare.

Die Abfrage nach einem ganzen eingebetteten Dokument funktioniert genauso wie eine normale Abfrage. Wenn wir zum Beispiel ein Dokument haben, das wie folgt aussieht:

{
    "name" : {
        "first" : "Joe",
        "last" : "Schmoe"
    },
    "age" : 45
}

können wir nach jemandem namens Joe Schmoe mit den folgenden Angaben suchen:

> db.people.find({"name" : {"first" : "Joe", "last" : "Schmoe"}})

Eine Abfrage für ein vollständiges Unterdokument muss jedoch genau mit dem Unterdokument übereinstimmen. Wenn Joe beschließt, ein Feld für den mittleren Namen hinzuzufügen, funktioniert diese Abfrage plötzlich nicht mehr; sie stimmt nicht mit dem gesamten eingebetteten Dokument überein! Diese Art der Abfrage ist auch von der Reihenfolge abhängig: {"last" : "Schmoe", "first" : "Joe"} wäre keine Übereinstimmung.

Wenn möglich, ist es eine gute Idee, nur nach einem bestimmten Schlüssel oder mehreren Schlüsseln eines eingebetteten Dokuments zu suchen. Wenn sich dein Schema ändert, werden nicht plötzlich alle deine Abfragen abgebrochen, weil sie nicht mehr exakt übereinstimmen. Du kannst nach eingebetteten Schlüsseln mit der Punktnotation suchen:

> db.people.find({"name.first" : "Joe", "name.last" : "Schmoe"})

Wenn Joe nun weitere Schlüssel hinzufügt, wird diese Abfrage immer noch mit seinem Vor- und Nachnamen übereinstimmen.

Diese Punktschreibweise ist der Hauptunterschied zwischen Abfragedokumenten und anderen Dokumenttypen. Abfragedokumente können Punkte enthalten, die "in ein eingebettetes Dokument greifen" bedeuten. Die Punktnotation ist auch der Grund dafür, dass Dokumente, die eingefügt werden sollen, nicht das Zeichen . enthalten können. Oft stößt man auf auf diese Einschränkung, wenn man versucht, URLs als Schlüssel zu speichern. Eine Möglichkeit, dies zu umgehen, besteht darin, vor dem Einfügen oder nach dem Abrufen immer ein globales Ersetzen durchzuführen und das Punktzeichen durch ein Zeichen zu ersetzen, das in URLs nicht zulässig ist.

Eingebettete Dokumentabgleiche können etwas komplizierter werden, wenn die Dokumentstruktur komplizierter wird. Nehmen wir zum Beispiel an, wir speichern Blog-Beiträge und wollen Kommentare von Joe finden, die mindestens mit 5 Punkten bewertet wurden. Wir könnten den Beitrag wie folgt modellieren:

> db.blog.find()
{
    "content" : "...",
    "comments" : [
        {
            "author" : "joe",
            "score" : 3,
            "comment" : "nice post"
        },
        {
            "author" : "mary",
            "score" : 6,
            "comment" : "terrible post"
        }
    ]
}

Jetzt können wir nicht mit db.blog.find({"comments" : {"author" : "joe", "score" : {"$gte" : 5}}}) abfragen. Eingebettete Dokumententreffer müssen dem gesamten Dokument entsprechen, und das passt nicht zum Schlüssel "comment". Auch db.blog.find({"comments.author" : "joe", "comments.score" : {"$gte" : 5}}) würde nicht funktionieren, weil das Autorenkriterium mit einem anderen Kommentar übereinstimmen könnte als das Punktekriterium. Das heißt, es würde das oben gezeigte Dokument zurückgeben: Es würde "author" : "joe" im ersten Kommentar und "score" : 6 im zweiten Kommentar entsprechen.

Um Kriterien korrekt zu gruppieren, ohne dass du jeden Schlüssel angeben musst, verwende "$elemMatch". Mit dieser vage benannten Bedingung kannst du teilweise Kriterien angeben, um ein einzelnes eingebettetes Dokument in einem Array zu finden. Die korrekte Abfrage sieht so aus:

> db.blog.find({"comments" : {"$elemMatch" : 
... {"author" : "joe", "score" : {"$gte" : 5}}}})

"$elemMatch" ermöglicht es dir, deine Kriterien zu "gruppieren". Sie wird nur benötigt, wenn du mehr als einen Schlüssel in einem eingebetteten Dokument abgleichen möchtest.

$where Abfragen

Schlüssel/Wert-Paare sind eine ziemlich ausdrucksstarke Art der Abfrage, aber es gibt einige Abfragen, die sie nicht darstellen können. Für Abfragen, die auf andere Weise nicht möglich sind, gibt es "$where"Klauseln, mit denen du beliebiges JavaScript als Teil deiner Abfrage ausführen kannst. Damit kannst du (fast) alles innerhalb einer Abfrage tun. Aus Sicherheitsgründen sollte die Verwendung von "$where" Klauseln stark eingeschränkt oder ganz unterbunden werden. Endnutzer sollten niemals die Möglichkeit haben, beliebige "$where"Klauseln auszuführen.

Der häufigste Fall, in dem "$where" verwendet wird, ist der Vergleich der Werte für zwei Schlüssel in einem Dokument. Nehmen wir zum Beispiel an, wir haben Dokumente, die wie folgt aussehen:

> db.foo.insertOne({"apple" : 1, "banana" : 6, "peach" : 3})
> db.foo.insertOne({"apple" : 8, "spinach" : 4, "watermelon" : 4})

Wir möchten Dokumente zurückgeben, bei denen zwei der Felder gleich sind. Zum Beispiel haben im zweiten Dokument "spinach" und "watermelon" den gleichen Wert, also möchten wir dieses Dokument zurückgeben. Es ist unwahrscheinlich, dass MongoDB dafür eine $ Bedingung hat, also können wir eine "$where" Klausel verwenden, um dies mit JavaScript zu erreichen:

> db.foo.find({"$where" : function () {
... for (var current in this) {
...     for (var other in this) {
...         if (current != other && this[current] == this[other]) {
...             return true;
...         }
...     }
... }
... return false;
... }});

Wenn die Funktion true zurückgibt, ist das Dokument Teil der Ergebnismenge; wenn sie false zurückgibt, ist es nicht dabei.

"$where" Abfragen sollten nur verwendet werden, wenn es unbedingt notwendig ist: Sie sind viel langsamer als reguläre Abfragen. Jedes Dokument muss von BSON in ein JavaScript-Objekt umgewandelt werden und dann den "$where"Ausdruck durchlaufen. Auch Indizes können nicht verwendet werden, um eine "$where" zu erfüllen. Daher solltest du "$where" nur dann verwenden, wenn es keine andere Möglichkeit gibt, die Abfrage durchzuführen. Du kannst die Strafe verringern, indem du andere Abfragefilter in Kombination mit "$where" verwendest. Wenn möglich, wird ein Index verwendet, um auf der Grundlage der nicht$where Klauseln zu filtern; der "$where" Ausdruck wird nur zur Feinabstimmung der Ergebnisse verwendet. Mit MongoDB 3.6 wurde der $exprOperator hinzugefügt, der die Verwendung von Aggregationsausdrücken mit der MongoDB-Abfragesprache ermöglicht. Er ist schneller als $where, da er kein JavaScript ausführt, und wird, wenn möglich, als Ersatz für diesen Operator empfohlen.

Eine andere Möglichkeit, komplexe Abfragen durchzuführen, ist die Verwendung eines der Aggregationswerkzeuge, die in Kapitel 7 behandelt werden.

Cursors

Die Datenbank gibt die Ergebnisse von find mithilfe eines Cursors zurück. Die clientseitigen Implementierungen von Cursors ermöglichen es dir im Allgemeinen, viel über die Ausgabe einer Abfrage zu bestimmen. Du kannst die Anzahl der Ergebnisse begrenzen, eine bestimmte Anzahl von Ergebnissen überspringen, die Ergebnisse nach einer beliebigen Kombination von Schlüsseln in eine beliebige Richtung sortieren und eine Reihe anderer leistungsstarker Operationen durchführen.

Auf erstellst du mit der Shell einen Cursor, legst einige Dokumente in einer Sammlung ab, führst eine Abfrage durch und weist die Ergebnisse einer lokalen Variablen zu (Variablen, die mit "var" definiert wurden, sind lokal). Hier erstellen wir eine sehr einfache Sammlung, fragen sie ab und speichern die Ergebnisse in der Variablen cursor:

> for(i=0; i<100; i++) {
...     db.collection.insertOne({x : i});
... }
> var cursor = db.collection.find();

Das hat den Vorteil, dass du dir jeweils nur ein Ergebnis ansehen kannst. Wenn du die Ergebnisse in einer globalen Variable oder gar keiner Variable speicherst, wird die MongoDB-Shell automatisch die ersten paar Dokumente durchgehen und anzeigen. Das ist das, was wir bis jetzt gesehen haben, und es ist oft das Verhalten, das du dir wünschst, um zu sehen, was in einer Sammlung ist, aber nicht, um mit der Shell zu programmieren.

Um durch die Ergebnisse zu iterieren, kannst du die Methode next auf den Cursor anwenden. Du kannst hasNext verwenden, um zu prüfen, ob es ein weiteres Ergebnis gibt. Ein typisches Durchlaufergebnis sieht wie folgt aus:

> while (cursor.hasNext()) {
...     obj = cursor.next();
...     // do stuff
... }

cursor.hasNext() prüft, ob das nächste Ergebnis existiert, und cursor.next()holt es ab.

Die Klasse cursor implementiert auch die Iterator-Schnittstelle von JavaScript, sodass du sie in einer forEach Schleife verwenden kannst:

> var cursor = db.people.find();
> cursor.forEach(function(x) {
...     print(x.name);
... });
adam
matt
zak

Wenn du find aufrufst, fragt die Shell die Datenbank nicht sofort ab. Sie wartet, bis du anfängst, Ergebnisse anzufordern, um die Abfrage zu senden. So kannst du zusätzliche Optionen an eine Abfrage ketten, bevor sie ausgeführt wird. Fast jede Methode eines cursor Objekts gibt den Cursor selbst zurück, so dass du Optionen in beliebiger Reihenfolge aneinanderreihen kannst. Die folgenden Methoden sind zum Beispiel alle gleichwertig:

> var cursor = db.foo.find().sort({"x" : 1}).limit(1).skip(10);
> var cursor = db.foo.find().limit(1).sort({"x" : 1}).skip(10);
> var cursor = db.foo.find().skip(10).limit(1).sort({"x" : 1});

Zu diesem Zeitpunkt ist die Abfrage noch nicht ausgeführt worden. Alle diese Funktionen bauen lediglich die Abfrage auf. Nehmen wir nun an, wir rufen Folgendes auf:

> cursor.hasNext()

Unter wird die Abfrage an den Server gesendet. Die Shell holt die ersten 100 Ergebnisse oder die ersten 4 MB an Ergebnissen (je nachdem, was kleiner ist) auf einmal ab, damit die nächsten Aufrufe von next oder hasNext nicht zum Server gehen müssen. Nachdem der Client den ersten Satz von Ergebnissen durchlaufen hat, kontaktiert die Shell erneut die Datenbank und fragt mit einer getMore Anfrage nach weiteren Ergebnissen. getMore Anfragen enthalten im Wesentlichen eine Kennung für den Cursor und fragen die Datenbank, ob es weitere Ergebnisse gibt, und geben gegebenenfalls den nächsten Stapel zurück. Dieser Vorgang wird so lange fortgesetzt, bis der Cursor erschöpft ist und alle Ergebnisse zurückgegeben wurden.

Grenzwerte, Auslassungen und Sortierungen

Die häufigsten Abfrageoptionen sind die Begrenzung der Anzahl der zurückgegebenen Ergebnisse, das Überspringen einer Reihe von Ergebnissen und die Sortierung. Alle diese Optionen müssen hinzugefügt werden, bevor eine Abfrage an die Datenbank gesendet wird.

Um eine Grenze zu setzen, kette die Funktion limit an deinen Aufruf von find. Wenn du zum Beispiel nur drei Ergebnisse zurückgeben willst, kannst du dies verwenden:

> db.c.find().limit(3)

Wenn es in der Sammlung weniger als drei Dokumente gibt, die deiner Abfrage entsprechen, wird nur die Anzahl der übereinstimmenden Dokumente zurückgegeben; limit setzt eine Obergrenze, keine Untergrenze.

skip funktioniert ähnlich wie limit:

> db.c.find().skip(3)

Dadurch werden die ersten drei übereinstimmenden Dokumente übersprungen und der Rest der Treffer zurückgegeben. Wenn sich weniger als drei Dokumente in deiner Sammlung befinden, werden keine Dokumente zurückgegeben.

sort benötigt ein Objekt: eine Reihe von Schlüssel/Wert-Paaren, wobei die Schlüssel die Schlüsselnamen und die Werte die Sortierrichtungen sind. Die Sortierrichtung kann 1 (aufsteigend) oder −1 (absteigend) sein. Wenn mehrere Schlüssel angegeben werden, werden die Ergebnisse in dieser Reihenfolge sortiert. Um die Ergebnisse zum Beispiel nach "username" aufsteigend und "age" absteigend zu sortieren, gehen wir wie folgt vor:

> db.c.find().sort({username : 1, age : -1})

Diese drei Methoden können kombiniert werden. Das ist oft praktisch für die Paginierung. Nehmen wir zum Beispiel an, du betreibst einen Online-Shop und jemand sucht nach mp3. Wenn du 50 Ergebnisse pro Seite haben möchtest, die nach Preis sortiert sind, kannst du Folgendes tun:

> db.stock.find({"desc" : "mp3"}).limit(50).sort({"price" : -1})

Wenn die Person auf "Nächste Seite" klickt, um mehr Ergebnisse zu sehen, kannst du einfach eine Überspringung in die Abfrage einfügen, die die ersten 50 Treffer überspringt (die der Nutzer bereits auf Seite 1 gesehen hat):

> db.stock.find({"desc" : "mp3"}).limit(50).skip(50).sort({"price" : -1})

Große Sprünge sind jedoch nicht sehr leistungsfähig; im nächsten Abschnitt gibt es Vorschläge, wie man sie vermeiden kann.

Vergleich Bestellung

In MongoDB gibt es eine Hierarchie, wie Typen verglichen werden. Manchmal gibt es einen einzigen Schlüssel mit mehreren Typen: zum Beispiel Ganzzahlen und Boolesche Werte oder Strings und Nullen. Wenn du einen Schlüssel mit verschiedenen Typen sortierst, gibt es eine vordefinierte Reihenfolge, in der sie sortiert werden. Diese Reihenfolge ist vom kleinsten zum größten Wert wie folgt:

  1. Mindestwert

  2. Null

  3. Zahlen (ganze Zahlen, lange Zahlen, Doppelzahlen, Dezimalzahlen)

  4. Strings

  5. Objekt/Dokument

  6. Array

  7. Binäre Daten

  8. Objekt-ID

  9. Boolesche

  10. Datum

  11. Zeitstempel

  12. Regulärer Ausdruck

  13. Maximaler Wert

Vermeiden von großen Sprüngen

Die Verwendung von skip für eine kleine Anzahl von Dokumenten ist in Ordnung. Aber bei einer großen Anzahl von Ergebnissen kann skip langsam sein, da es alle übersprungenen Ergebnisse finden und dann verwerfen muss. Die meisten Datenbanken halten mehr Metadaten im Index, um bei Übersprüngen zu helfen, aber MongoDB unterstützt dies noch nicht, daher sollten große Übersprünge vermieden werden. Oft kannst du die Ergebnisse der nächsten Abfrage auf der Grundlage der vorherigen Abfrage berechnen.

Ergebnisse ohne Überspringen paginieren

Die einfachste Art der Paginierung ist, die erste Seite der Ergebnisse mit limit zurückzugeben und dann jede weitere Seite als Offset vom Anfang zurückzugeben:

> // do not use: slow for large skips
> var page1 = db.foo.find(criteria).limit(100)
> var page2 = db.foo.find(criteria).skip(100).limit(100)
> var page3 = db.foo.find(criteria).skip(200).limit(100)
...

Abhängig von deiner Abfrage kannst du jedoch in der Regel einen Weg finden, ohne Überspringen zu paginieren. Nehmen wir zum Beispiel an, wir wollen die Dokumente in absteigender Reihenfolge nach "date" anzeigen. Wir können die erste Ergebnisseite mit folgendem Befehl erhalten:

> var page1 = db.foo.find().sort({"date" : -1}).limit(100)

Wenn das Datum eindeutig ist, können wir den "date" Wert des letzten Dokuments als Kriterium für den Abruf der nächsten Seite verwenden:

var latest = null;

// display first page
while (page1.hasNext()) {
   latest = page1.next();
   display(latest);
}

// get next page
var page2 = db.foo.find({"date" : {"$lt" : latest.date}});
page2.sort({"date" : -1}).limit(100);

Jetzt muss die Abfrage keine Überspringung enthalten.

Suche nach einem zufälligen Dokument

Ein recht häufiges Problem ist, wie man ein zufälliges Dokument aus einer Sammlung erhält. Die naive (und langsame) Lösung besteht darin, die Anzahl der Dokumente zu zählen und dann find eine zufällige Anzahl von Dokumenten zwischen Null und der Größe der Sammlung zu überspringen:

> // do not use
> var total = db.foo.count()
> var random = Math.floor(Math.random()*total)
> db.foo.find().skip(random).limit(1)

Es ist eigentlich sehr ineffizient, auf diese Weise ein Zufallselement zu erhalten: Du musst eine Zählung durchführen (was teuer sein kann, wenn du Kriterien verwendest), und das Überspringen einer großen Anzahl von Elementen kann zeitaufwändig sein.

Es erfordert ein wenig Voraussicht, aber wenn du weißt, dass du ein zufälliges Element in einer Sammlung suchen wirst, gibt es einen viel effizienteren Weg, dies zu tun. Der Trick besteht darin, jedem Dokument einen zusätzlichen Zufallsschlüssel hinzuzufügen, wenn es eingefügt wird. Wenn wir zum Beispiel die Shell benutzen, können wir die Funktion Math.random() verwenden (die eine Zufallszahl zwischen 0 und 1 erzeugt):

> db.people.insertOne({"name" : "joe", "random" : Math.random()})
> db.people.insertOne({"name" : "john", "random" : Math.random()})
> db.people.insertOne({"name" : "jim", "random" : Math.random()})

Wenn wir nun ein zufälliges Dokument aus der Sammlung finden wollen, können wir eine Zufallszahl berechnen und diese als Abfragekriterium verwenden, anstatt skip zu verwenden:

> var random = Math.random()
> result = db.people.findOne({"random" : {"$gt" : random}})

Es besteht eine geringe Chance, dass random größer ist als einer der "random"Werte in der Sammlung und keine Ergebnisse zurückgegeben werden. Wir können dem vorbeugen, indem wir einfach ein Dokument in die andere Richtung zurückgeben:

> if (result == null) {
...     result = db.people.findOne({"random" : {"$lte" : random}})
... }

Wenn es keine Dokumente in der Sammlung gibt, wird diese Technik am Ende null zurückgeben, was sinnvoll ist.

Diese Technik kann für beliebig komplexe Abfragen verwendet werden. Du musst nur darauf achten, dass du einen Index hast, der den Zufallsschlüssel enthält. Wenn wir zum Beispiel einen zufälligen Klempner in Kalifornien finden wollen, können wir einen Index auf "profession", "state" und "random" erstellen :

> db.people.ensureIndex({"profession" : 1, "state" : 1, "random" : 1})

So können wir schnell ein zufälliges Ergebnis finden (siehe Kapitel 5 für weitere Informationen zur Indizierung).

Unsterbliche Flüche

ein Cursor hat zwei Seiten: den Client-Cursor und den Datenbank-Cursor, den der Client-Cursor darstellt. Bis jetzt haben wir über den clientseitigen Cursor gesprochen, aber jetzt werfen wir einen kurzen Blick darauf, was auf dem Server passiert.

Auf der Serverseite nimmt ein Cursor Speicher und Ressourcen in Anspruch. Sobald ein Cursor keine Ergebnisse mehr liefert oder der Client ihm mitteilt, dass er sterben soll, kann die Datenbank die Ressourcen, die er verbraucht hat, freigeben. Wenn die Datenbank diese Ressourcen freigibt, kann sie sie für andere Dinge nutzen. Das ist gut, deshalb wollen wir sicherstellen, dass Cursor schnell freigegeben werden können (in einem vernünftigen Rahmen).

Es gibt eine Reihe von Bedingungen, die das Ende eines Cursors (und die anschließende Bereinigung) verursachen können. Erstens: Wenn ein Cursor die Iteration durch die übereinstimmenden Ergebnisse abgeschlossen hat, löscht er sich selbst. Wenn ein Cursor auf der Client-Seite den Gültigkeitsbereich verlässt, senden die Treiber eine spezielle Nachricht an die Datenbank, um sie darüber zu informieren, dass sie den Cursor löschen kann. Und selbst wenn der Benutzer noch nicht alle Ergebnisse durchlaufen hat und der Cursor noch im Gültigkeitsbereich ist, wird ein Datenbankcursor nach 10 Minuten Inaktivität automatisch "gelöscht". Auf diese Weise wird MongoDB nicht mit Tausenden von offenen Cursors zurückgelassen, wenn ein Client abstürzt oder fehlerhaft ist.

Dieser "Tod durch Zeitüberschreitung" ist normalerweise das gewünschte Verhalten: Die wenigsten Anwendungen erwarten, dass ihre Nutzer/innen minutenlang auf Ergebnisse warten. Manchmal kann es jedoch vorkommen, dass ein Cursor sehr lange benötigt wird. Für diesen Fall haben viele Treiber eine Funktion namens immortal oder einen ähnlichen Mechanismus implementiert, der die Datenbank anweist, den Cursor nicht auslaufen zu lassen. Wenn du die Zeitüberschreitung eines Cursors ausschaltest, musst du alle Ergebnisse durchgehen oder ihn beenden, um sicherzustellen, dass er geschlossen wird. Andernfalls bleibt er in der Datenbank und verbraucht Ressourcen, bis der Server neu gestartet wird.

Get MongoDB: Das ultimative Handbuch, 3. Auflage now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.