Architektura
ArchitekturaManipulowanie kolejnością rozwiązywania pól

Manipulowanie kolejnością rozwiązywania pól

Celem dyrektywy @export dostarczanej przez Multiple Query Execution jest eksportowanie wartości pola (lub zestawu pól) do zmiennej, która zostanie użyta w innym miejscu w query.

Ta dyrektywa nie działałaby, gdyby odczyt zmiennej następował przed wyeksportowaniem wartości do zmiennej. Dlatego silnik musi zapewniać sposób kontrolowania kolejności wykonywania pól.

Gato GraphQL oferuje sposób manipulowania kolejnością wykonywania pól za pomocą samej query. Silnik ładuje dane iteracyjnie dla każdego typu — najpierw rozwiązuje wszystkie pola pierwszego napotkanego w query typu, potem wszystkie pola drugiego napotkanego typu i tak dalej, aż nie pozostanie żaden typ do przetworzenia.

Na przykład poniższa query obejmująca obiekty typów Director, Film i Actor:

{
  directors {
    name
    films {
      title
      actors {
        name
      }
    }
  }
}

...jest rozwiązywana przez silnik GraphQL w tej kolejności:

Obsługa typów w iteracjach

Jeśli po przetworzeniu typ zostanie ponownie odwołany w query w celu pobrania niepobranych danych (np. z dodatkowych obiektów lub dodatkowych pól już załadowanych obiektów), typ jest dodawany ponownie na końcu listy iteracji.

Na przykład, jeśli odpytamy również pole preferredDirector obiektu Actor (które zwraca obiekt typu Director) w taki sposób:

{
  directors {
    name
    films {
      title
      actors {
        name
        preferredDirector {
          name
        }
      }
    }
  }
}

...to silnik GraphQL przetwarza query w tej kolejności:

Powtarzające się typy w iteracjach

Zobaczmy, jak wygląda wykonanie @export w pojedynczej query. W pierwszej próbie tworzymy query tak jak zwykle, nie myśląc o kolejności wykonywania pól:

query GetPostsAuthorNames {
  user(by: { id: 1 }) {
    name @export(as: "authorName")
  }
  posts(filter: { search: $authorName }) {
    id
    title
  }
}

Po uruchomieniu query otrzymujemy taką odpowiedź:

Wykonanie query z użyciem zmiennej

...która zawiera następujący błąd:

{
  "errors": [
    {
      "message": "Expression 'authorName' is undefined",
    }
  ]
}

Ten błąd oznacza, że w momencie odczytu zmiennej $authorName nie była ona jeszcze ustawiona; miała wartość undefined.

Zobaczmy, dlaczego tak się dzieje. Najpierw analizujemy, jakie typy pojawiają się w query — dodane jako komentarze poniżej:

# Type: Root
query GetPostsAuthorNames {
  # Type: User
  user(by: {id: 1}) {
    # Type: String
    name @export(as: "authorName")
  }
  # Type: Post
  posts(filter: { search: $authorName }) {
    # Type: ID
    id
    # Type: String
    title
  }
}

Aby przetworzyć typy i załadować ich dane, silnik ładowania danych dodaje typ query Root do listy FIFO (First-In, First-Out, „pierwszy wchodzi, pierwszy wychodzi"), czyniąc [Root] listą początkową przekazywaną do algorytmu, a następnie iteruje po typach sekwencyjnie w taki sposób:

#OperacjaLista
0Przygotowanie listy FIFO[Root]
1aUsunięcie pierwszego typu z listy (Root)[]
1bPrzetworzenie wszystkich pól odpytanych z typu Root:
user(by: {id: 1})
posts(filter: { search: $authorName })
Dodanie ich typów (User i Post) do listy
[User, Post]
2aUsunięcie pierwszego typu z listy (User)[Post]
2bPrzetworzenie pola odpytanego z typu User:
name @export(as: "authorName")
Ponieważ jest to typ skalarny (String), nie ma potrzeby dodawania go do listy
[Post]
3aUsunięcie pierwszego typu z listy (Post)[]
3bPrzetworzenie wszystkich pól odpytanych z typu Post:
id
title
Ponieważ są to typy skalarne (ID i String), nie ma potrzeby dodawania ich do listy
[]
4Lista jest pusta, iteracja kończy się. 

Tutaj widać problem: @export jest wykonywany w kroku 2b, ale był odczytywany w kroku 1b.

Tu właśnie musimy kontrolować przepływ wykonywania pól. Zaimplementowane rozwiązanie polega na opóźnieniu momentu odczytu wyeksportowanej zmiennej, osiągniętym przez sztuczne odpytanie pola self z typu Root.

Pole self, jak wskazuje jego nazwa, zwraca ten sam obiekt; zastosowane do obiektu Root zwraca ten sam obiekt Root. Można zapytać: „skoro już mam obiekt główny, po co miałbym go pobierać ponownie?". Dlatego że algorytm silnika będzie musiał dodać to nowe odwołanie do Root na końcu listy FIFO, a my możemy celowo rozmieszczać odpytywane pola przed każdą z tych iteracji lub po niej.

Dlatego pole posts(filter:{ search: $authorName }) jest umieszczone wewnątrz pola self w powyższej query, a jej uruchomienie daje oczekiwaną odpowiedź:

query GetPostsAuthorNames {
  user(by: {id: 1}) {
    name @export(as: "authorName")
  }
  self {
    posts(filter: { search: $authorName }) {
      id
      title
    }
  }
}

Uruchomienie pierwszej query z @export

Prześledźmy kolejność przetwarzania typów w tej query, aby zrozumieć, dlaczego działa ona poprawnie:

#OperacjaLista
0Przygotowanie listy FIFO[Root]
1aUsunięcie pierwszego typu z listy (Root)[]
1bPrzetworzenie wszystkich pól odpytanych z typu Root:
user(by: {id: 1})
self
Dodanie ich typów (User i Root) do listy
[User, Root]
2aUsunięcie pierwszego typu z listy (User)[Root]
2bPrzetworzenie pola odpytanego z typu User:
name @export(as: "authorName")
Ponieważ jest to typ skalarny (String), nie ma potrzeby dodawania go do listy
[Root]
3aUsunięcie pierwszego typu z listy (Root)[]
3bPrzetworzenie pola odpytanego z typu Root:
posts(filter:{ search: $authorName })
Dodanie jego typu (Post) do listy
[Post]
4aUsunięcie pierwszego typu z listy (Post)[]
4bPrzetworzenie wszystkich pól odpytanych z typu Post:
id
title
Ponieważ są to typy skalarne (ID i String), nie ma potrzeby dodawania ich do listy
[]
5Lista jest pusta, iteracja kończy się. 

Teraz widać, że problem został rozwiązany: @export jest wykonywany w kroku 2b, a odczytywany w kroku 3b.

Multiple Query Execution robi dokładnie to podczas oddzielania queries: konwertuje dokument GraphQL, dodając pola self, tak aby pola każdej operacji były wykonywane dopiero po rozwiązaniu wszystkich pól ze wszystkich poprzednich operacji.