W tym przewodniku omówiono niektóre kluczowe koncepcje architektury danych i najlepsze praktyki dotyczące strukturyzowania danych JSON w bazie danych czasu rzeczywistego Firebase.
Zbudowanie odpowiednio ustrukturyzowanej bazy danych wymaga sporo przezorności. Co najważniejsze, musisz zaplanować, w jaki sposób dane będą zapisywane i później odzyskiwane, aby proces ten był jak najłatwiejszy.
Struktura danych: to drzewo JSON
Wszystkie dane Firebase Realtime Database są przechowywane jako obiekty JSON. Możesz myśleć o bazie danych jako o drzewie JSON hostowanym w chmurze. W przeciwieństwie do bazy danych SQL nie ma tabel ani rekordów. Kiedy dodajesz dane do drzewa JSON, staje 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żesz je udostępnić za pomocą push()
.
Jeśli tworzysz własne klucze, muszą one być zakodowane w UTF-8, maksymalnie 768 bajtów i nie mogą zawierać rozszerzenia .
, $
, #
, [
, ]
, /
lub znaków sterujących ASCII 0-31 lub 127. Nie można również używać znaków sterujących 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, takiej jak /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 korzysta z drzewa JSON, dane przechowywane w bazie danych mogą być reprezentowane jako pewne typy natywne, które odpowiadają dostępnym typom JSON, aby pomóc w pisaniu łatwiejszego w utrzymaniu kodu.
Najlepsze praktyki dotyczące struktury danych
Unikaj zagnieżdżania danych
Ponieważ baza danych czasu rzeczywistego Firebase umożliwia zagnieżdżanie danych do 32 poziomów, możesz ulec pokusie, aby pomyśleć, że powinna to być struktura domyślna. Jednak gdy pobierasz dane z lokalizacji w bazie danych, pobierasz również wszystkie jej węzły podrzędne. Ponadto, przyznając komuś dostęp do odczytu lub zapisu w węźle w bazie danych, przyznajesz mu również dostęp do wszystkich danych w tym węźle. Dlatego w praktyce najlepiej jest, aby struktura danych była jak najbardziej płaska.
Aby zobaczyć przykład, dlaczego dane zagnieżdżone są złe, rozważ następującą strukturę z wieloma zagnieżdżeniami:
{ // 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łaszcz struktury danych
Jeśli zamiast tego dane są podzielone na osobne ścieżki, zwane również denormalizacją, można je wydajnie pobrać w oddzielnych wywołaniach, gdy jest to potrzebne. 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": { ... } } }
Teraz można przeglądać listę pokoi, pobierając tylko kilka bajtów na konwersację, szybko pobierając metadane do wystawiania lub wyświetlania pokoi 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 skalowalne dane
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 ta zależność jest bardziej dynamiczna lub może być konieczna denormalizacja tych danych. Wiele razy można zdenormalizować dane, używając zapytania w celu pobrania podzbioru danych, jak omówiono w sekcji Pobieranie danych .
Ale nawet to może być niewystarczające. Rozważmy na przykład dwukierunkową relację między użytkownikami a grupami. Użytkownicy mogą należeć do grupy, a grupy zawierają listę użytkowników. Kiedy przychodzi czas na podjęcie decyzji, do której grupy należy użytkownik, sprawy się komplikują.
Potrzebny jest elegancki sposób wyświetlania listy grup, do których należy użytkownik, i pobierania 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 duplikuje to niektóre dane, przechowując relację zarówno w rekordzie Ady, jak iw grupie. Teraz alovelace
jest indeksowane w ramach grupy, a techpioneers
jest wymieniony w profilu Ady. Aby więc usunąć Adę z grupy, należy ją zaktualizować w dwóch miejscach.
Jest to konieczna redundancja dla relacji dwukierunkowych. Pozwala szybko i sprawnie pobrać członkostwo Ady, nawet gdy lista użytkowników lub grup liczy miliony lub gdy zasady bezpieczeństwa Realtime Database uniemożliwiają dostęp do niektórych rekordów.
Takie podejście, polegające na odwróceniu danych poprzez wypisanie identyfikatorów jako kluczy i ustawienie wartości na true, sprawia, że sprawdzanie klucza jest tak proste, jak odczytanie /users/$uid/groups/$group_id
i sprawdzenie, czy jest on null
. Indeks jest szybszy i znacznie wydajniejszy niż wysyłanie zapytań lub skanowanie danych.