Khái niệm, ý tưởng, chiến lược
Khái niệm, ý tưởng, chiến lượcGiải thích nested mutations

Giải thích nested mutations

Mutations là các thao tác có thể thay đổi dữ liệu trên máy chủ GraphQL, chẳng hạn như khi tạo một bài viết, cập nhật tên người dùng, thêm bình luận vào bài viết, hoặc các thao tác khác.

Trong GraphQL, mutations chỉ được hiển thị dưới kiểu MutationRoot, như sau:

type MutationRoot {
  createPost(id: ID!, title: String!, content: String): Post!
  updateUserName(userID: ID!, newName: String!): User!
  addCommentToPost(postID: ID!, comment: String!, userID: ID): Comment!
}

(Schema GraphQL trong hướng dẫn này chỉ để minh họa các ví dụ; nó khác với schema được cung cấp trong plugin.)

Với schema này, việc thay đổi tên người dùng được thực hiện như sau:

mutation {
  updateUserName(userID: 37, newName: "Peter") {
    name
  }
}

Mutations chỉ được hiển thị trong kiểu đối tượng gốc của mutation để đảm bảo chúng được thực thi tuần tự, như đã giải thích trong đặc tả GraphQL:

It is expected that the top level fields in a mutation operation perform side‐effects on the underlying data system. Serial execution of the provided mutations ensures against race conditions during these side‐effects.

Thuật ngữ "thực thi tuần tự" trái ngược với "thực thi song song", vốn là hành vi được khuyến nghị khi phân giải các trường.

Ví dụ, trong query dưới đây, không quan trọng máy chủ GraphQL phân giải trường nào trước (dù là name hay email), và chúng có thể được phân giải song song:

query {
  user(by: { id: 37 }) {
    name
    email
  }
}

Tuy nhiên, mutations làm thay đổi dữ liệu, nên thứ tự phân giải các trường quan trọng, vì vậy chúng phải được thực thi tuần tự (nếu không, chúng có thể gây ra race conditions).

Ví dụ, hai queries dưới đây sẽ cho kết quả khác nhau:

# Query 1: sau khi thực thi, tên người dùng sẽ là "John"
mutation {
  updateUserName(userID: 37, newName: "Peter") {
    name
  }
  updateUserName(userID: 37, newName: "John") {
    name
  }
}
 
# Query 2: sau khi thực thi, tên người dùng sẽ là "Peter"
mutation {
  updateUserName(userID: 37, newName: "John") {
    name
  }
  updateUserName(userID: 37, newName: "Peter") {
    name
  }
}

Hậu quả của việc chỉ hiển thị mutations thông qua MutationRoot là kiểu này trở nên rất cồng kềnh, chứa các trường không có điểm chung ngoài việc phải được thực thi tuần tự (đây là vấn đề kỹ thuật, không phải quyết định thiết kế giao diện).

Lý do cần nested mutations

Trong số các mutations ở trên, chỉ có createPost thực sự thuộc kiểu MutationRoot, vì nó tạo ra một phần tử mới từ đầu. Tuy nhiên, mutations updateUserNameaddCommentToPost hoàn toàn có thể có các thao tác tương đương được áp dụng trên một thực thể hiện có từ kiểu khác:

type User {
  updateName(newName: String!): User!
}
 
type Post {
  addComment(comment: String!, userID: ID): Comment!
}

Với schema này, việc thay đổi tên người dùng có thể được thực hiện như sau:

mutation {
  user(ID: 37) {
    updateName(newName: "Peter") {
      name
    }
  }
}

Tính năng này được gọi là "nested mutations": áp dụng một mutation lên kết quả của một thao tác khác, dù là query hay mutation.

Hãy chú ý cách sử dụng nested mutations làm cho schema GraphQL trở nên thanh lịch hơn:

  • Trong khi thao tác MutationRoot.updateUserName phải nhận ID của người dùng, thao tác tương đương User.updateName thì không cần, vì nó đã được thực thi trên một thực thể người dùng
  • Tên trường được rút gọn từ updateUserName thành updateName

Ngoài ra, dịch vụ GraphQL trở nên đơn giản và dễ hiểu hơn, vì chúng ta có thể điều hướng qua các thực thể trong đồ thị để sửa đổi dữ liệu của chúng theo cách tương tự như khi truy vấn dữ liệu.

Nested mutations có thể đi xuống nhiều cấp. Ví dụ, chúng ta có thể thêm bình luận vào một bài viết mới tạo, tất cả trong một query duy nhất:

mutation {
  createPost(ID: 37, title: "Hello world!", content: "Just another post") {
    id
    addComment(comment: "Lovely post") {
      id
    }
  }
}

Từ đó, nested mutations cũng có thể cải thiện hiệu suất bằng cách giảm độ trễ vòng lặp, từ việc thực thi nhiều queries để thay đổi nhiều phần tử, sang thực thi chỉ một query duy nhất.

Tại sao nested mutations không nằm trong đặc tả

Đặc tả GraphQL được thiết kế để hoạt động với tất cả các triển khai máy chủ GraphQL cho mọi ngôn ngữ. Tuy nhiên, động lực chính của nó là JavaScript thông qua graphql-js, triển khai tham chiếu.

Nói cách khác, bất kỳ tính năng nào không thể được hỗ trợ bởi graphql-js sẽ không được đưa vào đặc tả.

Vì JavaScript hỗ trợ promises, việc phân giải song song các trường là khả thi, và tính song song đã trở thành một trong những nguyên tắc cơ bản khi thiết kế ban đầu graphql-js, như thể hiện qua DataLoader (lớp truy xuất dữ liệu), có các hàm batching trả về JavaScript promises.

Lợi thế của thực thi song song đối với hiệu suất là quá nhiều, và nested mutations không thể hoạt động với tính song song. Người ta đã quyết định rằng sẽ không đáng để đánh đổi thực thi song song lấy nested mutations.

Nested mutations và hiệu suất

Đối với plugin Gato GraphQL, các trường luôn được phân giải tuần tự, và thứ tự phân giải có tính xác định. (Đặc điểm này không ảnh hưởng đến hiệu suất phân giải query, vì máy chủ trước tiên chuyển đổi đồ thị trong query thành mô hình thành phần, được phân giải trong thời gian tuyến tính tối ưu).

Điều này có nghĩa là plugin có thể hỗ trợ nested mutations, tận dụng tất cả các lợi ích của nó mà không phải chịu bất kỳ hậu quả nào.

Đặc tả GraphQL

Chức năng này hiện không nằm trong đặc tả GraphQL, nhưng đã được yêu cầu trong: