Strukturyzowanie danych
W tym przewodniku opisujemy niektóre z kluczowych pojęć związanych z architekturą danych oraz sprawdzone metody strukturyzowania danych JSON w Firebase Realtime Database.
Utworzenie prawidłowo ustrukturyzowanej bazy danych wymaga sporo przemyśleń. Najważniejsze jest zaplanowanie sposobu zapisywania i późniejszego pobierania danych, aby maksymalnie uprościć ten proces.
Jak dane są uporządkowane: jest to drzewo JSON.
Wszystkie dane Firebase Realtime Database są przechowywane jako obiekty JSON. Bazę danych można traktować jako drzewo JSON hostowane w chmurze. W odróżnieniu od bazy danych SQL nie ma tu tabel ani rekordów. Gdy dodajesz dane do drzewa JSON, stają się one 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, albo skorzystać z metody Push().
Weź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, np. /users/$uid. Wpis w bazie danych dotyczący użytkownika alovelace może wyglądać mniej więcej tak:
{ "users": { "alovelace": { "name": "Ada Lovelace", "contacts": { "ghopper": true }, }, "ghopper": { "..." }, "eclarke": { "..." } } }
Chociaż baza danych używa drzewa JSON, dane przechowywane w magazynie danych mogą być reprezentowane jako określone typy natywne, które odpowiadają dostępnym typom JSON, co ułatwia pisanie kodu, który jest łatwiejszy w utrzymaniu.
Sprawdzone metody dotyczące struktury danych
Unikaj zagnieżdżania danych
Firebase Realtime Database umożliwia zagnieżdżanie danych do 32 poziomów, więc może się wydawać, że powinna to być domyślna struktura. Jednak gdy pobierasz dane z lokalizacji w bazie danych, pobierasz też wszystkie jej węzły podrzędne. Dodatkowo, gdy przyznasz komuś uprawnienia do odczytu lub zapisu w węźle bazy danych, dajesz mu też dostęp do wszystkich danych w tym węźle. Dlatego w praktyce najlepiej jest zachować jak najbardziej płaską strukturę danych.
Aby zobaczyć, dlaczego zagnieżdżone dane są niekorzystne, rozważ tę strukturę zagnieżdżoną wielokrotnie:
{ // 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 takiego zagnieżdżonego projektu iteracja danych staje się problematyczna. Na przykład wyświetlenie listy tytułów rozmów na czacie wymaga pobrania na klienta całego drzewa chats, w tym wszystkich uczestników i wiadomości.
Spłaszczanie struktur danych
Jeśli dane są podzielone na osobne ścieżki, co nazywa się denormalizacją, można je skutecznie pobierać w osobnych wywołaniach, gdy są potrzebne. Rozważ tę uproszczoną 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 rozmowę, co pozwala szybko pobierać metadane do wyświetlania listy lub pokoi w interfejsie. Wiadomości można pobierać osobno i wyświetlać w miarę ich napływania, dzięki czemu interfejs pozostaje szybki i responsywny.
Tworzenie danych, które można skalować
Podczas tworzenia aplikacji często lepiej jest pobrać podzbiór listy. Zdarza się to zwłaszcza wtedy, 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ć konieczne zdenormalizowanie tych danych. W wielu przypadkach możesz zdenormalizować dane, używając zapytania do pobrania podzbioru danych, jak opisano w sekcji Pobieranie danych.
Jednak 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 zawierają listę użytkowników. Gdy trzeba zdecydować, do których grup należy użytkownik, sprawy się komplikują.
Potrzebujemy eleganckiego sposobu na wyświetlenie listy grup, do których należy użytkownik, i pobranie danych tylko dla tych grup. W tym przypadku bardzo pomocny może być indeks grup:
// 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 powielenie niektórych danych, ponieważ relacja jest przechowywana zarówno w rekordzie Ady, jak i w grupie. Teraz alovelace jest indeksowany w grupie, a techpioneers jest wymieniony w profilu Ady. Aby usunąć Adę z grupy, trzeba ją zaktualizować w 2 miejscach.
Jest to niezbędna nadmiarowość w przypadku relacji dwukierunkowych. Umożliwia szybkie i wydajne pobieranie informacji o członkostwie Ady, nawet gdy lista użytkowników lub grup obejmuje miliony pozycji lub gdy reguły zabezpieczeń Realtime Database uniemożliwiają dostęp do niektórych rekordów.
Takie podejście, czyli odwrócenie danych przez wyświetlenie 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 jest to null. Indeks jest szybszy i znacznie bardziej wydajny niż wysyłanie zapytań do danych lub ich skanowanie.