Konfigurowanie schematu
Konfigurowanie schematuWykonywanie wielu queries jednocześnie

Wykonywanie wielu queries jednocześnie

Wiele queries można łączyć i wykonywać jako jedną operację, współdzieląc ich stan i dane.

Różni się to od query batching, gdzie serwer GraphQL również wykonuje wiele queries w jednym żądaniu, ale te queries są po prostu wykonywane jedna po drugiej, niezależnie od siebie.

Ta funkcja poprawia wydajność. Zamiast wykonywać queries niezależnie w różnych żądaniach (najpierw wykonując operację przeciwko serwerowi GraphQL, czekając na odpowiedź, a następnie używając tego wyniku do wykonania kolejnej operacji), możemy wykonać je razem, unikając w ten sposób opóźnień wynikających z wielu żądań.

Multiple Query Execution pozwala nam również lepiej organizować nasze queries GraphQL, dzieląc je na logiczne jednostki, które od siebie zależą i są warunkowo wykonywane na podstawie wyniku poprzedniej operacji.

Jak używać wykonywania wielu queries

Załóżmy, że chcemy wyszukać wszystkie posty, które zawierają imię zalogowanego użytkownika. Normalnie potrzebowalibyśmy dwóch queries, aby to osiągnąć:

Najpierw pobieramy name użytkownika:

query GetLoggedInUserName {
  me {
    name
  }
}

...a następnie, po wykonaniu pierwszej query, możemy przekazać pobrane name użytkownika jako zmienną $search, aby wykonać wyszukiwanie w drugiej query:

query GetPostsContainingString($search: String!) {
  posts(filter: { search: $search }) {
    id
    title
  }
}

Multiple Query Execution upraszcza ten proces, umożliwiając pobranie wszystkich danych i wykonanie całej wymaganej logiki w jednym żądaniu:

query GetLoggedInUserName {
  me {
    name @export(as: "search")
  }
}
 
query GetPostsContainingString @depends(on: "GetLoggedInUserName") {
  posts(filter: { search: $search }) {
    id
    title
  }
}

Multiple Query Execution jest osiągane za pomocą tych specjalnych dyrektyw:

  • @depends (dyrektywa operacji): sprawia, że operacja (czy to query, czy mutation) wskazuje, jakie inne operacje muszą zostać wykonane wcześniej
  • @export (dyrektywa pola): eksportuje wartość pola z jednej operacji, aby wstrzyknąć ją jako dane wejściowe do pola w innej operacji
  • @deferredExport (dyrektywa pola): podobna do @export, ale do użycia z Multi-Field Directives.

Ponadto dyrektywy @include i @skip są również dostępne jako dyrektywy operacji (normalnie są tylko dyrektywami pola), i mogą być używane do warunkowego wykonania operacji, jeśli spełnia ona pewien warunek.

Serwer GraphQL utworzy listę operacji do załadowania i wykonania, pobierając je z każdego @depends(on: ...), i wyeksportuje wartości z dowolnego pola zawierającego @export jako zmienną dynamiczną (o nazwie zdefiniowanej w argumencie as), aby użyć jej jako danych wejściowych w kolejnych operacjach.

Łącząc te dyrektywy, jesteśmy w stanie podzielić dowolną złożoną funkcjonalność na etapy pośrednie, naprzemiennie używając operacji query i mutation, dodając ich zależności w wymaganej kolejności i wykonując je wszystkie w jednym żądaniu, definiując najbardziej zewnętrzną operację w ?operationName=... (w powyższym przykładzie będzie to ?operationName=GetPostsContainingString).

Definiowanie operacji do załadowania i wykonania via @depends

Gdy dokument GraphQL zawiera wiele operacji, wskazujemy serwerowi, którą z nich wykonać za pomocą parametru URL ?operationName=...; w przeciwnym razie zostanie wykonana ostatnia operacja.

Zaczynając od tej początkowej operacji, serwer zbierze wszystkie operacje do wykonania, które są definiowane przez dodanie dyrektywy depends(on: [...]), i wykona je w odpowiedniej kolejności, respektując zależności.

Argument operations dyrektywy przyjmuje tablicę nazw operacji ([String]), lub możemy również podać pojedynczą nazwę operacji (String).

W tej query przekazujemy ?operationName=Four, a wykonane operacje (czy to query, czy mutation) będą ["One", "Two", "Three", "Four"]:

mutation One {
  # Do something ...
}
 
mutation Two {
  # Do something ...
}
 
query Three @depends(on: ["One", "Two"]) {
  # Do something ...
}
 
query Four @depends(on: "Three") {
  # Do something ...
}

Współdzielenie danych między queries via @export

Dyrektywa @export eksportuje wartość pola (lub zestawu pól) do zmiennej dynamicznej, która ma być użyta jako dane wejściowe w polu innej query.

Na przykład, w tej query eksportujemy imię zalogowanego użytkownika i używamy tej wartości do wyszukiwania postów zawierających ten ciąg znaków (należy zauważyć, że zmienna $loggedInUserName, ponieważ jest dynamiczna, nie musi być zdefiniowana w operacji FindPosts):

query GetLoggedInUserName {
  me {
    name @export(as: "loggedInUserName")
  }
}
 
query FindPosts @depends(on: "GetLoggedInUserName") {
  posts(filter: { search: $loggedInUserName }) {
    id
  }
}

Wyniki zmiennych dynamicznych

@export może produkować 6 różnych wyników, opartych na kombinacji:

  • Wartości argumentu type (albo SINGLE, LIST lub DICTIONARY)
  • Czy dyrektywa jest stosowana do jednego pola, czy do wielu pól (via moduł Multi-Field Directives)

6 możliwych wyników to:

  1. Typ SINGLE:
    1. Pojedyncze pole
    2. Wiele pól
  2. Typ LIST:
    1. Pojedyncze pole
    2. Wiele pól
  3. Typ DICTIONARY:
    1. Pojedyncze pole
    2. Wiele pól

Typ SINGLE / Pojedyncze pole

Wynik jest pojedynczą wartością przy przekazaniu parametru type: SINGLE (który jest ustawiony jako wartość domyślna).

W tej query:

query {
  post(by: { id: 1 }) {
    title @export(as: "postTitle", type: SINGLE)
  }
}

...zmienna dynamiczna $postTitle będzie miała wartość:

"Hello world!"

Należy zauważyć, że jeśli SINGLE jest zastosowany do tablicy encji, eksportowana jest wartość ostatniej encji.

W tej query:

query {
  posts(filter: { ids: [1, 5] }) {
    title @export(as: "postTitle", type: SINGLE)
  }
}

...zmienna dynamiczna $postTitle będzie miała wartość dla posta z ID 5:

"Everything good?"

Typ SINGLE / Wiele pól

Jeśli @export jest stosowany do kilku pól (przez dodanie parametru affectAdditionalFieldsUnderPos dostarczonego przez moduł Multi-Field Directives), wartość ustawiana w zmiennej dynamicznej jest słownikiem { key: alias pola, value: wartość pola } (typu JSONObject).

Ta query:

query {
  post(by: { id: 1 }) {
    title
    content
      @export(
        as: "postData",
        type: SINGLE,
        affectAdditionalFieldsUnderPos: [1]
      )
  }
}

...eksportuje zmienną dynamiczną $postData z wartością:

{
  "title": "Hello world!",
  "content": "Lorem ipsum."
}

Typ LIST / Pojedyncze pole

Zmienna dynamiczna będzie zawierać tablicę z wartością pola ze wszystkich zapytanych encji (z otaczającego pola), przez przekazanie parametru type: LIST.

Podczas wykonywania tej query (w której zapytanymi encjami są posty z ID 1 i 5):

query {
  posts(filter: { ids: [1, 5] }) {
    title @export(as: "postTitles", type: LIST)
  }
}

...zmienna dynamiczna $postTitles będzie miała wartość:

[
  "Hello world!",
  "Everything good?"
]

Typ LIST / Wiele pól

Otrzymujemy tablicę słowników (typu JSONObject), z których każdy zawiera wartości pól, do których dyrektywa jest stosowana.

Ta query:

query {
  posts(filter: { ids: [1, 5] }) {
    title
    content
      @export(
        as: "postsData",
        type: LIST,
        affectAdditionalFieldsUnderPos: [1]
      )
  }
}

...eksportuje zmienną dynamiczną $postsData z wartością:

[
  {
    "title": "Hello world!",
    "content": "Lorem ipsum."
  },
  {
    "title": "Everything good?",
    "content": "Quisque convallis libero in sapien pharetra tincidunt."
  }
]

Typ DICTIONARY / Pojedyncze pole

Zmienna dynamiczna będzie zawierać słownik (typu JSONObject) z ID zapytanej encji jako kluczem i wartościami pola jako wartością, przez przekazanie parametru type: DICTIONARY.

Ta query:

query {
  posts(filter: { ids: [1, 5] }) {
    title @export(as: "postIDTitles", type: DICTIONARY)
  }
}

...eksportuje zmienną dynamiczną $postIDTitles z wartością:

{
  "1": "Hello world!",
  "5": "Everything good?"
}

Typ DICTIONARY / Wiele pól

W tej kombinacji eksportujemy słownik słowników: { key: ID encji, value: { key: alias pola, value: wartość pola } } (używając typu JSONObject, który będzie zawierał wpisy typu JSONObject).

Ta query:

query {
  posts(filter: { ids: [1, 5] }) {
    title
    content
      @export(
        as: "postsIDProperties",
        type: DICTIONARY,
        affectAdditionalFieldsUnderPos: [1]
      )
  }
}

...eksportuje zmienną dynamiczną $postsIDProperties z wartością:

{
  "1": {
    "title": "Hello world!",
    "content": "Lorem ipsum."
  },
  "5": {
    "title": "Everything good?",
    "content": "Quisque convallis libero in sapien pharetra tincidunt."
  }
}

Warunkowe wykonywanie operacji

Gdy Multiple Query Execution jest włączone, dyrektywy @include i @skip są również dostępne jako dyrektywy operacji i mogą być używane do warunkowego wykonania operacji, jeśli spełnia ona pewien warunek.

Na przykład, w tej query operacja CheckIfPostExists eksportuje zmienną dynamiczną $postExists i tylko jeśli jej wartość wynosi true, mutacja ExecuteOnlyIfPostExists zostanie wykonana:

query CheckIfPostExists($id: ID!) {
  # Initialize the dynamic variable to `false`
  postExists: _echo(value: false) @export(as: "postExists")
 
  post(by: { id: $id }) {
    # Found the Post => Set dynamic variable to `true`
    postExists: _echo(value: true) @export(as: "postExists")
  }
}
 
mutation ExecuteOnlyIfPostExists
  @depends(on: "CheckIfPostExists")
  @include(if: $postExists)
{
  # Do something...
}

Eksportowanie wartości podczas iterowania tablicy lub obiektu JSON

@export respektuje kardynalność z dowolnej otaczającej meta-dyrektywy.

W szczególności, gdy @export jest zagnieżdżony poniżej meta-dyrektywy, która iteruje po elementach tablicy lub właściwościach obiektu JSON (tj. @underEachArrayItem i @underEachJSONObjectProperty), eksportowana wartość będzie tablicą.

Ta query:

{
  post(by: { id: 19 }) {
    coreContentAttributeBlocks: blockFlattenedDataItems(
      filterBy: { include: "core/heading" }
    )
      @underEachArrayItem
        @underJSONObjectProperty(
          by: { path: "attributes.content" },
        )
          @export(
            as: "contentAttributes",
          )
  }
}

...produkuje $contentAttributes z wartością:

[
  "List Block",
  "Columns Block",
  "Columns inside Columns (nested inner blocks)",
  "Life is so rich",
  "Life is so dynamic"
]

W przeciwieństwie do tego, ta sama query, która uzyskuje dostęp do konkretnego elementu tablicy zamiast iterować po wszystkich (zastępując @underEachArrayItem przez @underArrayItem(index: 0)), wyeksportuje pojedynczą wartość.

Ta query:

{
  post(by: { id: 19 }) {
    coreContentAttributeBlocks: blockFlattenedDataItems(
      filterBy: { include: "core/heading" }
    )
      @underArrayItem(index: 0)
        @underJSONObjectProperty(
          by: { path: "attributes.content" },
        )
          @export(
            as: "contentAttributes",
          )
  }
}

...produkuje $contentAttributes z wartością:

"List Block"

Kolejność wykonywania dyrektyw

Jeśli przed @export znajdują się inne dyrektywy, eksportowana wartość będzie odzwierciedlać modyfikacje dokonane przez te poprzednie dyrektywy.

Na przykład, w tej query, w zależności od tego, czy @export następuje przed, czy po @strUpperCase, wynik będzie inny:

query One {
  id
    # First export "root", only then will be converted to "ROOT"
    @export(as: "id")
    @strUpperCase
 
  again: id
    # First convert to "ROOT" and then export this value
    @strUpperCase
    @export(as: "again")
}
 
query Two @depends(on: "One") {
  mirrorID: _echo(value: $id)
  mirrorAgain: _echo(value: $again)
}

Produkując:

{
  "data": {
    "id": "ROOT",
    "again": "ROOT",
    "mirrorID": "root",
    "mirrorAgain": "ROOT"
  }
}

Multi-Field Directives

Gdy funkcja Multi-Field Directives jest włączona i eksportujemy wartość wielu pól do słownika, użyj @deferredExport zamiast @export, aby zagwarantować, że wszystkie dyrektywy z każdego zaangażowanego pola zostały wykonane przed eksportowaniem wartości pola.

Na przykład, w tej query, pierwsze pole ma zastosowaną dyrektywę @strUpperCase, a drugie @titleCase. Podczas wykonywania @deferredExport eksportowana wartość będzie miała zastosowane te dyrektywy:

query One {
  id @strUpperCase # Will be exported as "ROOT"
  again: id @titleCase # Will be exported as "Root"
    @deferredExport(as: "props", affectAdditionalFieldsUnderPos: [1])
}
 
query Two @depends(on: "One") {
  mirrorProps: _echo(value: $props)
}

Produkując:

{
  "data": {
    "id": "ROOT",
    "again": "Root",
    "mirrorProps": {
      "id": "ROOT",
      "again": "Root"
    }
  }
}

Specyfikacja GraphQL

Ta funkcjonalność nie jest obecnie częścią specyfikacji GraphQL, ale została zgłoszona jako propozycja: