Struktura bazy danych

W tym przewodniku omówimy niektóre kluczowe pojęcia związane z architekturą danych oraz sprawdzone metody tworzenia struktury danych JSON w Bazie danych czasu rzeczywistego Firebase.

Stworzenie prawidłowo zorganizowanej bazy danych wymaga dużej precyzji. Przede wszystkim musisz zaplanować sposób, w jaki dane będą zapisywane i pobierane, aby maksymalnie uprościć ten proces.

Jaka jest struktura danych: drzewo JSON

Wszystkie dane Bazy danych czasu rzeczywistego Firebase są przechowywane jako obiekty JSON. Można wyobrazić ją jako drzewo JSON hostowane w chmurze. W przeciwieństwie do bazy danych SQL nie ma 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, na przykład identyfikatory użytkowników lub nazwy semantyczne, lub możesz je podać za pomocą metody push().

Jeśli tworzysz własne klucze, muszą być one 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 ani 127. Nie można też używać w samych wartościach znaków kontrolnych ASCII.

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 bazy danych podobny do tego:

{
  "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 określone typy natywne, które odpowiadają dostępnym typom JSON, aby ułatwić pisanie bardziej łatwego w utrzymaniu kodu.

Sprawdzone metody tworzenia struktury danych

Unikaj zagnieżdżania danych

Baza danych czasu rzeczywistego Firebase umożliwia zagnieżdżanie danych nawet do 32 poziomów, więc możesz pomyśleć, że powinna to być struktura domyślna. Jeśli jednak pobierasz dane z lokalizacji w bazie danych, pobierane są też wszystkie jego węzły podrzędne. Poza tym, gdy przyznajesz komuś uprawnienia do odczytu lub zapisu w węźle swojej bazy danych, dajesz mu też dostęp do wszystkich danych w tym węźle. Dlatego najlepiej jest zadbać o to, by struktura danych była jak najbardziej płaska.

Oto przykład takiej struktury:

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

Taki zagnieżdżony układ sprawia, że 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 zamiast tego dane zostaną podzielone na oddzielne ścieżki (zwane też denormalizacją), można je w razie potrzeby efektywnie pobrać w osobnych wywołaniach. Weźmy pod uwagę 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ę pokojów przez pobieranie zaledwie kilku bajtów na rozmowę, a także szybko pobierać metadane na potrzeby listy pokoi lub wyświetlać je w interfejsie. Wiadomości można pobierać osobno i wyświetlać je w miarę ich odbierania, co pozwala na szybkie i elastyczne działanie interfejsu.

Tworzenie skalujących danych

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

Czasami ta relacja jest bardziej dynamiczna lub może być konieczna denormalizacja danych. W wielu przypadkach można denormalizować dane za pomocą zapytania, aby pobrać podzbiór danych. Zostało to omówione w sekcji Pobieranie danych.

Jednak nawet to może nie być wystarczające. Rozważ na przykład dwukierunkową relację między użytkownikami i grupami. Użytkownicy mogą należeć do określonej grupy, która stanowi listę użytkowników. Przy podejmowaniu decyzji, do której grupy należy użytkownik, sprawa staje się skomplikowana.

Potrzebny jest do tego elegancki sposób na wyświetlanie listy grup, do których należy użytkownik, oraz pobieranie tylko danych dotyczących tych grup. Taki indeks grup może okazać się bardzo pomocny:

// 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 część danych jest duplikowana, ponieważ przechowuje ona relację zarówno w rekordzie Ady, jak i w grupie. Element alovelace jest teraz indeksowany w ramach grupy, a element techpioneers znajduje się w profilu Ady. Aby usunąć Adę z grupy, trzeba ją zaktualizować w 2 miejscach.

Jest to konieczne nadmiarowość w przypadku relacji dwukierunkowych. Umożliwia ona szybkie i skuteczne pobieranie danych o członkostwie Ady, nawet jeśli lista użytkowników lub grup powiększy się do milionów lub gdy reguły zabezpieczeń Bazy danych czasu rzeczywistego blokują dostęp do niektórych rekordów.

W ramach tej metody, polegającej na odwróceniu danych przez wyświetlanie identyfikatorów w postaci kluczy i ustawienie wartości jako „prawda”, sprawdzanie klucza jest proste, ponieważ wystarczy odczytać wartość /users/$uid/groups/$group_id i sprawdzić, czy to null. Indeks jest szybszy i wydajniejszy niż wykonywanie zapytań czy skanowania danych.

Następne kroki