Thực Thi Nhiều Queries
Thực Thi Nhiều QueriesThực Thi Nhiều Queries

Thực Thi Nhiều Queries

Included in the “Power Extensions” bundle

Kết hợp nhiều queries thành một query duy nhất, chia sẻ trạng thái giữa chúng và thực thi theo thứ tự yêu cầu.

Mô tả

Thực thi nhiều queries kết hợp các queries thành một query duy nhất, đảm bảo chúng được thực thi theo đúng thứ tự yêu cầu. Các operation có thể chia sẻ trạng thái với nhau thông qua các biến động, chỉ được tính toán một lần nhưng có thể được đọc nhiều lần trong toàn bộ tài liệu.

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

Tính năng này mang lại nhiều lợi ích:

  • Cải thiện hiệu suất: Thay vì thực thi một query với server GraphQL, chờ phản hồi, rồi dùng kết quả đó để thực thi một query khác, chúng ta có thể kết hợp các queries lại thành một và thực thi trong một request duy nhất, tránh được độ trễ từ nhiều kết nối HTTP.
  • Cho phép quản lý các GraphQL queries thành các operation nguyên tử (hay đơn vị logic) phụ thuộc vào nhau, và có thể được thực thi có điều kiện dựa trên kết quả của một operation trước đó.

Thực thi nhiều queries khác với query batching, trong đó server GraphQL cũng thực thi nhiều queries trong một request, nhưng các queries đó chỉ đơn thuần được thực thi lần lượt, độc lập với nhau.

Các directive được kích hoạt

Khi Thực thi nhiều queries được kích hoạt, các directive sau đây sẽ có sẵn trong schema GraphQL:

  • @depends (operation directive): Để một operation (dù là query hay mutation) chỉ định các operation khác phải được thực thi trước
  • @export (field directive): Xuất giá trị của một field từ một query dưới dạng biến động, để đưa vào một field hoặc directive trong một query khác
  • @exportFrom (field directive): Tương tự @export nhưng để xuất giá trị của một biến động có phạm vi (được truyền qua @passOnwards(as: "...") hoặc @applyField(passOnwardsAs: "..."))
  • @deferredExport (field directive): Tương tự @export nhưng dùng với Multi-Field Directives

Ngoài ra, các directive @include@skip cũng được cung cấp dưới dạng operation directives (thông thường chúng chỉ là field directives), và có thể dùng để thực thi có điều kiện một operation nếu nó thỏa mãn một điều kiện nào đó.

@depends

Khi tài liệu GraphQL chứa nhiều operation, chúng ta chỉ định cho server biết operation nào cần thực thi qua tham số URL ?operationName=...; nếu không, operation cuối cùng sẽ được thực thi.

Bắt đầu từ operation ban đầu này, server sẽ thu thập tất cả các operation cần thực thi, được xác định bằng cách thêm directive depends(on: [...]), và thực thi chúng theo thứ tự tương ứng tôn trọng các phụ thuộc.

Đối số operations của directive nhận một mảng tên operation ([String]), hoặc cũng có thể cung cấp một tên operation đơn lẻ (String).

Trong query này, chúng ta truyền ?operationName=Four, và các operation được thực thi (dù là query hay mutation) sẽ là ["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 xuất giá trị của một field (hoặc tập hợp các fields) thành một biến động, để dùng làm đầu vào trong một field hoặc query từ query khác.

Ví dụ, trong query này chúng ta xuất tên của người dùng đã đăng nhập, và dùng giá trị này để tìm kiếm các bài viết chứa chuỗi đó (lưu ý rằng biến $loggedInUserName, vì là biến động, không cần được định nghĩa trong operation FindPosts):

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

@exportFrom

Tương tự @export, nhưng thay vì xuất giá trị field, nó xuất giá trị của một biến động có phạm vi, được truyền qua @passOnwards(as: "...") hoặc @applyField(passOnwardsAs: "...").

Ví dụ, trong query này chúng ta dùng @applyField để sửa đổi các phần tử trong mảng và gán giá trị mới này cho biến động có phạm vi $replaced. Sau đó, chúng ta dùng @exportFrom để làm cho giá trị đó có thể truy cập toàn cục qua biến động $replacedList, để có thể lấy từ một query tiếp theo.

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

Điều này sẽ tạo ra:

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

@deferredExport

Khi tính năng Multi-Field Directives được kích hoạt và chúng ta xuất giá trị của nhiều fields vào một dictionary, hãy dùng @deferredExport thay vì @export để đảm bảo rằng tất cả các directive từ mỗi field liên quan đã được thực thi trước khi xuất giá trị của field.

Ví dụ, trong query này, field đầu tiên có directive @strUpperCase được áp dụng, và field thứ hai có @strTitleCase. Khi thực thi @deferredExport, giá trị được xuất sẽ có các directive này được áp dụng:

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

Tạo ra:

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

@skip@include (trong các operation)

Khi Thực thi nhiều queries được kích hoạt, các directive @include@skip cũng có sẵn dưới dạng operation directives, và có thể dùng để thực thi có điều kiện một operation nếu nó thỏa mãn một điều kiện nào đó.

Ví dụ, trong query này, operation CheckIfPostExists xuất một biến động $postExists và, chỉ khi giá trị của nó là true, mutation ExecuteOnlyIfPostExists mới được thực thi:

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

Các đầu ra của biến động

@export có thể tạo ra 6 đầu ra khác nhau, dựa trên sự kết hợp của:

  • Giá trị của đối số type (hoặc SINGLE, LIST hoặc DICTIONARY)
  • Directive được áp dụng cho một field đơn lẻ, hay cho nhiều fields (qua module Multi-Field Directives)

6 đầu ra có thể có là:

  1. Kiểu SINGLE:
    1. Field đơn lẻ
    2. Multi-field
  2. Kiểu LIST:
    1. Field đơn lẻ
    2. Multi-field
  3. Kiểu DICTIONARY:
    1. Field đơn lẻ
    2. Multi-field

Kiểu SINGLE / Field đơn lẻ

Đầu ra là một giá trị đơn lẻ khi truyền tham số type: SINGLE (đây là giá trị mặc định).

Trong query này:

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

...biến động $postTitle sẽ có giá trị:

"Hello world!"

Lưu ý rằng nếu SINGLE được áp dụng trên một mảng thực thể, thì giá trị của thực thể cuối cùng là giá trị được xuất.

Trong query này:

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

...biến động $postTitle sẽ có giá trị cho bài viết có ID 5:

"Everything good?"

Kiểu SINGLE / Multi-field

Nếu @export được áp dụng trên nhiều fields (bằng cách thêm tham số affectAdditionalFieldsUnderPos do module Multi-Field Directives cung cấp), thì giá trị được đặt trên biến động là một dictionary { key: bí danh field, value: giá trị field } (kiểu JSONObject).

Query này:

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

...xuất biến động $postData với giá trị:

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

Kiểu LIST / Field đơn lẻ

Biến động sẽ chứa một mảng với giá trị field từ tất cả các thực thể được truy vấn (từ field bao quanh), bằng cách truyền tham số type: LIST.

Khi chạy query này (trong đó các thực thể được truy vấn là các bài viết có ID 15):

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

...biến động $postTitles sẽ có giá trị:

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

Kiểu LIST / Multi-field

Chúng ta nhận được một mảng các dictionary (kiểu JSONObject), mỗi cái chứa giá trị của các fields mà directive được áp dụng.

Query này:

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

...xuất biến động $postsData với giá trị:

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

Kiểu DICTIONARY / Field đơn lẻ

Biến động sẽ chứa một dictionary (kiểu JSONObject) với ID của thực thể được truy vấn làm key và giá trị field làm value, bằng cách truyền tham số type: DICTIONARY.

Query này:

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

...xuất biến động $postIDTitles với giá trị:

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

Kiểu DICTIONARY / Multi-field

Trong sự kết hợp này, chúng ta xuất một dictionary của các dictionary: { key: entity ID, value: { key: field alias, value: field value } } (sử dụng kiểu JSONObject sẽ chứa các mục kiểu JSONObject).

Query này:

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

...xuất biến động $postsIDProperties với giá trị:

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

Xuất giá trị khi duyệt qua mảng hoặc đối tượng JSON

@export tôn trọng tính số nhiều từ bất kỳ meta-directive bao quanh nào.

Cụ thể, bất cứ khi nào @export được lồng bên dưới một meta-directive lặp qua các mục mảng hoặc thuộc tính đối tượng JSON (tức là @underEachArrayItem@underEachJSONObjectProperty), thì giá trị được xuất sẽ là một mảng.

Query này:

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

...tạo ra $contentAttributes với giá trị:

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

Ngược lại, cùng một query truy cập một mục cụ thể trong mảng thay vì duyệt qua tất cả (bằng cách thay thế @underEachArrayItem bằng @underArrayItem(index: 0)) sẽ xuất một giá trị đơn lẻ.

Query này:

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

...tạo ra $contentAttributes với giá trị:

"List Block"

Thứ tự thực thi directive

Nếu có các directive khác trước @export, giá trị được xuất sẽ phản ánh những sửa đổi của các directive trước đó.

Ví dụ, trong query này, tùy thuộc vào @export diễn ra trước hay sau @strUpperCase, kết quả sẽ khác nhau:

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

Tạo ra:

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

Thực thi trong Persisted Queries

Khi một GraphQL query chứa nhiều operation trong một Persisted Query, chúng ta có thể gọi endpoint tương ứng bằng cách truyền tham số URL ?operationName=... với tên của operation cần thực thi; nếu không, operation cuối cùng sẽ được thực thi.

Ví dụ, để thực thi operation GetPostsContainingString trong một Persisted Query với endpoint /graphql-query/posts-with-user-name/, chúng ta phải gọi:

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

Ví dụ

Nhập nội dung từ một API endpoint bên ngoài:

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

Lấy dữ liệu cho một bài viết, biến đổi nó, và lưu trữ lại:

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

Cập nhật một bài viết nếu nó tồn tại, hoặc hiển thị thông báo lỗi nếu không:

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
}

Đăng nhập người dùng trước khi thực thi một mutation, và đăng xuất ngay sau đó:

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
}

Đăng nhập người dùng có điều kiện trước khi thực thi một mutation, nếu được cung cấp:

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
}

Đặc tả GraphQL

Chức năng này hiện không thuộc về đặc tả GraphQL, nhưng đã được yêu cầu: