Konsep, Ide, Strategi
Konsep, Ide, StrategiMenjelaskan mutasi bersarang

Menjelaskan mutasi bersarang

Mutasi adalah operasi yang dapat mengubah data di server GraphQL, seperti saat membuat sebuah post, memperbarui nama pengguna, menambahkan komentar pada post, atau lainnya.

Dalam GraphQL, mutasi hanya diekspos di bawah tipe MutationRoot, seperti ini:

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

(Skema GraphQL dalam panduan ini digunakan untuk mengilustrasikan contoh-contoh; berbeda dari skema yang disediakan dalam plugin.)

Dengan skema ini, memodifikasi nama pengguna dilakukan seperti ini:

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

Mutasi diekspos di mutation root object type saja untuk memastikan bahwa mutasi dieksekusi secara serial, sebagaimana dijelaskan dalam spesifikasi 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.

Istilah "eksekusi serial" berlawanan dengan "eksekusi paralel", yang merupakan perilaku yang direkomendasikan untuk menyelesaikan field.

Misalnya, dalam query di bawah ini, tidak masalah field mana (apakah name atau email) yang pertama kali diselesaikan oleh server GraphQL, dan keduanya dapat diselesaikan secara paralel:

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

Namun mutasi mengubah data, sehingga urutan penyelesaian field memang penting, karenanya mutasi harus dieksekusi secara serial (atau, jika tidak, bisa menimbulkan race condition).

Misalnya, dua query di bawah ini akan menghasilkan hasil yang berbeda:

# Query 1: setelah eksekusi, nama pengguna akan menjadi "John"
mutation {
  updateUserName(userID: 37, newName: "Peter") {
    name
  }
  updateUserName(userID: 37, newName: "John") {
    name
  }
}
 
# Query 2: setelah eksekusi, nama pengguna akan menjadi "Peter"
mutation {
  updateUserName(userID: 37, newName: "John") {
    name
  }
  updateUserName(userID: 37, newName: "Peter") {
    name
  }
}

Konsekuensi dari mengekspos mutasi hanya melalui MutationRoot adalah tipe ini menjadi sangat membengkak, berisi field-field yang tidak memiliki kesamaan satu sama lain selain harus dieksekusi secara serial (yang merupakan masalah teknis, bukan keputusan desain antarmuka).

Alasan perlunya mutasi bersarang

Dari mutasi-mutasi di atas, hanya createPost yang benar-benar berada di bawah tipe MutationRoot, karena ia menciptakan elemen baru dari awal. Mutasi updateUserName dan addCommentToPost, bagaimanapun, dapat dengan sempurna memiliki operasi setara yang diterapkan pada entitas yang sudah ada dari tipe lain:

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

Dengan skema ini, memodifikasi nama pengguna dapat dilakukan seperti ini:

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

Fitur ini disebut "nested mutations": menerapkan mutasi pada hasil operasi lain, baik query maupun mutasi.

Perhatikan bagaimana menggunakan mutasi bersarang membuat skema GraphQL menjadi lebih elegan:

  • Sementara operasi MutationRoot.updateUserName harus menerima ID pengguna, operasi setaranya User.updateName tidak perlu, karena sudah dieksekusi pada entitas pengguna
  • Nama field dipersingkat dari updateUserName menjadi updateName

Selain itu, layanan GraphQL menjadi lebih sederhana dan mudah dipahami, karena kita dapat menavigasi di antara entitas dalam graph untuk memodifikasi datanya dengan cara yang sama seperti untuk melakukan query datanya.

Mutasi bersarang dapat turun ke beberapa level. Misalnya, kita dapat menambahkan komentar pada post yang baru dibuat, semuanya dalam satu query:

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

Dari sini, mutasi bersarang juga dapat meningkatkan performa dengan mengurangi latensi round-trip, dari mengeksekusi beberapa query untuk memutasi beberapa elemen, menjadi mengeksekusi satu query saja.

Mengapa mutasi bersarang bukan bagian dari spec

Spesifikasi GraphQL dimaksudkan untuk bekerja pada semua implementasi server GraphQL untuk bahasa apapun. Namun, kekuatan pendorongnya adalah JavaScript melalui graphql-js, implementasi referensi.

Dengan kata lain, fitur apapun yang tidak dapat didukung oleh graphql-js tidak akan menjadi bagian dari spesifikasi.

Karena JavaScript mendukung promises, resolusi paralel field menjadi layak, dan paralelisme menjadi salah satu prinsip fundamental saat pertama kali merancang graphql-js, sebagaimana terlihat dalam DataLoader (lapisan pengambilan data), yang fungsi batchingnya mengembalikan JavaScript promises.

Keuntungan eksekusi paralel untuk performa terlalu banyak, dan mutasi bersarang tidak dapat bekerja dengan paralelisme. Telah diputuskan bahwa tidak sepadan untuk menukar eksekusi paralel demi mutasi bersarang.

Mutasi bersarang dan performa

Untuk plugin Gato GraphQL, field selalu diselesaikan secara serial, dan urutan penyelesaiannya bersifat deterministik. (Karakteristik ini tidak mempengaruhi performa resolusi query, karena server terlebih dahulu mengubah graph dalam query menjadi model komponen, yang diselesaikan dalam waktu linear yang optimal).

Artinya plugin dapat mendukung mutasi bersarang, menyediakan semua manfaatnya, tanpa menanggung konsekuensinya.

Spesifikasi GraphQL

Fungsionalitas ini saat ini bukan bagian dari spesifikasi GraphQL, namun telah diminta dalam: