Çoklu Query Yürütme
Çoklu Query YürütmeÇoklu Query Yürütme

Çoklu Query Yürütme

Included in the “Power Extensions” bundle

Birden fazla queries'i tek bir query'de birleştirerek aralarında durumu paylaşır ve istenen sırayla yürütür.

Açıklama

Çoklu query yürütme, birden fazla queries'i tek bir query'de birleştirir ve bunların istenen aynı sırayla yürütülmesini sağlar. Operasyonlar, yalnızca bir kez hesaplanan ancak belge boyunca birden fazla kez okunabilen dinamik değişkenler aracılığıyla birbirleriyle durum paylaşabilir.

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

Bu özellik çeşitli avantajlar sunar:

  • Performansı artırır: GraphQL sunucusuna karşı bir query yürütmek, yanıtını beklemek ve ardından bu sonucu kullanarak başka bir query yürütmek yerine, queries'i bir araya getirip tek bir istekte yürütebiliriz; böylece birden fazla HTTP bağlantısından kaynaklanan gecikmeyi önlemiş oluruz.
  • GraphQL queries'imizi birbirine bağımlı atomik operasyonlar (ya da mantıksal birimler) olarak yönetmemize olanak tanır; bu operasyonlar bir önceki operasyonun sonucuna göre koşullu olarak yürütülebilir.

Çoklu query yürütme, query batching'den farklıdır; query batching'de GraphQL sunucusu da tek bir istekte birden fazla query yürütür, ancak bu queries yalnızca birbiri ardına ve birbirinden bağımsız olarak yürütülür.

Etkinleştirilen direktifler

Çoklu query yürütme etkinleştirildiğinde, aşağıdaki direktifler GraphQL şemasında kullanılabilir hale gelir:

  • @depends (operasyon direktifi): Bir operasyonun (query veya mutation) kendisinden önce hangi diğer operasyonların yürütülmesi gerektiğini belirtmesi için
  • @export (alan direktifi): Bir query'deki bir alan değerini dinamik değişken olarak dışa aktarmak ve başka bir query'deki bir alan ya da direktife girdi olarak kullanmak için
  • @exportFrom (alan direktifi): @export'a benzer, ancak kapsamlı dinamik değişkenin değerini dışa aktarmak için kullanılır (@passOnwards(as: "...") veya @applyField(passOnwardsAs: "...") aracılığıyla iletilir)
  • @deferredExport (alan direktifi): @export'a benzer, ancak Multi-Field Directives ile birlikte kullanılmak üzere tasarlanmıştır

Bunlara ek olarak, @include ve @skip direktifleri de operasyon direktifleri olarak kullanılabilir hale gelir (normalde yalnızca alan direktifleridir) ve bir operasyonu belirli bir koşulu sağlaması durumunda koşullu olarak yürütmek için kullanılabilir.

@depends

GraphQL belgesi birden fazla operasyon içerdiğinde, sunucuya hangisini yürüteceğini URL parametresi ?operationName=... aracılığıyla belirtiriz; aksi takdirde son operasyon yürütülür.

Bu başlangıç operasyonundan itibaren sunucu, depends(on: [...]) direktifi eklenerek tanımlanan yürütülecek tüm operasyonları toplar ve bunları bağımlılıklara saygı göstererek karşılık gelen sırayla yürütür.

Direktif argümanı operations, bir operasyon adları dizisi ([String]) alır ya da tek bir operasyon adı (String) da sağlayabiliriz.

Bu query'de ?operationName=Four parametresini geçiyoruz ve yürütülen operasyonlar (query veya mutation olsun) ["One", "Two", "Three", "Four"] olacaktır:

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

@export

@export direktifi, bir alanın (veya alanlar kümesinin) değerini dinamik bir değişken olarak dışa aktarır; bu değişken başka bir query'deki bir alan ya da query'de girdi olarak kullanılır.

Örneğin, bu query'de oturum açmış kullanıcının adını dışa aktarıyoruz ve bu değeri söz konusu dizeyi içeren gönderileri aramak için kullanıyoruz (lütfen dikkat edin: $loggedInUserName değişkeni dinamik olduğu için FindPosts operasyonunda tanımlanmasına gerek yoktur):

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

@exportFrom

@export'a benzerdir, ancak alan değerini dışa aktarmak yerine, @passOnwards(as: "...") veya @applyField(passOnwardsAs: "...") aracılığıyla iletilen kapsamlı dinamik değişkenin değerini dışa aktarır.

Örneğin, bu query'de dizideki öğeleri değiştirmek ve bu yeni değeri kapsamlı dinamik değişken $replaced'e atamak için @applyField kullanıyoruz. Ardından, bu değeri dinamik değişken $replacedList aracılığıyla global olarak erişilebilir kılmak için @exportFrom kullanıyoruz; böylece sonraki bir query'den alınabilir.

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

Bu şunu üretecektir:

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

@deferredExport

Multi-Field Directives özelliği etkinleştirildiğinde ve birden fazla alanın değerini bir sözlüğe dışa aktardığımızda, alan değerini dışa aktarmadan önce her ilgili alanın tüm direktiflerinin yürütüldüğünü garanti etmek için @export yerine @deferredExport kullanın.

Örneğin, bu query'de birinci alana @strUpperCase direktifi, ikincisine ise @strTitleCase uygulanmaktadır. @deferredExport yürütüldüğünde, dışa aktarılan değer bu direktiflerin uygulanmış halini taşır:

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

Şunu üretir:

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

@skip ve @include (operasyonlarda)

Çoklu Query Yürütme etkinleştirildiğinde, @include ve @skip direktifleri de operasyon direktifleri olarak kullanılabilir hale gelir ve bir operasyonu belirli bir koşulu sağlaması durumunda koşullu olarak yürütmek için kullanılabilir.

