Arsitektur
ArsitekturMemanipulasi urutan resolusi field

Memanipulasi urutan resolusi field

Tujuan dari direktif @export yang disediakan oleh Multiple Query Execution adalah untuk mengekspor nilai sebuah field (atau sekumpulan field) ke dalam sebuah variabel, yang kemudian digunakan di tempat lain dalam query.

Direktif ini tidak akan berfungsi jika pembacaan variabel terjadi sebelum nilai diekspor ke dalam variabel tersebut. Oleh karena itu, engine perlu menyediakan cara untuk mengontrol urutan eksekusi field.

Gato GraphQL menyediakan cara untuk memanipulasi urutan eksekusi field melalui query itu sendiri. Engine memuat data dalam iterasi untuk setiap tipe, pertama-tama menyelesaikan semua field dari tipe pertama yang ditemukan dalam query, kemudian menyelesaikan semua field dari tipe kedua yang ditemukan, dan seterusnya hingga tidak ada lagi tipe yang perlu diproses.

Misalnya, query berikut yang melibatkan objek bertipe Director, Film, dan Actor:

{
  directors {
    name
    films {
      title
      actors {
        name
      }
    }
  }
}

...diselesaikan oleh engine GraphQL dalam urutan ini:

Menangani tipe dalam iterasi

Jika setelah diproses, sebuah tipe direferensikan kembali dalam query untuk mengambil data yang belum dimuat (misalnya: dari objek tambahan, atau field tambahan dari objek yang sudah dimuat), maka tipe tersebut ditambahkan kembali di akhir daftar iterasi.

Misalnya, jika kita juga melakukan query pada field preferredDirector milik Actor (yang mengembalikan objek bertipe Director) seperti ini:

{
  directors {
    name
    films {
      title
      actors {
        name
        preferredDirector {
          name
        }
      }
    }
  }
}

...maka engine GraphQL memproses query dalam urutan ini:

Tipe yang berulang dalam iterasi

Mari kita lihat bagaimana ini berlaku untuk mengeksekusi @export dalam satu query. Pada percobaan pertama, kita membuat query seperti biasanya, tanpa memikirkan urutan eksekusi field:

query GetPostsAuthorNames {
  user(by: { id: 1 }) {
    name @export(as: "authorName")
  }
  posts(filter: { search: $authorName }) {
    id
    title
  }
}

Saat menjalankan query, respons yang dihasilkan adalah:

Mengeksekusi query yang menggunakan variabel

...yang mengandung error berikut:

{
  "errors": [
    {
      "message": "Expression 'authorName' is undefined",
    }
  ]
}

Error ini berarti bahwa pada saat variabel $authorName dibaca, nilainya belum diset; masih undefined.

Mari kita lihat mengapa ini terjadi. Pertama, kita analisis tipe apa saja yang muncul dalam query, ditambahkan sebagai komentar di bawah ini:

# Type: Root
query GetPostsAuthorNames {
  # Type: User
  user(by: {id: 1}) {
    # Type: String
    name @export(as: "authorName")
  }
  # Type: Post
  posts(filter: { search: $authorName }) {
    # Type: ID
    id
    # Type: String
    title
  }
}

Untuk memproses tipe-tipe dan memuat datanya, engine data-loading menambahkan tipe query Root ke dalam daftar FIFO (First-In, First-Out), sehingga [Root] menjadi daftar awal yang diteruskan ke algoritma, kemudian melakukan iterasi atas tipe-tipe secara berurutan, seperti ini:

#OperasiDaftar
0Siapkan daftar FIFO[Root]
1aAmbil tipe pertama dari daftar (Root)[]
1bProses semua field yang di-query dari tipe Root:
→ user(by: {id: 1})
→ posts(filter: { search: $authorName })
Tambahkan tipenya (User dan Post) ke daftar
[User, Post]
2aAmbil tipe pertama dari daftar (User)[Post]
2bProses field yang di-query dari tipe User:
→ name @export(as: "authorName")
Karena ini adalah tipe skalar (String), tidak perlu ditambahkan ke daftar
[Post]
3aAmbil tipe pertama dari daftar (Post)[]
3bProses semua field yang di-query dari tipe Post:
→ id
→ title
Karena ini adalah tipe skalar (ID dan String), tidak perlu ditambahkan ke daftar
[]
4Daftar kosong, iterasi selesai. 

Di sini kita dapat melihat masalahnya: @export dieksekusi pada langkah 2b, tetapi dibaca pada langkah 1b.

Di sinilah kita perlu mengontrol alur eksekusi field. Solusi yang diimplementasikan adalah menunda saat variabel yang diekspor dibaca, yang dicapai dengan melakukan query secara artifisial pada field self dari tipe Root.

Field self, sesuai namanya, mengembalikan objek yang sama; diterapkan pada objek Root, ia mengembalikan objek Root yang sama. Anda mungkin bertanya-tanya: "jika saya sudah memiliki objek root, mengapa saya perlu mengambilnya lagi?". Karena kemudian algoritma engine perlu menambahkan referensi baru ke Root ini di akhir daftar FIFO, dan kita dapat dengan sengaja mendistribusikan field-field yang di-query sebelum atau sesudah setiap iterasi tersebut.

Itulah mengapa field posts(filter:{ search: $authorName }) ditempatkan di dalam field self pada query di atas, dan menjalankan query menghasilkan respons yang diharapkan:

query GetPostsAuthorNames {
  user(by: {id: 1}) {
    name @export(as: "authorName")
  }
  self {
    posts(filter: { search: $authorName }) {
      id
      title
    }
  }
}

Menjalankan query pertama dengan @export

Mari kita eksplorasi urutan pemrosesan tipe untuk query ini, untuk memahami mengapa ini berjalan dengan baik:

#OperasiDaftar
0Siapkan daftar FIFO[Root]
1aAmbil tipe pertama dari daftar (Root)[]
1bProses semua field yang di-query dari tipe Root:
→ user(by: {id: 1})
→ self
Tambahkan tipenya (User dan Root) ke daftar
[User, Root]
2aAmbil tipe pertama dari daftar (User)[Root]
2bProses field yang di-query dari tipe User:
→ name @export(as: "authorName")
Karena ini adalah tipe skalar (String), tidak perlu ditambahkan ke daftar
[Root]
3aAmbil tipe pertama dari daftar (Root)[]
3bProses field yang di-query dari tipe Root:
→ posts(filter:{ search: $authorName })
Tambahkan tipenya (Post) ke daftar
[Post]
4aAmbil tipe pertama dari daftar (Post)[]
4bProses semua field yang di-query dari tipe Post:
→ id
→ title
Karena ini adalah tipe skalar (ID dan String), tidak perlu ditambahkan ke daftar
[]
5Daftar kosong, iterasi selesai. 

Sekarang kita dapat melihat bahwa masalah telah teratasi: @export dieksekusi pada langkah 2b, dan dibaca pada langkah 3b.

Multiple Query Execution melakukan hal ini persis saat memisahkan query: ia mengkonversi dokumen GraphQL dengan menambahkan field-field self, sehingga field-field dalam setiap operasi hanya dieksekusi setelah semua field dalam semua operasi sebelumnya telah diselesaikan.