Strukturieren Sie Ihre Datenbank

Dieser Leitfaden behandelt einige der Schlüsselkonzepte der Datenarchitektur und Best Practices für die Strukturierung der JSON-Daten in Ihrer Firebase-Echtzeitdatenbank.

Der Aufbau einer richtig strukturierten Datenbank erfordert einiges an Voraussicht. Am wichtigsten ist, dass Sie planen, wie Daten gespeichert und später abgerufen werden, um diesen Vorgang so einfach wie möglich zu gestalten.

Wie Daten strukturiert sind: Es ist ein JSON-Baum

Alle Daten der Firebase Realtime Database werden als JSON-Objekte gespeichert. Sie können sich die Datenbank als einen in der Cloud gehosteten JSON-Baum vorstellen. Im Gegensatz zu einer SQL-Datenbank gibt es keine Tabellen oder Datensätze. Wenn Sie der JSON-Struktur Daten hinzufügen, werden diese zu einem Knoten in der vorhandenen JSON-Struktur mit einem zugehörigen Schlüssel. Sie können Ihre eigenen Schlüssel bereitstellen, z. B. Benutzer-IDs oder semantische Namen, oder sie können Ihnen mithilfe von push() bereitgestellt werden.

Wenn Sie Ihre eigenen Schlüssel erstellen, müssen diese UTF-8-codiert sein, dürfen maximal 768 Byte lang sein und dürfen keine . , $ , # , [ , ] , / , oder ASCII-Steuerzeichen 0-31 oder 127. Sie können auch keine ASCII-Steuerzeichen in den Werten selbst verwenden.

Stellen Sie sich beispielsweise eine Chat-Anwendung vor, die es Benutzern ermöglicht, ein Basisprofil und eine Kontaktliste zu speichern. Ein typisches Benutzerprofil befindet sich in einem Pfad wie /users/$uid . Der Benutzer alovelace könnte einen Datenbankeintrag haben, der etwa so aussieht:

{
  "users": {
    "alovelace": {
      "name": "Ada Lovelace",
      "contacts": { "ghopper": true },
    },
    "ghopper": { ... },
    "eclarke": { ... }
  }
}

Obwohl die Datenbank eine JSON-Struktur verwendet, können in der Datenbank gespeicherte Daten als bestimmte native Typen dargestellt werden, die verfügbaren JSON-Typen entsprechen, um Ihnen beim Schreiben von besser wartbarem Code zu helfen.

Best Practices für die Datenstruktur

Vermeiden Sie das Verschachteln von Daten

Da die Firebase-Echtzeitdatenbank das Verschachteln von Daten mit einer Tiefe von bis zu 32 Ebenen ermöglicht, könnten Sie versucht sein zu glauben, dass dies die Standardstruktur sein sollte. Wenn Sie jedoch Daten an einem Ort in Ihrer Datenbank abrufen, rufen Sie auch alle untergeordneten Knoten ab. Wenn Sie jemandem außerdem Lese- oder Schreibzugriff auf einen Knoten in Ihrer Datenbank gewähren, gewähren Sie ihm auch Zugriff auf alle Daten unter diesem Knoten. Daher ist es in der Praxis am besten, Ihre Datenstruktur so flach wie möglich zu halten.

Als Beispiel dafür, warum verschachtelte Daten schlecht sind, betrachten Sie die folgende mehrfach verschachtelte 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 das Durchlaufen der Daten problematisch. Beispielsweise erfordert das Auflisten der Titel von Chat-Gesprächen, dass der gesamte chats Baum, einschließlich aller Mitglieder und Nachrichten, auf den Client heruntergeladen wird.

Datenstrukturen glätten

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 abgeflachte 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 zu durchlaufen, indem nur wenige Bytes pro Konversation heruntergeladen und Metadaten schnell abgerufen werden, um Räume in einer Benutzeroberfläche aufzulisten oder anzuzeigen. Nachrichten können separat abgerufen und bei ihrem Eintreffen angezeigt werden, sodass die Benutzeroberfläche reaktionsschnell und schnell bleibt.

Erstellen Sie skalierbare Daten

Beim Erstellen von Apps ist es oft besser, eine Teilmenge einer Liste herunterzuladen. Dies ist besonders häufig der Fall, wenn die Liste Tausende von Datensätzen enthält. Wenn diese Beziehung statisch und unidirektional ist, können Sie die untergeordneten Objekte einfach unter den übergeordneten Objekten verschachteln.

Manchmal ist diese Beziehung dynamischer, oder es kann erforderlich sein, diese Daten zu denormalisieren. Oft können Sie die Daten denormalisieren, indem Sie eine Abfrage verwenden, um eine Teilmenge der Daten abzurufen, wie in Daten abrufen beschrieben.

Aber auch dies kann unzureichend sein. Stellen Sie sich beispielsweise eine wechselseitige Beziehung zwischen Benutzern und Gruppen vor. Benutzer können einer Gruppe angehören, und Gruppen bestehen aus einer Liste von Benutzern. Wenn es darum geht, zu entscheiden, zu welchen Gruppen ein Benutzer gehört, wird es kompliziert.

Was benötigt wird, ist eine elegante Möglichkeit, die Gruppen aufzulisten, denen ein Benutzer angehört, und nur Daten für diese Gruppen abzurufen. Ein Gruppenverzeichnis kann hier sehr hilfreich sein:

// 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 dadurch einige Daten dupliziert werden, indem die Beziehung sowohl unter Adas Datensatz als auch unter der Gruppe gespeichert wird. Jetzt wird alovelace unter einer Gruppe indiziert und techpioneers wird in Adas Profil aufgeführt. Um Ada aus der Gruppe zu löschen, muss sie also an zwei Stellen aktualisiert werden.

Dies ist eine notwendige Redundanz für bidirektionale Beziehungen. Es ermöglicht Ihnen, Adas Mitgliedschaften schnell und effizient abzurufen, selbst wenn die Liste der Benutzer oder Gruppen in die Millionen skaliert oder wenn die Sicherheitsregeln der Echtzeitdatenbank den Zugriff auf einige der Datensätze verhindern.

Dieser Ansatz, bei dem die Daten invertiert werden, indem die IDs als Schlüssel aufgelistet und der Wert auf true gesetzt werden, macht die Überprüfung auf einen Schlüssel so einfach wie das Lesen von /users/$uid/groups/$group_id und das Überprüfen, ob er null ist. Der Index ist schneller und wesentlich effizienter als das Abfragen oder Scannen der Daten.

Nächste Schritte