Architektura
ArchitekturaSilnik ładowania danych

Silnik ładowania danych

Gato GraphQL używa komponentów po stronie serwera do reprezentowania modelu danych (nie grafów ani drzew). Przyjrzyjmy się, jak wykonuje proces ładowania danych w celu rozwiązania query GraphQL.

Aby przetworzyć dane, musimy spłaszczyć komponenty do typów (<FeaturedDirector> => Director, <Film> => Film, <Actor> => Actor), uporządkować je zgodnie z kolejnością pojawiania się w hierarchii komponentów (Director, następnie Film, następnie Actor) i obsługiwać je w "iteracjach", pobierając dane obiektów dla każdego typu w jego własnej iteracji, w ten sposób:

Obsługa typów w iteracjach

Silnik ładowania danych serwera musi zaimplementować następujący (pseudo-)algorytm do ładowania danych:

Przygotowanie:

  1. Przygotować pustą kolejkę do przechowywania listy identyfikatorów obiektów, które muszą zostać pobrane z bazy danych, zorganizowanych według typu (każdy wpis będzie miał postać: [type => lista identyfikatorów])
  2. Pobrać identyfikator obiektu wyróżnionego reżysera i umieścić go w kolejce pod typem Director

Pętla aż do wyczerpania wpisów w kolejce:

  1. Pobrać pierwszy wpis z kolejki: typ i listę identyfikatorów (np.: Director i [2]), a następnie usunąć ten wpis z kolejki
  2. Używając obiektu TypeDataLoader danego typu, wykonać jedno zapytanie do bazy danych w celu pobrania wszystkich obiektów dla tego typu z podanymi identyfikatorami
  3. Jeśli typ posiada pola relacyjne (np.: typ Director posiada pole relacyjne films typu Film), zebrać wszystkie identyfikatory z tych pól ze wszystkich obiektów pobranych w bieżącej iteracji (np.: wszystkie identyfikatory z pola films ze wszystkich obiektów typu Director) i umieścić te identyfikatory w kolejce pod odpowiednim typem (np.: identyfikatory [3, 8] pod typem Film).

Po zakończeniu iteracji załadujemy wszystkie dane obiektów dla wszystkich typów, w ten sposób:

Obsługa typów w iteracjach

Zwróć uwagę, jak wszystkie identyfikatory dla danego typu są zbierane, dopóki typ nie zostanie przetworzony w kolejce. Jeśli na przykład dodamy pole relacyjne preferredActors do typu Director, te identyfikatory zostaną dodane do kolejki pod typem Actor i zostaną przetworzone razem z identyfikatorami z pola actors typu Film:

Obsługa typów w iteracjach

Jeśli jednak typ został już przetworzony i następnie musimy załadować więcej danych z tego typu, jest to nowa iteracja na tym typie. Na przykład dodanie pola relacyjnego preferredDirector do typu Author spowoduje ponowne dodanie typu Director do kolejki:

Iterowanie nad powtarzającym się typem

Teraz, gdy pobraliśmy wszystkie dane obiektów, musimy nadać im kształt oczekiwanej odpowiedzi, odzwierciedlając query GraphQL. Jednak, jak widać, dane nie mają wymaganej struktury drzewa. Zamiast tego pola relacyjne zawierają identyfikatory zagnieżdżonego obiektu, naśladując sposób reprezentacji danych w relacyjnej bazie danych. Dlatego, korzystając z tego porównania, dane pobrane dla każdego typu mogą być przedstawione jako tabela, w ten sposób:

Tabela dla typu Director:

IDnamecountryavatarfilms
2George LucasUSAgeorge-lucas.jpg[3, 8]

Tabela dla typu Film:

IDtitlethumbnailactors
3The Phantom Menaceepisode-1.jpg[4, 6]
8Attack of the Clonesepisode-2.jpg[6, 7]

Tabela dla typu Actor:

IDnameavatar
4Ewan McGregormcgregor.jpg
6Nathalie Portmanportman.jpg
7Hayden Christensenchristensen.jpg

Mając wszystkie dane zorganizowane w tabelach i wiedząc, jak każdy typ odnosi się do innych (tj. Director odwołuje się do Film przez pole films, Film odwołuje się do Actor przez pole actors), serwer GraphQL może łatwo przekonwertować dane do oczekiwanego kształtu drzewa:

Odpowiedź w kształcie drzewa

Na koniec serwer GraphQL zwraca drzewo, które ma kształt oczekiwanej odpowiedzi:

{
  data: {
    featuredDirector: {
      name: "George Lucas",
      country: "USA",
      avatar: "george-lucas.jpg",
      films: [
        {
          title: "Star Wars: Episode I",
          thumbnail: "episode-1.jpg",
          actors: [
            {
              name: "Ewan McGregor",
              avatar: "mcgregor.jpg",
            },
            {
              name: "Natalie Portman",
              avatar: "portman.jpg",
            }
          ]
        },
        {
          title: "Star Wars: Episode II",
          thumbnail: "episode-2.jpg",
          actors: [
            {
              name: "Natalie Portman",
              avatar: "portman.jpg",
            },
            {
              name: "Hayden Christensen",
              avatar: "christensen.jpg",
            }
          ]
        }
      ]
    }
  }
}

Analiza złożoności czasowej rozwiązania

Przeanalizujmy notację dużego O algorytmu ładowania danych, aby zrozumieć, jak liczba zapytań wykonywanych do bazy danych rośnie wraz ze wzrostem liczby danych wejściowych, upewniając się, że to rozwiązanie jest wydajne.

Silnik ładowania danych ładuje dane w iteracjach odpowiadających każdemu typowi. W momencie rozpoczęcia iteracji będzie już posiadał listę wszystkich identyfikatorów wszystkich obiektów do pobrania, a zatem może wykonać jedno zapytanie w celu pobrania wszystkich danych dla odpowiednich obiektów. Z tego wynika, że liczba zapytań do bazy danych będzie rosła liniowo wraz z liczbą typów zaangażowanych w query. Innymi słowy, złożoność czasowa wynosi O(n), gdzie n jest liczbą typów w query (jednak jeśli typ jest iterowany więcej niż raz, musi być dodany więcej niż raz do n).

To rozwiązanie jest bardzo wydajne, znacznie lepsze niż wykładnicza złożoność oczekiwana przy obsłudze grafów lub logarytmiczna złożoność oczekiwana przy obsłudze drzew.

Zaimplementowany kod PHP

Proces ładowania danych odbywa się w funkcji getComponentData klasy Engine w pakiecie Component Model.