Multiple Query Execution
Multiple Query ExecutionMeervoudige query-uitvoering

Meervoudige query-uitvoering

Included in the “Power Extensions” bundle

Combineer meerdere queries in één query, deel de toestand ertussen en voer ze uit in de gevraagde volgorde.

Beschrijving

Meervoudige query-uitvoering combineert meerdere queries in één query, waarbij wordt gegarandeerd dat ze worden uitgevoerd in dezelfde gevraagde volgorde. Operaties kunnen via dynamische variabelen toestand met elkaar delen; deze worden slechts eenmaal berekend maar kunnen meerdere keren door het document worden gelezen.

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

Deze functie biedt verschillende voordelen:

  • Het verbetert de prestaties: in plaats van een query uit te voeren op de GraphQL-server, te wachten op het antwoord en dat resultaat vervolgens te gebruiken voor een nieuwe query, kunnen we de queries samenvoegen en in één enkel verzoek uitvoeren, waardoor de vertraging door meerdere HTTP-verbindingen wordt vermeden.
  • Het stelt ons in staat onze GraphQL-queries te beheren als atomaire operaties (of logische eenheden) die van elkaar afhankelijk zijn en voorwaardelijk kunnen worden uitgevoerd op basis van het resultaat van een vorige operatie.

Meervoudige query-uitvoering verschilt van query batching, waarbij de GraphQL-server ook meerdere queries in één verzoek uitvoert, maar die queries worden gewoon achter elkaar uitgevoerd, onafhankelijk van elkaar.

Ingeschakelde directives

Wanneer meervoudige query-uitvoering is ingeschakeld, zijn de volgende directives beschikbaar in het GraphQL-schema:

  • @depends (operatie-directive): om aan te geven dat een operatie (of een query of mutation) welke andere operaties eerst moeten worden uitgevoerd
  • @export (veld-directive): om de waarde van een veld uit één query te exporteren als een dynamische variabele, als invoer voor een veld of directive in een andere query
  • @exportFrom (veld-directive): vergelijkbaar met @export, maar om de waarde van een scoped dynamische variabele te exporteren (doorgegeven via @passOnwards(as: "...") of @applyField(passOnwardsAs: "..."))
  • @deferredExport (veld-directive): vergelijkbaar met @export, maar bedoeld voor gebruik met Multi-Field Directives

Bovendien zijn de directives @include en @skip ook beschikbaar als operatie-directives (normaal zijn het alleen veld-directives), en deze kunnen worden gebruikt om een operatie voorwaardelijk uit te voeren als aan een bepaalde conditie is voldaan.

@depends

Wanneer het GraphQL-document meerdere operaties bevat, geven we de server aan welke er uitgevoerd moet worden via de URL-parameter ?operationName=...; anders wordt de laatste operatie uitgevoerd.

Vanaf deze initiële operatie verzamelt de server alle uit te voeren operaties, die worden gedefinieerd door de directive depends(on: [...]) toe te voegen, en voert ze uit in de bijbehorende volgorde met respect voor de afhankelijkheden.

Het directive-argument operations ontvangt een array van operatienamen ([String]), of je kunt ook een enkele operatienaam opgeven (String).

In deze query geven we ?operationName=Four mee, en de uitgevoerde operaties (of query of mutation) zijn ["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

Directive @export exporteert de waarde van een veld (of een set velden) naar een dynamische variabele, om te gebruiken als invoer in een veld of query uit een andere query.

In deze query exporteren we bijvoorbeeld de naam van de ingelogde gebruiker en gebruiken we die waarde om berichten te zoeken die deze string bevatten (let op dat variabele $loggedInUserName, omdat het dynamisch is, niet hoeft te worden gedefinieerd in operatie FindPosts):

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

@exportFrom

Het is vergelijkbaar met @export, maar in plaats van de veldwaarde te exporteren, exporteert het de waarde van een scoped dynamische variabele, doorgegeven via @passOnwards(as: "...") of @applyField(passOnwardsAs: "...").

In deze query gebruiken we @applyField om de elementen in de array te wijzigen en deze nieuwe waarde toe te wijzen aan de scoped dynamische variabele $replaced. Vervolgens gebruiken we @exportFrom om die waarde globaal toegankelijk te maken via dynamische variabele $replacedList, zodat deze uit een volgende query kan worden opgehaald.

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

Dit produceert:

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

@deferredExport

Wanneer de functie Multi-Field Directives is ingeschakeld en we de waarde van meerdere velden naar een woordenboek exporteren, gebruik dan @deferredExport in plaats van @export om te garanderen dat alle directives van elk betrokken veld zijn uitgevoerd voordat de veldwaarde wordt geëxporteerd.

In deze query heeft het eerste veld bijvoorbeeld de directive @strUpperCase en het tweede @strTitleCase. Bij het uitvoeren van @deferredExport zal de geëxporteerde waarde deze directives toegepast hebben:

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

Dit produceert:

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

@skip en @include (in operaties)

Wanneer meervoudige query-uitvoering is ingeschakeld, zijn de directives @include en @skip ook beschikbaar als operatie-directives, en kunnen ze worden gebruikt om een operatie voorwaardelijk uit te voeren als aan een bepaalde conditie is voldaan.

In deze query exporteert operatie CheckIfPostExists bijvoorbeeld een dynamische variabele $postExists en, alleen als de waarde true is, wordt mutatie ExecuteOnlyIfPostExists uitgevoerd:

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

Uitvoer van dynamische variabelen

@export kan 6 verschillende uitvoervormen produceren, op basis van een combinatie van:

  • De waarde van het argument type (ofwel SINGLE, LIST of DICTIONARY)
  • Of de directive wordt toegepast op een enkel veld of op meerdere velden (via de module Multi-Field Directives)

De 6 mogelijke uitvoervormen zijn:

  1. Type SINGLE:
    1. Enkel veld
    2. Meerdere velden
  2. Type LIST:
    1. Enkel veld
    2. Meerdere velden
  3. Type DICTIONARY:
    1. Enkel veld
    2. Meerdere velden

Type SINGLE / Enkel veld

De uitvoer is een enkele waarde bij het doorgeven van parameter type: SINGLE (wat als standaardwaarde is ingesteld).

In deze query:

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

...heeft de dynamische variabele $postTitle de waarde:

"Hello world!"

Let op dat als SINGLE wordt toegepast op een array van entiteiten, de waarde van de laatste entiteit de geëxporteerde waarde is.

In deze query:

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

...heeft de dynamische variabele $postTitle de waarde voor het bericht met ID 5:

"Everything good?"

Type SINGLE / Meerdere velden

Als @export wordt toegepast op meerdere velden (door parameter affectAdditionalFieldsUnderPos toe te voegen, geleverd door de module Multi-Field Directives), is de waarde die wordt ingesteld op de dynamische variabele een woordenboek van { key: field alias, value: field value } (van het type JSONObject).

Deze query:

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

...exporteert dynamische variabele $postData met de waarde:

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

Type LIST / Enkel veld

De dynamische variabele bevat een array met de veldwaarde van alle bevraagde entiteiten (van het omsluitende veld), door parameter type: LIST door te geven.

Bij het uitvoeren van deze query (waarbij de bevraagde entiteiten berichten zijn met ID 1 en 5):

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

...heeft de dynamische variabele $postTitles de waarde:

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

Type LIST / Meerdere velden

We krijgen een array van woordenboeken (van het type JSONObject), elk met de waarden van de velden waarop de directive wordt toegepast.

Deze query:

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

...exporteert dynamische variabele $postsData met de waarde:

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

Type DICTIONARY / Enkel veld

De dynamische variabele bevat een woordenboek (van het type JSONObject) met het ID van de bevraagde entiteit als sleutel en de veldwaarden als waarde, door parameter type: DICTIONARY door te geven.

Deze query:

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

...exporteert dynamische variabele $postIDTitles met de waarde:

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

Type DICTIONARY / Meerdere velden

In deze combinatie exporteren we een woordenboek van woordenboeken: { key: entity ID, value: { key: field alias, value: field value } } (met een type JSONObject dat vermeldingen van het type JSONObject bevat).

Deze query:

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

...exporteert dynamische variabele $postsIDProperties met de waarde:

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

Waarden exporteren bij het itereren van een array of JSON-object

@export respecteert de kardinaliteit van elke omsluitende meta-directive.

In het bijzonder, wanneer @export is genest onder een meta-directive die itereert over array-items of JSON-objecteigenschappen (d.w.z. @underEachArrayItem en @underEachJSONObjectProperty), is de geëxporteerde waarde een array.

Deze query:

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

...produceert $contentAttributes met de waarde:

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

Dezelfde query die toegang geeft tot een specifiek item in de array in plaats van over alle items te itereren (door @underEachArrayItem te vervangen door @underArrayItem(index: 0)) exporteert daarentegen een enkele waarde.

Deze query:

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

...produceert $contentAttributes met de waarde:

"List Block"

Volgorde van directive-uitvoering

Als er andere directives voor @export staan, weerspiegelt de geëxporteerde waarde de wijzigingen door die vorige directives.

In deze query geeft de volgorde waarin @export plaatsvindt ten opzichte van @strUpperCase bijvoorbeeld een ander resultaat:

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

Dit produceert:

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

Uitvoering in Persisted Queries

Wanneer een GraphQL-query meerdere operaties bevat in een Persisted Query, kunnen we het bijbehorende eindpunt aanroepen met de URL-parameter ?operationName=... met de naam van de uit te voeren operatie; anders wordt de laatste operatie uitgevoerd.

Om operatie GetPostsContainingString uit te voeren in een Persisted Query met eindpunt /graphql-query/posts-with-user-name/, moeten we het volgende aanroepen:

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

Voorbeelden

Inhoud importeren van een extern API-eindpunt:

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

De gegevens van een bericht ophalen, transformeren en opnieuw opslaan:

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

Een bericht bijwerken als het bestaat, of anders een foutmelding tonen:

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
}

De gebruiker inloggen vóór het uitvoeren van een mutatie, en direct daarna uitloggen:

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
}

De gebruiker voorwaardelijk inloggen vóór het uitvoeren van een mutatie, indien opgegeven:

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
}

GraphQL-specificatie

Deze functionaliteit maakt momenteel geen deel uit van de GraphQL-specificatie, maar er is om gevraagd: