Tworzenie struktury bazy danych

Zanim zaczniesz

Zanim użyjesz Realtime Database, musisz:

  • Zarejestruj projekt Unity i skonfiguruj go tak, aby używał Firebase.

    • Jeśli Twój projekt w Unity korzysta już z Firebase, jest już zarejestrowany i skonfigurowany pod kątem tej usługi.

    • Jeśli nie masz projektu w Unity, możesz pobrać przykładową aplikację.

  • Dodaj pakiet SDK Firebase Unity (szczególnie plik FirebaseDatabase.unitypackage) do projektu Unity.

Pamiętaj, że dodanie Firebase do projektu Unity wymaga wykonania zadań zarówno w konsoli Firebase, jak i w otwartym projekcie Unity (np. pobieranie plików konfiguracji Firebase z konsoli i przenoszenie ich do projektu Unity).

Tworzenie struktury danych

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. Bazę danych możesz sobie wyobrazić 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 dotychczasowej strukturze JSON z powiązanym kluczem. Możesz podać własne klucze, takie jak identyfikatory użytkowników lub nazwy semantyczne, albo możesz je otrzymać za pomocą metody 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 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ć wpis bazy danych podobny do tego:

{
  "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

Ponieważ Firebase Realtime Database umożliwia zagnieżdżanie danych na 32 poziomach, możesz pomyśleć, że to powinna być domyślna struktura. Gdy jednak pobierasz dane z lokalizacji w bazie danych, pobierasz też wszystkie jej węzły podrzędne. Ponadto, gdy przyznasz komuś dostęp do odczytu lub zapisu w węźle w bazie danych, przyznasz mu też 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 tytułów rozmów na czacie wymaga pobrania na klienta całego drzewa chats, w tym wszystkich użytkowników i wiadomości.

Spłaszczanie struktur danych

Jeśli dane są podzielone na osobne ścieżki (tzw. denormalizacja), można je efektywnie pobierać 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 wyświetlania informacji o pokojach lub wyświetlania ich 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. 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. Często możesz zdenormalizować dane, używając zapytania do pobrania podzbioru danych, jak opisano w artykule Pobieranie danych.

Jednak nawet to może nie być wystarczające. Rozważ na przykład dwukierunkową relację między użytkownikami a 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 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 część danych jest duplikowana, ponieważ przechowuje ona relację zarówno w rekordzie Ady, jak i w grupie. Teraz alovelace jest indeksowany w ramach grupy, a techpioneers jest wymieniony w profilu Ada. Aby usunąć Ada z grupy, trzeba zaktualizować tę informację w 2 miejscach.

Jest to konieczna nadmiarowość w przypadku relacji dwukierunkowych. Pozwala na 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ń Realtime Database uniemożliwiają dostęp do niektórych rekordów.

Takie podejście, polegające na odwróceniu danych przez podanie identyfikatorów jako kluczy i ustawienie wartości na „prawda”, sprawia, że sprawdzenie klucza jest tak proste jak odczytanie wartości /users/$uid/groups/$group_id i sprawdzenie, czy jest to null. Indeks jest szybszy i znacznie wydajniejszy niż skanowanie lub wysyłanie zapytań do danych.

Następne kroki