Ten przewodnik zawiera niektóre kluczowe koncepcje architektury danych oraz najlepsze praktyki dotyczące strukturyzowania danych JSON w Twoim pliku Firebase Realtime Database.
Tworzenie odpowiednio uporządkowanej bazy danych wymaga sporo przemyślenia. Najważniejsze jest zaplanowanie sposobu zapisywania i późniejszego pobierania danych, aby proces ten był jak najprostszy.
Sposób uporządkowania danych: drzewo JSON
Wszystkie dane Firebase Realtime Database są przechowywane jako obiekty JSON. Można wyobrazić ją w postaci drzewa JSON hostowanego 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 dotychczasowej strukturze JSON z powiązanym kluczem. Możesz podać własne klucze, takie jak identyfikatory użytkowników lub nazwy semantyczne, lub mogą one zostać udostępnione za pomocą
push()
.
Jeśli tworzysz własne klucze, muszą one być zakodowane w formacie UTF-8, mieć maksymalnie 768 bajtów i nie mogą zawierać znaków .
, $
, #
, [
, ]
, /
ani znaków sterujących ASCII o numerze 0–31 lub 127. W wartościach nie można też używać 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ę na ścieżce takiej jak /users/$uid
. Użytkownik alovelace
może mieć w bazie danych wpis o takiej treści:
{ "users": { "alovelace": { "name": "Ada Lovelace", "contacts": { "ghopper": true }, }, "ghopper": { ... }, "eclarke": { ... } } }
Chociaż baza danych używa drzewa JSON, dane w niej zapisane mogą być reprezentowane jako określone typy natywne, które odpowiadają dostępnym typom JSON. Pomaga to w pisaniu kodu łatwiejszego do utrzymania.
Sprawdzone metody dotyczące struktury danych
Unikaj zagnieżdżania danych
Funkcja Firebase Realtime Database umożliwia zagnieżdżanie danych nawet do 32 poziomów, więc możesz sądzić, ż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. Poza tym, gdy przyznasz komuś uprawnienia do odczytu lub zapisu w węźle swojej bazy danych, jednocześnie przyznasz tej osobie dostęp do wszystkich danych w tym węźle. Dlatego w praktyce najlepiej jest, aby struktura danych była jak najbardziej płaska.
Przykładem tego, dlaczego dane ułożone są złe, jest ta wielokrotnie zagnieżdżona struktura:
{ // 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żchłej struktury przechodzenie przez dane 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 są podzielone na osobne ścieżki (tzw. denormalizacja), można je efektywnie pobierać w osobnych wywołaniach. 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 przejść przez listę pokoi, pobierając tylko kilka bajtów na rozmowę, szybko pobierając metadane do wyświetlania listy lub wyświetlania pokoi w interfejsie. Wiadomości można pobierać osobno i wyświetlać w miarę ich przychodzenia, dzięki czemu interfejs 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 umieścić obiekty podrzędne pod obiektem nadrzędnym.
Czasami ta relacja jest bardziej dynamiczna lub konieczne może być zdenormalizowanie tych danych. W wielu przypadkach można denormalizować dane za pomocą zapytania w celu pobrania podzbioru danych. Zostało to omówione w sekcji Pobieranie danych.
Ale nawet to może nie wystarczyć. Rozważ na przykład dwukierunkową relację między użytkownikami i grupami. Użytkownicy mogą należeć do grupy, a grupy zawierają listę użytkowników. Gdy przychodzi czas na określenie, do których grup należy użytkownik, sprawy się komplikują.
Potrzebny jest elegancki sposób na wyświetlenie listy grup, do których należy użytkownik, oraz pobieranie danych tylko z tych grup. Indeks grup może 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 niektóre dane są powielane, ponieważ relacja jest przechowywana zarówno w rekordzie Ada, 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 to szybkie i wydajne pobieranie członkostw Ada, nawet gdy lista użytkowników lub grup obejmuje miliony elementów lub gdy Realtime Database zasady bezpieczeństwa uniemożliwiają 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 znacznie wydajniejszy niż skanowanie lub wysyłanie zapytań do danych.