Datenbank strukturieren

In diesem Leitfaden werden einige der wichtigsten Konzepte der Datenarchitektur sowie Best Practices für die Strukturierung der JSON-Daten in Firebase Realtime Database behandelt.

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

Datenstruktur: 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 Ihre eigenen Schlüssel angeben, z. B. Nutzer-IDs oder semantische Namen. Alternativ können sie mithilfe der push()-Methode für Sie bereitgestellt werden.

Wenn Sie eigene Schlüssel erstellen, müssen diese UTF-8-codiert sein, dürfen maximal 768 Byte groß sein und dürfen keine ., $, #, [, ], / oder die ASCII-Steuerzeichen 0–31 oder 127 enthalten. Auch in den Werten selbst können keine ASCII-Steuerzeichen verwendet werden.

Denken Sie beispielsweise an eine Chat-Anwendung, mit der Nutzer ein einfaches Profil und eine Kontaktliste speichern können. Ein typisches Nutzerprofil befindet sich unter einem Pfad wie /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 die Datenbank ein JSON-Baum verwendet, können die in der Datenbank 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

Verschachteln von Daten vermeiden

Da Firebase Realtime Database das Verschachteln von Daten mit bis zu 32 Ebenen zulässt, könnten Sie vermuten, dass dies die Standardstruktur sein sollte. Wenn Sie jedoch Daten an einem Ort 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 außerdem Zugriff auf alle Daten unter diesem Knoten. Daher ist es in der Praxis am besten, die Datenstruktur so flach wie möglich zu halten.

Hier ein Beispiel dafür, warum verschachtelte Daten nicht gut sind:

{
  // 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 Iterieren durch die Daten problematisch. Wenn Sie beispielsweise die Titel von Chatunterhaltungen auflisten möchten, muss die gesamte chats-Struktur, 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 Chatrooms durchzugehen, indem nur wenige Byte pro Unterhaltung heruntergeladen werden. So können Metadaten schnell abgerufen werden, um Chatrooms in einer Benutzeroberfläche aufzulisten oder anzuzeigen. Nachrichten können separat abgerufen und beim Eintreffen angezeigt werden, sodass die Benutzeroberfläche responsiv und schnell bleibt.

Skalierbare Daten erstellen

Beim Erstellen von Apps ist es oft besser, einen Teil einer Liste herunterzuladen. Das ist besonders häufig der Fall, wenn die Liste Tausende von Einträgen 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 abrufen beschrieben.

Aber selbst das ist möglicherweise nicht ausreichend. Nehmen wir zum Beispiel eine wechselseitige Beziehung zwischen Nutzern und Gruppen. Nutzer können einer Gruppe angehören und Gruppen bestehen aus einer Liste von Nutzern. Bei der Entscheidung, zu welchen Gruppen die Nutzenden gehören, 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 einige Daten dupliziert werden, da die Beziehung sowohl im Datensatz von Ada als auch in der Gruppe gespeichert wird. Jetzt wird alovelace unter einer Gruppe indexiert und techpioneers wird im Profil von Ada 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 zweiseitige Beziehungen. So können Sie die Mitgliedschaften von Ada schnell und effizient abrufen, auch wenn die Liste der Nutzer oder Gruppen in die Millionen geht oder Realtime Database-Sicherheitsregeln den Zugriff auf einige der Einträge verhindern.

Bei diesem Ansatz werden die Daten invertiert, indem die IDs als Schlüssel aufgelistet und der Wert auf „true“ gesetzt wird. Dies vereinfacht die Suche nach einem Schlüssel, indem beispielsweise /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