Cấu hình schema
Cấu hình schemaThực thi nhiều queries đồng thời

Thực thi nhiều queries đồng thời

Nhiều queries có thể được kết hợp lại và thực thi như một thao tác duy nhất, tái sử dụng trạng thái và dữ liệu của chúng.

Điều này khác với query batching, trong đó máy chủ GraphQL cũng thực thi nhiều queries trong một yêu cầu duy nhất, nhưng các queries đó chỉ được thực thi lần lượt, độc lập với nhau.

Tính năng này cải thiện hiệu suất. Thay vì thực thi các queries một cách độc lập trong các yêu cầu khác nhau (để chúng ta trước tiên thực thi một thao tác với máy chủ GraphQL, sau đó chờ phản hồi của nó, rồi sử dụng kết quả đó để thực hiện một thao tác khác), chúng ta có thể thực thi chúng cùng nhau, từ đó tránh được độ trễ từ nhiều yêu cầu.

Multiple Query Execution cũng cho phép chúng ta tổ chức tốt hơn các queries GraphQL, chia chúng thành các đơn vị logic phụ thuộc lẫn nhau, và được thực thi có điều kiện dựa trên kết quả từ một thao tác trước đó.

Cách sử dụng multiple query execution

Giả sử chúng ta muốn tìm kiếm tất cả các bài viết đề cập đến tên của người dùng đang đăng nhập. Thông thường, chúng ta cần hai queries để thực hiện điều này:

Trước tiên chúng ta lấy name của người dùng:

query GetLoggedInUserName {
  me {
    name
  }
}

...và sau đó, sau khi đã thực thi query đầu tiên, chúng ta có thể truyền name của người dùng đã lấy được như biến $search để thực hiện tìm kiếm trong một query thứ hai:

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

Multiple Query Execution đơn giản hóa quy trình này, cho phép chúng ta lấy tất cả dữ liệu và thực thi tất cả logic cần thiết trong một yêu cầu duy nhất:

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

Multiple Query Execution đạt được thông qua việc sử dụng các directive đặc biệt sau:

  • @depends (operation directive): khiến một thao tác (dù là query hay mutation) chỉ ra những thao tác nào phải được thực thi trước
  • @export (field directive): xuất giá trị của một trường từ một thao tác, để đưa vào như đầu vào cho một trường trong thao tác khác
  • @deferredExport (field directive): Tương tự như @export nhưng dùng với Multi-Field Directives.

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

Máy chủ GraphQL sẽ tạo danh sách các thao tác cần tải và thực thi, lấy chúng từ mỗi @depends(on: ...), và sẽ xuất các giá trị từ bất kỳ trường nào chứa @export như một biến động (với tên được định nghĩa trong đối số as) để đưa vào bất kỳ thao tác tiếp theo nào.

Kết hợp các directive này, chúng ta có thể chia bất kỳ chức năng phức tạp nào thành các bước trung gian, xen kẽ các thao tác querymutation, thêm các phụ thuộc theo thứ tự cần thiết, và thực thi tất cả trong một yêu cầu duy nhất bằng cách định nghĩa thao tác ngoài cùng trong ?operationName=... (trong ví dụ trên, đó sẽ là ?operationName=GetPostsContainingString).

Định nghĩa các thao tác cần tải và thực thi thông qua @depends

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

Bắt đầu từ thao tác ban đầu này, máy chủ sẽ thu thập tất cả các thao tác cần thực thi, được định nghĩa 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 thao tác ([String]), hoặc chúng ta cũng có thể cung cấp một tên thao tác duy nhất (String).

Trong query này, chúng ta truyền ?operationName=Four, và các thao tác đượ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 ...
}

Chia sẻ dữ liệu giữa các queries thông qua @export

Directive @export xuất giá trị của một trường (hoặc tập hợp các trường) thành một biến động, để sử dụng như đầu vào trong một trường của query khác.

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

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

Đầ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 (có thể là SINGLE, LIST hoặc DICTIONARY)
  • Directive được áp dụng cho một trường đơn hay nhiều trường (thông qua module Multi-Field Directives)

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

  1. Kiểu SINGLE:
    1. Trường đơn
    2. Đa trường
  2. Kiểu LIST:
    1. Trường đơn
    2. Đa trường
  3. Kiểu DICTIONARY:
    1. Trường đơn
    2. Đa trường

Kiểu SINGLE / Trường đơn

Đầu ra là một giá trị duy nhất khi truyền tham số type: SINGLE (được đặt làm 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 các 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ị của bài viết có ID 5:

"Everything good?"

Kiểu SINGLE / Đa trường

Nếu @export được áp dụng trên nhiều trường (bằng cách thêm tham số affectAdditionalFieldsUnderPos được cung cấp bởi module Multi-Field Directives), thì giá trị được đặt vào biến động là một từ điển của { key: alias của trường, value: giá trị của trường } (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 / Trường đơn

Biến động sẽ chứa một mảng với giá trị của trường từ tất cả các thực thể được truy vấn (từ trường bao ngoài), 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 / Đa trường

Chúng ta nhận được một mảng các từ điển (kiểu JSONObject), mỗi từ điển chứa các giá trị của các trường 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 / Trường đơn

Biến động sẽ chứa một từ điển (kiểu JSONObject) với ID của thực thể được truy vấn làm khóa và các giá trị của trường làm giá trị, 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 / Đa trường

Trong sự kết hợp này, chúng ta xuất một từ điển của các từ điển: { key: ID của thực thể, value: { key: alias của trường, value: giá trị của trường } } (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."
  }
}

Thực thi có điều kiện các thao tác

Khi Multiple Query Execution được bật, các directive @include@skip cũng có thể dùng như operation directives, và chúng có thể được sử dụng để thực thi có điều kiện một thao tác nếu nó thỏa mãn một điều kiện nào đó.

Ví dụ, trong query này, thao tác 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...
}

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

@export tuân theo số lượng phần tử từ bất kỳ meta-directive bao ngoài 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 phần tử 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 phần tử cụ thể trong mảng thay vì lặp qua tất cả (bằng cách thay thế @underEachArrayItem bằng @underArrayItem(index: 0)) sẽ xuất một giá trị duy nhất.

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 các thay đổi từ những directive trước đó.

Ví dụ, trong query này, tùy thuộc vào việc @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"
  }
}

Multi-Field Directives

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

Ví dụ, trong query này, trường đầu tiên có directive @strUpperCase được áp dụng, và trường thứ hai có @titleCase. 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 @titleCase # 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"
    }
  }
}

Đặc tả GraphQL

Chức năng này hiện chưa là một phần của đặc tả GraphQL, nhưng đã được yêu cầu: