Struktura bazy danych

W tym przewodniku omawiamy kluczowe pojęcia związane z architekturą danych oraz sprawdzone metody tworzenia struktury danych JSON w bazie danych czasu rzeczywistego Firebase.

Stworzenie poprawnie uporządkowanej bazy danych wymaga przemyśleń. Przede wszystkim musisz zaplanować sposób zapisywania i pobierania danych, aby maksymalnie to ułatwić.

Jaka jest struktura danych: to drzewo JSON

Wszystkie dane Bazy danych czasu rzeczywistego Firebase są przechowywane jako obiekty JSON. Możesz traktować ją jak 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, np. identyfikatory użytkowników lub nazwy semantyczne, albo możesz je przekazać za pomocą metody push().

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

Weźmy na przykład aplikację do obsługi 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 podobny do tego:

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

Baza danych korzysta z drzewa JSON, ale przechowywane w niej dane mogą być reprezentowane jako określone typy natywne odpowiadające dostępnym typom JSON. Ułatwia to pisanie kodu, który można łatwiej obsługiwać.

Sprawdzone metody tworzenia struktury danych

Unikaj zagnieżdżania danych

Baza danych czasu rzeczywistego Firebase umożliwia zagnieżdżanie danych na 32 poziomach, więc może się wydawać, że to powinna być struktura domyślna. Jeśli jednak pobierasz dane z określonej lokalizacji w bazie danych, pobierasz też wszystkie jej węzły podrzędne. Oprócz tego przyznając komuś uprawnienia do odczytu lub zapisu w węźle w bazie danych, dajesz tej osobie dostęp do wszystkich danych w tym węźle. Dlatego w praktyce najlepiej jest zadbać o jak najbardziej płaską strukturę danych.

Przykładem wyjaśniającego, dlaczego dane zagnieżdżone są nieprawidłowe, przyjrzyjmy się tej strukturze z wieloma zagnieżdżonymi warstwami:

{
  // 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": { ... }
  }
}

Przy takiej zagnieżdżonej strukturze powtarzanie danych staje się problematyczne. Na przykład wyświetlanie listy tytułów rozmów na czacie wymaga pobrania do klienta całego drzewa chats, w tym wszystkich członków i wiadomości.

Spłaszcz struktury danych

Jeśli dane zostaną zamiast tego podzielone na osobne ścieżki (nazywane też denormalizacją), w razie potrzeby można je wydajnie pobrać w oddzielnych wywołaniach. Rozważmy tę płaską 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": { ... }
  }
}

Teraz można iterować listę pokoi, pobierając tylko kilka bajtów na rozmowę, szybko pobierając metadane do wyświetlania listy lub wyświetlając pokoje w interfejsie użytkownika. Wiadomości mogą być pobierane oddzielnie i wyświetlane w miarę ich odbierania, dzięki czemu interfejs użytkownika szybko i elastycznie.

Twórz skalowalne dane

Podczas tworzenia aplikacji często lepiej jest pobrać podzbiór listy. Jest to szczególnie typowe, gdy lista zawiera tysiące rekordów. Gdy ta relacja jest statyczna i jednokierunkowa, możesz po prostu zagnieździć obiekty podrzędne w ramach elementu nadrzędnego.

Czasami ta zależność jest bardziej dynamiczna lub konieczna może być denormalizacja danych. Często można zdenormalizować dane za pomocą zapytania służącego do ich pobrania podzbioru, co zostało omówione w sekcji Pobieranie danych.

Jednak nawet to może być niewystarczające. Weźmy na przykład dwukierunkową relację między użytkownikami a grupami. Użytkownicy mogą należeć do grupy, a grupy to lista użytkowników. Sprawa się komplikuje, gdy chodzi o podjęcie decyzji, do której grupy należy dany użytkownik.

Potrzebny jest elegancki sposób wyświetlania listy grup, do których należy użytkownik, i pobierania tylko danych z tych grup. Indeks grup może być bardzo pomocny w tych kwestiach:

// 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że się zdarzyć, że spowoduje to zduplikowanie niektórych danych, ponieważ zapisano relację w rekordzie Ady i w grupie. Teraz indeks alovelace jest zindeksowany w ramach grupy, a techpioneers jest wymieniony w profilu Ady. Aby usunąć Adę z grupy, trzeba ją zaktualizować w dwóch miejscach.

Jest to niezbędne nadmiarowość w przypadku relacji dwukierunkowych. Umożliwia szybkie i wydajne pobieranie informacji o członkostwach Ady nawet wtedy, gdy lista użytkowników lub grup powiększy się do milionów lub gdy reguły zabezpieczeń Bazy danych czasu rzeczywistego uniemożliwiają dostęp do niektórych rekordów.

Dzięki temu odwróceniu danych poprzez wyświetlenie identyfikatorów w postaci kluczy i ustawienie wartości na prawda sprawia, że sprawdzanie klucza jest tak proste, jak odczytywanie /users/$uid/groups/$group_id i sprawdzanie, czy ma on wartość null. Indeks jest szybszy i znacznie bardziej wydajny niż wysyłanie zapytań o dane czy ich skanowanie.

Następne kroki