Multiple Query Execution
Multiple Query ExecutionWykonywanie wielu queries

Wykonywanie wielu queries

Included in the “Power Extensions” bundle

Połącz wiele queries w jedną query, współdzieląc stan między nimi i wykonując je w żądanej kolejności.

Opis

Wykonywanie wielu queries łączy kilka queries w jedną query, zapewniając, że są one wykonywane w tej samej żądanej kolejności. Operacje mogą komunikować stan między sobą za pomocą zmiennych dynamicznych, które są obliczane tylko raz, ale mogą być odczytywane wielokrotnie w całym dokumencie.

query SomeQuery {
  id @export(as: "rootID")
}
 
query AnotherQuery
  @depends(on: "SomeQuery")
{
  _echo(value: $rootID )
}

Ta funkcja oferuje kilka korzyści:

  • Poprawia wydajność: zamiast wykonywać query na serwerze GraphQL, czekać na odpowiedź, a następnie używać tego wyniku do wykonania kolejnej query, możemy połączyć queries w jedną i wykonać je w jednym żądaniu, unikając tym samym opóźnienia wynikającego z wielu połączeń HTTP.
  • Pozwala nam zarządzać naszymi queries GraphQL jako atomowymi operacjami (lub jednostkami logicznymi), które zależą od siebie i mogą być wykonywane warunkowo na podstawie wyniku poprzedniej operacji.

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

Włączone dyrektywy

Gdy wykonywanie wielu queries jest włączone, następujące dyrektywy są udostępniane w schemacie GraphQL:

  • @depends (dyrektywa operacji): aby operacja (czy to query czy mutation) wskazywała, jakie inne operacje muszą być wykonane wcześniej
  • @export (dyrektywa pola): aby wyeksportować wartość pola z jednej query jako zmienną dynamiczną, do użycia jako wejście w jakimś polu lub dyrektywie w innej query
  • @exportFrom (dyrektywa pola): podobna do @export, ale służy do eksportowania wartości zmiennej dynamicznej o ograniczonym zasięgu (przekazywanej przez @passOnwards(as: "...") lub @applyField(passOnwardsAs: "..."))
  • @deferredExport (dyrektywa pola): podobna do @export, ale przeznaczona do użycia z Multi-Field Directives

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

@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 we właściwej kolejności, respektując zależności.

Argument operations dyrektywy przyjmuje tablicę nazw operacji ([String]) lub możemy też 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 ...
}

@export

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

Na przykład w tej query eksportujemy nazwę zalogowanego użytkownika i używamy tej wartości do wyszukiwania postów zawierających ten ciąg znaków (proszę zwrócić uwagę, ż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
  }
}

@exportFrom

Jest podobna do @export, ale zamiast eksportować wartość pola, eksportuje wartość zmiennej dynamicznej o ograniczonym zasięgu, przekazywanej przez @passOnwards(as: "...") lub @applyField(passOnwardsAs: "...").

Na przykład w tej query używamy @applyField do modyfikowania elementów tablicy i przypisania tej nowej wartości do zmiennej dynamicznej o ograniczonym zasięgu $replaced. Następnie używamy @exportFrom, aby udostępnić tę wartość globalnie przez zmienną dynamiczną $replacedList, tak aby mogła być pobrana z kolejnej query.

query One {    
  originalList: _echo(value: ["Hello everyone", "How are you?"])
    @underEachArrayItem(
      passValueOnwardsAs: "value"
      affectDirectivesUnderPos: [1, 2]
    )
      @applyField(
        name: "_strReplace"
        arguments: {
          search: " "
          replaceWith: "-"
          in: $value
        },
        passOnwardsAs: "replaced"
      )
      @exportFrom(
        scopedDynamicVariable: $replaced,
        as: "replacedList"
      )
}
 
query Two @depends(on: "One") {
  transformedList: _echo(value: $replacedList)
}

Spowoduje to wyprodukowanie:

{
  "data": {
    "originalList": [
      "Hello everyone",
      "How are you?"
    ],
    "transformedList": [
      "Hello-everyone",
      "How-are-you?"
    ]
  }
}

@deferredExport

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 wyeksportowaniem wartości pola.

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

query One {
  id @strUpperCase # Will be exported as "ROOT"
  again: id @strTitleCase # 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"
    }
  }
}

@skip i @include (w operacjach)

Gdy wykonywanie wielu queries 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 pewien warunek.

Na przykład w tej query operacja CheckIfPostExists eksportuje zmienną dynamiczną $postExists i tylko wtedy, gdy 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...
}

Wyjścia zmiennych dynamicznych

@export może produkować 6 różnych wyjść, na podstawie kombinacji:

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

6 możliwych wyjść 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

Wyjście jest pojedynczą wartością po przekazaniu parametru type: SINGLE (ustawionego jako wartość domyślna).

W tej query:

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

...zmienna dynamiczna $postTitle będzie mieć wartość:

"Hello world!"

Proszę zwrócić uwagę, że jeśli SINGLE jest zastosowane 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 mieć wartość dla posta z ID 5:

"Everything good?"

Typ SINGLE / Wiele pól

Jeśli @export jest stosowane do kilku pól (przez dodanie parametru affectAdditionalFieldsUnderPos dostarczonego przez moduł Multi-Field Directives), wartość ustawiona w zmiennej dynamicznej jest słownikiem { key: field alias, value: field value } (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 pola obejmującego), po przekazaniu parametru type: LIST.

Przy uruchamianiu 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 mieć 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 stosowana jest dyrektywa.

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ą, po przekazaniu 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: entity ID, value: { key: field alias, value: field value } } (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."
  }
}

Eksportowanie wartości podczas iterowania tablicy lub obiektu JSON

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

W szczególności, zawsze gdy @export jest zagnieżdżone 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"
]

Natomiast ta sama query, która uzyskuje dostęp do konkretnego elementu w 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 są 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 różny:

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

Wykonywanie w Persisted Queries

Gdy query GraphQL zawiera wiele operacji w Persisted Query, możemy wywołać odpowiedni endpoint, przekazując parametr URL ?operationName=... z nazwą operacji do wykonania; w przeciwnym razie zostanie wykonana ostatnia operacja.

Na przykład, aby wykonać operację GetPostsContainingString w Persisted Query z endpointem /graphql-query/posts-with-user-name/, musimy wywołać:

https://mysite.com/graphql-query/posts-with-user-name/?operationName=GetPostsContainingString

Przykłady

Importowanie treści z zewnętrznego endpointu API:

query FetchDataFromExternalEndpoint
{
  _sendJSONObjectItemHTTPRequest(input: { url: "https://site.com/wp-json/wp/posts/1" } )
    @export(as: "externalData")
    @remove
}
 
query ManipulateDataIntoInput @depends(on: "FetchDataFromExternalEndpoint")
{
  title: _objectProperty(
    object: $externalData,
    by: {
      path: "title.rendered"
    }
  ) @export(as: "postTitle")
 
  excerpt: _objectProperty(
    object: $externalData,
    by: {
      key: "excerpt"
    }
  ) @export(as: "postExcerpt")
}
 
mutation CreatePost @depends(on: "ManipulateDataIntoInput")
{
  createPost(input: {
    title: $postTitle
    excerpt: $postExcerpt
  }) {
    id
  }
}

Pobieranie danych posta, przekształcanie ich i ponowne zapisywanie:

query GetPostData(
  $postId: ID!
) {
  post(by: {id: $postId}) {
    id
    title @export(as: "postTitle")
    rawContent @export(as: "postContent")
  }
}
 
query AdaptPostData(
  $replaceFrom: String!,
  $replaceTo: String!
)
  @depends(on: "GetPostData")
{
  adaptedPostTitle: _strReplace(
    search: $replaceFrom
    replaceWith: $replaceTo
    in: $postTitle
  )
    @export(as: "adaptedPostTitle")
 
  adaptedPostContent: _strReplace(
    search: $replaceFrom
    replaceWith: $replaceTo
    in: $postContent
  )
    @export(as: "adaptedPostContent")
}
 
mutation StoreAdaptedPostData(
  $postId: ID!
)
  @depends(on: "AdaptPostData")
{
  updatePost(input: {
    id: $postId,
    title: $adaptedPostTitle,
    contentAs: { html: $adaptedPostContent },
  }) {
    status
    errors {
      __typename
      ...on ErrorPayload {
        message
      }
    }
    post {
      id
      title
      rawContent
    }
  }
}

Aktualizacja posta, jeśli istnieje, lub wyświetlenie komunikatu o błędzie w przeciwnym razie:

query GetPost($id: ID!) {
  post(by:{id: $id}) {
    id
    title
  }
  _notNull(value: $__post) @export(as: "postExists")
}
 
query FailIfPostNotExists($id: ID!)
  @skip(if: $postExists)
  @depends(on: "GetPost")
{
  errorMessage: _sprintf(
    string: "There is no post with ID '%s'",
    values: [$id]
  ) @remove
  _fail(
    message: $__errorMessage
    data: {
      id: $id
    }
  ) @remove
}
 
mutation UpdatePost($id: ID!, $postTitle: String)
  @include(if: $postExists)
  @depends(on: "GetPost")
{
  updatePost(input: {
    id: $id,
    title: $postTitle,
  }) {
    status
    errors {
      __typename
      ...on ErrorPayload {
        message
      }
    }
    post {
      id
      title
      rawContent
    }
  }
}
 
query MaybeUpdatePost
  @depends(on: [
      "FailIfPostNotExists",
      "UpdatePost"
  ])
{
  id @remove
}

Logowanie użytkownika przed wykonaniem mutacji i wylogowanie go natychmiast po:

mutation LogUserIn(
  $username: String!
  $password: String!
) {
  loginUser(by: {
    credentials: {
      usernameOrEmail: $username,
      password: $password
    }
  }) @remove {
    status
    user {
      id
      username
    }
  }
}
 
mutation AddComment(
  $customPostId: ID!
  $commentContent: HTML!
)
  @depends(on: "LogUserIn")
{
  addCommentToCustomPost(input: {
    customPostID: $customPostId,
    commentAs: { html: $commentContent }
  }) {
    status
    errors {
      __typename
      ...on ErrorPayload {
        message
      }
    }
    comment {
      id
      parent {
        id
      }
      content
      date
      author {
        name
        email
      }
    }
  }
}
 
mutation LogUserOut
  @depends(on: "AddComment")
{
  logoutUser @remove {
    status
    userID
  }
}
 
query ExecuteAllAddCommentOperations
  @depends(on: "LogUserOut")
{
  id @remove
}

Warunkowe logowanie użytkownika przed wykonaniem mutacji, jeśli dane uwierzytelniające zostały podane:

query ExportUserLogin(
  $username: String
) {
  _notNull(value: $username)
    @export(as: "hasUsername")
    @remove
}
 
mutation MaybeLogUserIn(
  $username: String
  $password: String
)
  @depends(on: "ExportUserLogin")
  @include(if: $hasUsername)
{
  loginUser(by: {
    credentials: {
      usernameOrEmail: $username,
      password: $password
    }
  }) @remove {
    status
    user {
      id
      username
    }
  }
}
 
mutation AddComment(
  $customPostId: ID!
  $commentContent: HTML!
)
  @depends(on: "MaybeLogUserIn")
{
  addCommentToCustomPost(input: {
    customPostID: $customPostId,
    commentAs: { html: $commentContent }
  }) {
    status
    errors {
      __typename
      ...on ErrorPayload {
        message
      }
    }
    comment {
      id
      parent {
        id
      }
      content
      date
      author {
        name
        email
      }
    }
  }
}
 
mutation MaybeLogUserOut
  @depends(on: "AddComment")
  @include(if: $hasUsername)
{
  logoutUser @remove {
    status
    userID
  }
}
 
query ExecuteAllAddCommentOperations
  @depends(on: "MaybeLogUserOut")
{
  id @remove
}

Specyfikacja GraphQL

Ta funkcjonalność nie jest obecnie częścią specyfikacji GraphQL, ale była wnioskowana: