Ustrukturyzuj swoją bazę danych

W tym przewodniku omówiono niektóre kluczowe pojęcia z zakresu architektury danych i najlepsze praktyki dotyczące strukturyzowania danych JSON w bazie danych Firebase Realtime Database.

Budowa odpowiednio zorganizowanej bazy danych wymaga sporo przezorności. Co najważniejsze, musisz zaplanować sposób zapisywania i późniejszego odzyskiwania danych, aby maksymalnie ułatwić ten proces.

Struktura danych: jest to drzewo JSON

Wszystkie dane bazy danych Firebase Realtime Database są przechowywane jako obiekty JSON. Bazę danych można traktować jak drzewo JSON hostowane w chmurze. W przeciwieństwie do bazy danych SQL nie ma w niej tabel ani rekordów. Kiedy dodasz dane do drzewa JSON, stanie się ono 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, lub można je udostępnić za pomocą metody 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ć plików . , $ , # , [ , ] , / lub znaki sterujące ASCII 0-31 lub 127. Znaków kontrolnych ASCII nie można także używać 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, takiej jak /users/$uid . Użytkownik alovelace może mieć wpis w bazie danych wyglądający mniej więcej tak:

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

Chociaż baza danych korzysta z drzewa JSON, dane przechowywane w bazie danych mogą być reprezentowane jako pewne typy natywne, które odpowiadają dostępnym typom JSON, aby ułatwić pisanie kodu łatwiejszego w utrzymaniu.

Najlepsze praktyki dotyczące struktury danych

Unikaj zagnieżdżania danych

Ponieważ baza danych Firebase Realtime umożliwia zagnieżdżanie danych na głębokość do 32 poziomów, możesz pokusić się o stwierdzenie, że powinna to być struktura domyślna. Jednak podczas pobierania danych z lokalizacji w bazie danych pobierane są także wszystkie jej węzły podrzędne. Ponadto, przyznając komuś dostęp do odczytu lub zapisu w węźle bazy danych, zapewniasz mu również dostęp do wszystkich danych w tym węźle. Dlatego w praktyce najlepiej jest zachować możliwie płaską strukturę danych.

Jako przykład tego, dlaczego dane zagnieżdżone są złe, rozważ następującą strukturę wielokrotnie zagnieżdżoną:

{
  // 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 tego zagnieżdżonego projektu iteracja danych staje się problematyczna. Na przykład wyświetlenie tytułów rozmów na czacie wymaga pobrania do klienta całego drzewa chats , w tym wszystkich członków i wiadomości.

Spłaszczaj struktury danych

Jeśli zamiast tego dane zostaną podzielone na osobne ścieżki, co nazywa się również denormalizacją, można je efektywnie pobrać w oddzielnych wywołaniach, jeśli zajdzie taka potrzeba. Rozważ tę spłaszczoną 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żna teraz przeglądać listę pokoi, pobierając tylko kilka bajtów na rozmowę, co pozwala szybko pobrać metadane w celu wystawienia listy pokoi lub wyświetlenia ich w interfejsie użytkownika. Wiadomości można pobierać oddzielnie i wyświetlać po ich otrzymaniu, dzięki czemu interfejs użytkownika pozostaje responsywny i szybki.

Twórz dane, które się skalują

Podczas tworzenia aplikacji często lepiej jest pobrać podzbiór listy. Jest to szczególnie częste, jeśli lista zawiera tysiące rekordów. Gdy ta relacja jest statyczna i jednokierunkowa, można po prostu zagnieździć obiekty podrzędne pod obiektem nadrzędnym.

Czasami relacja ta jest bardziej dynamiczna lub może zaistnieć konieczność denormalizacji tych danych. Wiele razy można zdenormalizować dane, używając zapytania w celu pobrania podzbioru danych, jak omówiono w temacie Sortowanie i filtrowanie danych .

Ale nawet to może nie wystarczyć. Rozważmy na przykład dwukierunkową relację między użytkownikami i grupami. Użytkownicy mogą należeć do grupy, a grupy tworzą listę użytkowników. Kiedy przychodzi czas na podjęcie decyzji, do której grupy należy użytkownik, sytuacja się komplikuje.

Potrzebny jest elegancki sposób na wyświetlenie listy grup, do których należy użytkownik i pobranie danych tylko dla tych grup. Indeks grup może tutaj bardzo pomóc:

// 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 zduplikowanie niektórych danych poprzez przechowywanie relacji zarówno w rekordzie Ady, jak i w grupie. Teraz alovelace jest indeksowane w grupie, a techpioneers są wymienione w profilu Ady. Aby więc usunąć Adę z grupy, należy ją zaktualizować w dwóch miejscach.

Jest to niezbędna redundancja w relacjach dwustronnych. Pozwala szybko i skutecznie pobrać członkostwo Ady, nawet gdy lista użytkowników lub grup skaluje się do milionów lub gdy reguły bezpieczeństwa Bazy danych czasu rzeczywistego uniemożliwiają dostęp do niektórych rekordów.

To podejście, polegające na odwróceniu danych poprzez wypisanie 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 ma on null . Indeks jest szybszy i znacznie wydajniejszy niż wysyłanie zapytań lub skanowanie danych.

Następne kroki