Tworzenie struktury bazy danych

W tym przewodniku omówimy najważniejsze koncepcje architektury danych i sprawdzone metody strukturyzowania danych JSON w Firebase Realtime Database.

Zbudowanie prawidłowo skonstruowanej bazy danych wymaga sporo przemyśleń. Najważniejsze jest zaplanowanie sposobu zapisywania danych i późniejszego ich pobierania, aby ten proces był jak najprostszy.

Struktura danych: drzewo JSON

Wszystkie dane Firebase Realtime Database są przechowywane jako obiekty JSON. Bazę danych można traktować jako drzewo JSON hostowane w chmurze. W przeciwieństwie do bazy danych SQL nie ma w niej tabel ani rekordów. Gdy dodasz dane do drzewa JSON, staną się one węzłem w istniejącej strukturze JSON z powiązanym kluczem. Możesz podać własne klucze, takie jak identyfikatory użytkowników lub nazwy semantyczne, albo skorzystać z kluczy udostępnianych przez push().

Jeśli tworzysz własne klucze, muszą one być zakodowane w formacie UTF-8, mogą mieć maksymalnie 768 bajtów i nie mogą zawierać znaków ., $, #, [, ], / ani znaków sterujących ASCII 0–31 lub 127. Nie możesz też używać znaków kontrolnych ASCII w samych wartościach.

Rozważmy na przykład aplikację do czatu, która umożliwia użytkownikom przechowywanie podstawowego profilu i listy kontaktów. Typowy profil użytkownika znajduje się w ścieżce, np. /users/$uid. Użytkownik alovelace może mieć wpis w bazie danych, który wygląda mniej więcej tak:

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

Chociaż baza danych używa drzewa JSON, dane w niej przechowywane mogą być reprezentowane jako określone typy natywne, które odpowiadają dostępnym typom JSON, co ułatwia pisanie kodu, który jest łatwiejszy w utrzymaniu.

Sprawdzone metody tworzenia struktury danych

Unikaj zagnieżdżania danych

Ponieważ Firebase Realtime Database umożliwia zagnieżdżanie danych na maksymalnie 32 poziomach, możesz uznać, że powinna to być struktura domyślna. Gdy jednak pobierasz dane z lokalizacji w bazie danych, pobierasz też wszystkie jej węzły podrzędne. Dodatkowo, gdy przyznasz komuś dostęp do odczytu lub zapisu w węźle w swojej bazie danych, przyznasz mu też dostęp do wszystkich danych znajdujących się w tym węźle. Dlatego w praktyce najlepiej jest zachować jak najpłaską strukturę danych.

Aby zobaczyć przykład, dlaczego zagnieżdżone dane są niekorzystne, rozważ następującą wielokrotnie zagnieżdżoną 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": { "..." }
  }
}

W przypadku takiej zagnieżdżonej struktury iteracja danych staje się problematyczna. Na przykład wyświetlenie tytułów rozmów na czacie wymaga pobrania na klienta całego chatsdrzewa, w tym wszystkich członków i wiadomości.

Spłaszczanie struktur danych

Jeśli dane są podzielone na osobne ścieżki (tzw. denormalizacja), można je efektywnie pobierać w osobnych wywołaniach, gdy są potrzebne. Rozważ tę uproszczoną 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": { "..." }
  }
}

Możesz teraz przeglądać listę pokoi, pobierając tylko kilka bajtów na rozmowę, co pozwala szybko pobierać metadane do wyświetlania pokoi na liście lub w interfejsie. Wiadomości można pobierać osobno i wyświetlać w miarę ich przychodzenia, dzięki czemu interfejs użytkownika pozostaje responsywny i szybki.

Tworzenie danych, które można skalować

Podczas tworzenia aplikacji często lepiej jest pobrać podzbiór listy. Dzieje się tak zwłaszcza wtedy, gdy lista zawiera tysiące rekordów. Jeśli ta relacja jest statyczna i jednokierunkowa, możesz po prostu zagnieździć obiekty podrzędne w obiekcie nadrzędnym.

Czasami ta relacja jest bardziej dynamiczna lub może być konieczne zdenormalizowanie tych danych. Wielokrotnie możesz zdenormalizować dane, używając zapytania do pobrania podzbioru danych, jak opisano w sekcji Pobieranie danych.

Ale nawet to może być niewystarczające. Rozważ na przykład dwukierunkową relację między użytkownikami a grupami. Użytkownicy mogą należeć do grupy, a grupy składają się z listy użytkowników. Gdy przychodzi czas na podjęcie decyzji, do których grup należy użytkownik, sytuacja się komplikuje.

Potrzebny jest elegancki sposób na wyświetlanie listy grup, do których należy użytkownik, i pobieranie tylko danych tych grup. W tym przypadku bardzo przydatny może być indeks grup:

// 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
      }
    },
    // ...
  }
}

Możesz zauważyć, że powoduje to duplikowanie niektórych danych, ponieważ relacja jest przechowywana zarówno w rekordzie Ady, jak i w grupie. Teraz alovelace jest indeksowany w grupie, a techpioneers jest wymieniony w profilu Ady. Aby usunąć Adę z grupy, musisz zaktualizować ją w 2 miejscach.

Jest to niezbędna nadmiarowość w przypadku relacji dwukierunkowych. Umożliwia szybkie i wydajne pobieranie informacji o członkostwie w usłudze Ada, nawet jeśli lista użytkowników lub grup liczy miliony pozycji albo Realtime Databasereguły bezpieczeństwaRealtime Database uniemożliwiają dostęp do niektórych rekordów.

Takie podejście, polegające na odwróceniu danych przez umieszczenie identyfikatorów jako kluczy i ustawienie wartości na „true”, sprawia, że sprawdzenie klucza jest tak proste, jak odczytanie /users/$uid/groups/$group_id i sprawdzenie, czy jest to null. Indeks jest szybszy i znacznie wydajniejszy niż wysyłanie zapytań czy skanowanie danych.

Następne kroki