In diesem Leitfaden werden einige der Schlüsselkonzepte der Datenarchitektur behandelt und Best Practices für die Strukturierung der JSON-Daten in Ihrer Firebase Realtime Database.
Der Aufbau einer richtig strukturierten Datenbank erfordert einiges an Voraussicht. Am wichtigsten ist es, zu planen, wie Daten gespeichert und später abgerufen werden, um diesen Vorgang so einfach wie möglich zu gestalten.
Wie Daten strukturiert sind: ein JSON-Baum
Alle Daten der Firebase-Echtzeitdatenbank werden als JSON-Objekte gespeichert. Sie können sich die Datenbank als eine cloud-gehostete JSON-Baumstruktur vorstellen. Im Gegensatz zu einer SQL-Datenbank gibt es keine Tabellen oder Datensätze. Wenn Sie der JSON-Baumstruktur Daten hinzufügen, werden diese zu einem Knoten in der vorhandenen JSON-Baumstruktur mit einem verknüpften Schlüssel. Sie können Ihre eigenen Schlüssel angeben, wie Nutzer-IDs oder semantische Namen. Sie können auch mit push()
bereitgestellt werden.
Wenn Sie eigene Schlüssel erstellen, müssen diese UTF-8-codiert sein, dürfen maximal 768 Byte lang sein und dürfen keine der Zeichen .
, $
, #
, [
, ]
, /
oder die ASCII-Kontrollzeichen 0–31 oder 127 enthalten. Auch in den Werten selbst dürfen keine ASCII-Steuerzeichen verwendet werden.
Stellen Sie sich z. B. eine Chat-Anwendung vor, mit der Nutzende eine einfache
und Kontaktliste. Ein typisches Nutzerprofil befindet sich unter folgendem Pfad:
/users/$uid
Der Nutzer alovelace
könnte einen Datenbankeintrag haben, der in etwa so aussieht:
{
"users": {
"alovelace": {
"name": "Ada Lovelace",
"contacts": { "ghopper": true },
},
"ghopper": { ... },
"eclarke": { ... }
}
}
Obwohl in der Datenbank ein JSON-Baum verwendet wird, können die darin gespeicherten Daten als bestimmte native Typen dargestellt werden, die den verfügbaren JSON-Typen entsprechen. So können Sie leichter wartbaren Code schreiben.
Best Practices für die Datenstruktur
Vermeiden Sie verschachtelte Daten
Da die Firebase Realtime Database das Verschachteln von Daten mit bis zu 32 Ebenen ermöglicht, könnten Sie annehmen, dass dies die Standardstruktur sein sollte. Wenn Sie jedoch Daten an einem Speicherort in Ihrer Datenbank abrufen, rufen Sie auch allen untergeordneten Knoten verwendet werden. Wenn Sie einem Nutzer Lese- oder Schreibzugriff auf einen Knoten in Ihrer Datenbank gewähren, erhalten Sie außerdem Zugriff auf alle Daten unter diesem Knoten. Daher ist es in der Praxis am besten, Ihre Datenstruktur so flach zu halten, wie möglich.
Ein Beispiel, warum verschachtelte Daten schlecht sind, finden Sie hier: mehrfach verschachtelten Struktur:
{
// This is a poorly nested data architecture, because iterating the children
// of the "chats" node to get a list of conversation titles requires
// potentially downloading hundreds of megabytes of messages
"chats": {
"one": {
"title": "Historical Tech Pioneers",
"messages": {
"m1": { "sender": "ghopper", "message": "Relay malfunction found. Cause: moth." },
"m2": { ... },
// a very long list of messages
}
},
"two": { ... }
}
}
Bei diesem verschachtelten Design wird die Iteration durch die Daten problematisch. Wenn Sie beispielsweise die Titel von Chatunterhaltungen auflisten möchten, muss der gesamte chats
-Baum, einschließlich aller Mitglieder und Nachrichten, auf den Client heruntergeladen werden.
Datenstrukturen flachstellen
Wenn die Daten stattdessen in separate Pfade aufgeteilt werden, was auch als Denormalisierung bezeichnet wird, können sie bei Bedarf effizient in separaten Aufrufen heruntergeladen werden. Betrachten Sie diese flache Struktur:
{
// Chats contains only meta info about each conversation
// stored under the chats's unique ID
"chats": {
"one": {
"title": "Historical Tech Pioneers",
"lastMessage": "ghopper: Relay malfunction found. Cause: moth.",
"timestamp": 1459361875666
},
"two": { ... },
"three": { ... }
},
// Conversation members are easily accessible
// and stored by chat conversation ID
"members": {
// we'll talk about indices like this below
"one": {
"ghopper": true,
"alovelace": true,
"eclarke": true
},
"two": { ... },
"three": { ... }
},
// Messages are separate from data we may want to iterate quickly
// but still easily paginated and queried, and organized by chat
// conversation ID
"messages": {
"one": {
"m1": {
"name": "eclarke",
"message": "The relay seems to be malfunctioning.",
"timestamp": 1459361875337
},
"m2": { ... },
"m3": { ... }
},
"two": { ... },
"three": { ... }
}
}
Es ist jetzt möglich, die Liste der Räume durchzugehen, indem nur eine Wenige Bytes pro Konversation, sodass schnell Metadaten zum Auflisten oder Anzeigen von in einer Benutzeroberfläche. Nachrichten können separat abgerufen und angezeigt werden, sobald sie eingehen. Dadurch bleibt die Benutzeroberfläche reaktionsschnell und leistungsfähig.
Skalierbare Daten erstellen
Beim Erstellen von Apps ist es oft besser, nur einen Teil einer Liste herunterzuladen. Dies ist besonders häufig der Fall, wenn die Liste Tausende von Datensätzen enthält. Wenn diese Beziehung statisch und einseitig ist, können Sie die untergeordneten Objekte einfach unter dem übergeordneten Objekt verschachteln.
Manchmal ist diese Beziehung dynamischer oder es ist erforderlich, diese Daten zu denormalisieren. In vielen Fällen können Sie die Daten denormalisieren, indem Sie mit einer Abfrage eine Teilmenge der Daten abrufen, wie unter Daten sortieren und filtern beschrieben.
Aber selbst das ist möglicherweise nicht ausreichend. Nehmen wir zum Beispiel eine wechselseitige Beziehung zwischen Nutzenden und Gruppen. Nutzer können einer Gruppe angehören, und Gruppen bilden eine Liste der Nutzenden. Bei der Entscheidung, welchen Gruppen ein Nutzer angehört, wird es kompliziert.
Es braucht eine elegante Möglichkeit, die Gruppen eines Nutzers aufzulisten, Sie rufen nur Daten für diese Gruppen ab. Ein Index von Gruppen kann für hier ein gutes Angebot:
// An index to track Ada's memberships
{
"users": {
"alovelace": {
"name": "Ada Lovelace",
// Index Ada's groups in her profile
"groups": {
// the value here doesn't matter, just that the key exists
"techpioneers": true,
"womentechmakers": true
}
},
...
},
"groups": {
"techpioneers": {
"name": "Historical Tech Pioneers",
"members": {
"alovelace": true,
"ghopper": true,
"eclarke": true
}
},
...
}
}
Möglicherweise stellen Sie fest, dass einige Daten dupliziert werden, da die Beziehung sowohl im Datensatz von Ada als auch in der Gruppe gespeichert wird. alovelace
ist jetzt indexiert unter
Gruppe und techpioneers
ist in Adas Profil aufgeführt. Wenn Sie Ada also aus der Gruppe löschen möchten, müssen Sie sie an zwei Stellen aktualisieren.
Dies ist eine notwendige Redundanz für bidirektionale Beziehungen. Damit können Sie schnell und effizient abrufen können, selbst wenn die Liste der Nutzer oder Gruppen in Millionenhöhe oder wenn Realtime Database-Sicherheitsregeln den Zugriff auf einige der Datensätze verhindern.
Bei diesem Ansatz werden die Daten invertiert, indem die IDs als Schlüssel aufgeführt und der Wert auf „wahr“ gesetzt wird. Dadurch ist die Suche nach einem Schlüssel so einfach wie das Lesen von /users/$uid/groups/$group_id
und das Prüfen, ob es sich um null
handelt. Der Index ist schneller und wesentlich effizienter als das Abfragen oder Scannen der Daten.