Datenbank strukturieren

In dieser Anleitung werden einige der wichtigsten Konzepte der Datenarchitektur und Best Practices für die Strukturierung der JSON-Daten in Ihrem Firebase Realtime Database behandelt.

Für den Aufbau einer gut strukturierten Datenbank ist einiges an Vorarbeit erforderlich. Vor allem müssen Sie planen, wie Daten gespeichert und später abgerufen werden, damit dieser Prozess so einfach wie möglich ist.

So sind die Daten strukturiert: als JSON-Baum

Alle Firebase Realtime Database-Daten 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 eigene Schlüssel wie Nutzer-IDs oder semantische Namen angeben oder diese können mithilfe der Methode push() für Sie bereitgestellt werden.

Beispiel: Eine Chatanwendung, in der Nutzer ein einfaches Profil und eine Kontaktliste speichern können. Ein typisches Nutzerprofil befindet sich unter einem Pfad wie /users/$uid. Für den Nutzer alovelace könnte ein Datenbankeintrag etwa so aussehen:

{
  "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 besser wartbaren Code schreiben.

Best Practices für die Datenstruktur

Vermeiden Sie das Verschachteln von Daten

Da mit Firebase Realtime Database Daten bis zu 32 Ebenen tief verschachtelt werden können, könnte man annehmen, dass dies die Standardstruktur sein sollte. Wenn Sie jedoch Daten an einem Speicherort in Ihrer Datenbank abrufen, werden auch alle untergeordneten Knoten abgerufen. Wenn Sie jemandem Lese- oder Schreibzugriff auf einen Knoten in Ihrer Datenbank gewähren, gewähren Sie ihm auch Zugriff auf alle Daten unter diesem Knoten. In der Praxis ist es daher am besten, die Datenstruktur so flach wie möglich zu halten.

Ein Beispiel dafür, warum verschachtelte Daten schlecht sind, ist 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 geschachtelten Design ist 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 vereinfachen

Wenn die Daten stattdessen in separate Pfade aufgeteilt werden (auch als Denormalisierung bezeichnet), können sie effizient in separaten Aufrufen heruntergeladen werden, wenn sie benötigt werden. Betrachten Sie diese vereinfachte 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 Byte pro Unterhaltung heruntergeladen werden. So können Metadaten schnell abgerufen werden, um Räume in einer Benutzeroberfläche aufzulisten oder anzuzeigen. Nachrichten können separat abgerufen und bei Eintreffen angezeigt werden, sodass die Benutzeroberfläche reaktionsschnell und schnell bleibt.

Daten erstellen, die skaliert werden können

Beim Erstellen von Apps ist es oft besser, eine Teilmenge einer Liste herunterzuladen. Das 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 dem übergeordneten Objekt verschachteln.

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

Aber selbst das reicht möglicherweise nicht aus. Betrachten Sie beispielsweise eine bidirektionale Beziehung zwischen Nutzern und Gruppen. Nutzer können einer Gruppe angehören und Gruppen bestehen aus einer Liste von Nutzern. Wenn es darum geht, zu entscheiden, zu welchen Gruppen ein Nutzer gehört, wird es kompliziert.

Es ist eine elegante Möglichkeit erforderlich, die Gruppen aufzulisten, zu denen ein Nutzer gehört, und nur Daten für diese Gruppen abzurufen. Ein Index von Gruppen 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, da die Beziehung sowohl im Datensatz von Ada als auch in der Gruppe gespeichert wird. alovelace ist jetzt unter einer Gruppe indexiert und techpioneers wird im Profil von Ada aufgeführt. Wenn Sie Ada aus der Gruppe entfernen möchten, muss die Gruppe also an zwei Stellen aktualisiert werden.

Dies ist eine notwendige Redundanz für bidirektionale Beziehungen. So können Sie die Mitgliedschaften von Ada schnell und effizient abrufen, auch wenn die Liste der Nutzer oder Gruppen Millionen von Einträgen umfasst oder wenn Realtime Database-Sicherheitsregeln den Zugriff auf einige Datensätze verhindern.

Bei diesem Ansatz werden die Daten invertiert, indem die IDs als Schlüssel aufgeführt und der Wert auf „true“ gesetzt wird. So lässt sich ganz einfach prüfen, ob ein Schlüssel vorhanden ist, indem /users/$uid/groups/$group_id gelesen und geprüft wird, ob er null ist. Der Index ist schneller und wesentlich effizienter als das Abfragen oder Scannen der Daten.

Nächste Schritte