Konsep, Ide, Strategi
Konsep, Ide, StrategiCache control melalui persisted queries

Cache control melalui persisted queries

GraphQL biasanya beroperasi melalui POST, dengan mengeksekusi semua query terhadap satu endpoint tunggal dan meneruskan parameter melalui body permintaan. URL endpoint tunggal tersebut akan menghasilkan respons yang berbeda-beda, yang berarti tidak dapat di-cache (setidaknya tidak menggunakan URL sebagai pengenal).

Jadi cara standar untuk mendukung caching di GraphQL adalah di lapisan klien, melalui Apollo client dan library serupa, yang menyimpan cache objek-objek yang dikembalikan secara independen satu sama lain, mengidentifikasi mereka dengan ID global unik mereka.

(Sebaliknya, saat melakukan caching di server, kita biasanya menggunakan URL sebagai pengenal, dan kita menyimpan cache data untuk semua entitas dalam respons secara bersama-sama.)

Namun solusi ini memiliki beberapa kelemahan:

  • Aplikasi harus menjalankan lebih banyak JavaScript di sisi klien. Mengakses situs web melalui ponsel kelas bawah akan mengalami penurunan performa
  • Aplikasi menjadi lebih kompleks, dan dengan lebih banyak bagian yang bergerak, karena sekarang kita juga perlu memikirkan implementasi lapisan caching
  • Tidak semua orang memahami JavaScript (mis.: situs web mungkin dikodekan dalam PHP), namun sekarang berurusan dengan JS juga menjadi tanggung jawab tersendiri

Solusi yang jauh lebih baik adalah menggunakan HTTP caching. Mari kita lihat prasyarat yang diperlukan agar ini dapat berjalan.

Mengakses GraphQL melalui GET

Menggunakan HTTP caching berarti kita akan menyimpan cache respons GraphQL menggunakan URL sebagai pengenal. Ini memiliki 2 implikasi:

  1. Kita harus mengakses endpoint tunggal GraphQL melalui GET
  2. Kita harus meneruskan query dan variabel sebagai parameter URL

Kemudian, jika endpoint tunggalnya adalah /graphql, operasi GET dapat dieksekusi terhadap URL /graphql?query=...&variables=....

Ini berlaku untuk mengambil data dari server (melalui operasi query). Untuk memutasi data (melalui operasi mutation), kita masih harus menggunakan POST. Tidak ada masalah di sini, karena mutasi selalu dieksekusi secara segar; kita tidak bisa menyimpan cache hasil mutasi, sehingga kita tidak akan menggunakan HTTP caching untuk itu.

Pendekatan ini berhasil (dan bahkan disarankan di situs resmi), namun ada beberapa pertimbangan yang harus kita perhatikan.

Sebuah GraphQL query biasanya mencakup beberapa baris. Misalnya:

{
  posts {
    id
    title
  }
}

Namun, kita tidak bisa memasukkan string multi-baris ini langsung ke dalam parameter URL.

Solusinya adalah dengan meng-encode-nya. Misalnya, klien GraphiQL akan meng-encode query di atas seperti ini:

%7B%0A%20%20posts%20%7B%0A%20%20%20%20id%0A%20%20%20%20title%0A%20%20%7D%0A%7D

Baiklah, ini berhasil. Tapi tidak terlihat bagus, bukan? Siapa yang bisa memahami query itu?

Salah satu kelebihan GraphQL adalah query-nya sangat mudah dipahami. Dengan sedikit latihan, begitu kita melihat query-nya, kita langsung memahaminya. Namun begitu sudah di-encode, semua itu hilang, dan hanya mesin yang bisa memahaminya; manusia tersingkir dari persamaan.

Solusi lain bisa dengan mengganti semua baris baru dalam query dengan spasi, yang berhasil karena baris baru tidak menambahkan makna semantik pada query. Kemudian, query di atas dapat direpresentasikan sebagai:

?query={ posts { id title } }

Ini bekerja baik untuk query sederhana. Namun jika Anda memiliki query yang sangat panjang, membuka dan menutup banyak { }, serta menambahkan argumen field dan direktif, maka semakin sulit untuk dipahami.

Misalnya, query ini:

{
  posts(limit:5) {
    id
    title @titleCase
    excerpt @default(
      value:"No title",
      condition:IS_EMPTY
    )
    author {
      name
    }
    tags {
      id
      name
    }
    comments(
      limit:3,
      order:"date|DESC"
    ) {
      id
      date(format:"d/m/Y")
      author {
        name
      }
      content
    }
  }
}

Akan menjadi query satu baris ini:

{ posts(limit:5) { id title @titleCase excerpt @default(value:"No title", condition:IS_EMPTY) author { name } tags { id name } comments(limit:3, order:"date|DESC") { id date(format:"d/m/Y") author { name } content } } } 

Sekali lagi, mengeksekusi query-nya akan berhasil, namun kita tidak akan tahu apa yang sedang kita eksekusi.

Dan jika query juga mengandung fragment, maka lupakan saja, tidak ada cara kita bisa memahaminya.

Persisted queries hadir sebagai penyelamat

Jika meneruskan query dalam URL tidak memuaskan, opsi apa lagi yang kita miliki? Nah, tidak meneruskan query dalam URL!

Inilah pendekatan yang disebut "persisted query": Kita menyimpan query di server, dan menggunakan pengenal (seperti ID numerik, atau string unik yang dihasilkan dengan menerapkan algoritma hashing menggunakan query sebagai input) untuk mengambilnya. Akhirnya, kita meneruskan pengenal ini sebagai parameter URL, bukan query-nya.

Misalnya, query bisa diidentifikasi dengan ID 2908 (atau hash seperti "50ac3e81"), dan kemudian kita mengeksekusi operasi GET terhadap URL /graphql?id=2908. Server GraphQL kemudian akan mengambil query yang sesuai dengan ID ini, mengeksekusinya, dan mengembalikan hasilnya.

Gato GraphQL membuatnya bahkan lebih sederhana: sebuah persisted query diimplementasikan sebagai custom post type, sehingga kita bisa membuatnya dan mempublikasikannya seperti post biasa, dan slug yang kita pilih (yang secara default berdasarkan judul yang kita masukkan) akan menjadi pengenalnya. Persisted queries membuat implementasi HTTP caching menjadi sangat mudah.

Menghitung nilai max-age

HTTP Caching bekerja dengan mengirimkan header Cache-Control dalam respons, dengan nilai max-age yang menunjukkan berapa lama respons harus di-cache, atau no-store yang menunjukkan untuk tidak menyimpan cache-nya.

Bagaimana server GraphQL akan menghitung nilai max-age untuk query, mengingat bahwa field yang berbeda dapat memiliki nilai max-age yang berbeda pula?

Jawabannya adalah: Dapatkan nilai max-age untuk semua field yang diminta dalam query, dan cari tahu mana yang paling rendah. Itulah yang akan menjadi max-age respons.

Misalnya, katakanlah kita memiliki entitas bertipe User. Mengikuti perilaku yang ditetapkan untuk entitas ini, kita dapat menetapkan berapa lama field yang bersangkutan dapat di-cache:

🛠 ID-nya tidak akan pernah berubah ⇒ Kita memberi field id sebuah max-age 1 tahun

🛠 URL-nya akan diperbarui sangat jarang (jika pernah) ⇒ Kita memberi field url sebuah max-age 1 hari

🛠 Nama orang tersebut mungkin berubah sewaktu-waktu (mis.: untuk menambahkan status, atau untuk mengatakan "Milton (memakai masker)") ⇒ Kita memberi field name sebuah max-age 1 jam

🛠 Karma pengguna di situs bisa berubah setiap saat (mis.: setelah seseorang memberikan upvote pada komentar mereka) ⇒ Kita memberi field karma sebuah max-age 1 menit

🛠 Jika melakukan query data dari pengguna yang sedang login, maka respons tidak bisa di-cache sama sekali (terlepas dari field mana pun yang kita ambil) ⇒ max-age harus berupa no-store

Akibatnya, respons terhadap GraphQL queries berikut akan memiliki nilai max-age sebagai berikut (untuk contoh ini, kita mengabaikan max-age untuk field Root.users, namun dalam praktiknya ini juga akan diperhitungkan):

QueryNilai max-age
{
  users {
    id
  }
}
1 tahun
{
  users {
    id
    url
  }
}
1 hari
{
  users {
    id
    url
    name
  }
}
1 jam
{
  users {
    id
    url
    name
    karma
  }
}
1 menit
{
  me {
    id
    url
    name
    karma
  }
}
no-store (jangan di-cache)

Membuat Cache Control List

Setelah kita mengidentifikasi max-age untuk setiap field, kita memasukkan informasi ini melalui Cache Control List:

Mendefinisikan kebijakan cache control

Gato GraphQL kemudian akan secara otomatis menghitung nilai max-age respons, dan mengirimkannya kembali sebagai header HTTP Cache-Control.