Örneğin, bu query'de CheckIfPostExists operasyonu bir $postExists dinamik değişkenini dışa aktarır ve yalnızca bu değişkenin değeri true ise ExecuteOnlyIfPostExists mutation'ı yürütülür:

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

Dinamik değişken çıktıları

@export şunların kombinasyonuna bağlı olarak 6 farklı çıktı üretebilir:

  • type argümanının değeri (SINGLE, LIST veya DICTIONARY)
  • Direktifin tek bir alana mı yoksa birden fazla alana mı uygulandığı (Multi-Field Directives modülü aracılığıyla)

6 olası çıktı şunlardır:

  1. SINGLE türü:
    1. Tek alan
    2. Çoklu alan
  2. LIST türü:
    1. Tek alan
    2. Çoklu alan
  3. DICTIONARY türü:
    1. Tek alan
    2. Çoklu alan

SINGLE türü / Tek alan

type: SINGLE parametresi geçildiğinde (varsayılan değer olarak ayarlanmıştır) çıktı tek bir değerdir.

Bu query'de:

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

...$postTitle dinamik değişkeni şu değere sahip olacaktır:

"Hello world!"

Lütfen dikkat edin: SINGLE bir varlık dizisi üzerinde uygulanırsa, dışa aktarılan değer son varlığa ait olan değerdir.

Bu query'de:

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

...$postTitle dinamik değişkeni ID'si 5 olan gönderi için şu değere sahip olacaktır:

"Everything good?"

SINGLE türü / Çoklu alan

@export birden fazla alana uygulanırsa (Multi-Field Directives modülünün sağladığı affectAdditionalFieldsUnderPos parametresi eklenerek), dinamik değişkene atanan değer { key: field alias, value: field value } biçiminde bir sözlük olur (JSONObject türünde).

Bu query:

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

...$postData dinamik değişkenini şu değerle dışa aktarır:

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

LIST türü / Tek alan

type: LIST parametresi geçildiğinde, dinamik değişken tüm sorgulanan varlıklardan (çevreleyen alandan) gelen alan değerini içeren bir dizi barındırır.

Bu query çalıştırıldığında (sorgulanan varlıklar ID'si 1 ve 5 olan gönderilerdir):

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

...$postTitles dinamik değişkeni şu değere sahip olacaktır:

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

LIST türü / Çoklu alan

Direktifin uygulandığı alanların değerlerini içeren sözlüklerden oluşan bir dizi (JSONObject türünde) elde ederiz.

Bu query:

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

...$postsData dinamik değişkenini şu değerle dışa aktarır:

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

DICTIONARY türü / Tek alan

type: DICTIONARY parametresi geçildiğinde, dinamik değişken sorgulanan varlığın ID'sini anahtar, alan değerlerini de değer olarak kullanan bir sözlük (JSONObject türünde) barındırır.

Bu query:

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

...$postIDTitles dinamik değişkenini şu değerle dışa aktarır:

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

DICTIONARY türü / Çoklu alan

Bu kombinasyonda, sözlüklerin sözlüğünü dışa aktarırız: { key: entity ID, value: { key: field alias, value: field value } } (JSONObject türü içinde JSONObject türünde girdiler barındırır).

Bu query:

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

...$postsIDProperties dinamik değişkenini şu değerle dışa aktarır:

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

Dizi veya JSON nesnesi üzerinde yineleme yaparken değerleri dışa aktarma

@export, çevreleyen herhangi bir meta-direktifin kardinalitesine saygı gösterir.

Özellikle, @export dizi öğeleri veya JSON nesne özellikleri üzerinde yineleme yapan bir meta-direktifin (@underEachArrayItem ve @underEachJSONObjectProperty gibi) altına yerleştirildiğinde, dışa aktarılan değer bir dizi olacaktır.

Bu query:

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

...$contentAttributes'u şu değerle üretir:

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

Buna karşılık, tüm öğeler üzerinde yineleme yapmak yerine dizideki belirli bir öğeye erişen aynı query (@underEachArrayItem yerine @underArrayItem(index: 0) kullanarak) tek bir değer dışa aktaracaktır.

Bu query:

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

...$contentAttributes'u şu değerle üretir:

"List Block"

Direktif yürütme sırası

@export'tan önce başka direktifler varsa, dışa aktarılan değer bu önceki direktiflerin yaptığı değişiklikleri yansıtır.

Örneğin, bu query'de @export'un @strUpperCase'den önce mi yoksa sonra mı gerçekleştiğine bağlı olarak sonuç farklı olacaktır:

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

Şunu üretir:

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

Persisted Queries'de yürütme

Bir GraphQL query'si, bir Persisted Query içinde birden fazla operasyon içerdiğinde, yürütülecek operasyonun adını ?operationName=... URL parametresiyle ileterek ilgili endpoint'i çağırabiliriz; aksi takdirde son operasyon yürütülür.

Örneğin, /graphql-query/posts-with-user-name/ endpoint'ine sahip bir Persisted Query'de GetPostsContainingString operasyonunu yürütmek için şunu çağırmalıyız:

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

Örnekler

Harici bir API endpoint'inden içerik içe aktarma:

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

Bir gönderi için verileri alın, dönüştürün ve yeniden saklayın:

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

Bir gönderi mevcutsa güncelleyin, aksi takdirde hata mesajı gösterin:

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
}

Bir mutation yürütmeden önce kullanıcıyı oturum açtırın ve hemen ardından oturumu kapatın:

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
}

Kimlik bilgileri sağlanmışsa, bir mutation yürütmeden önce kullanıcıyı koşullu olarak oturum açtırın:

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 spec

Bu işlevsellik şu anda GraphQL spec'in bir parçası değildir, ancak talep edilmiştir: