แนวคิด ไอเดีย และกลยุทธ์
แนวคิด ไอเดีย และกลยุทธ์Cache control ผ่าน persisted queries

Cache control ผ่าน persisted queries

GraphQL โดยทั่วไปทำงานผ่าน POST โดยการรัน queries ทั้งหมดกับ endpoint เดียว และส่งพารามิเตอร์ผ่าน body ของ request URL ของ endpoint เดียวนั้นจะให้ผลลัพธ์ response ที่แตกต่างกัน ซึ่งหมายความว่าไม่สามารถแคชได้ (อย่างน้อย ในกรณีที่ใช้ URL เป็นตัวระบุ)

ดังนั้น วิธีมาตรฐานในการรองรับการแคชใน GraphQL คือที่ชั้น client ผ่าน Apollo client และไลบรารีที่คล้ายกัน ซึ่งแคช object ที่ส่งคืนแต่ละชิ้นอย่างอิสระจากกัน โดยระบุด้วย global ID ที่ไม่ซ้ำกัน

(ในทางตรงกันข้าม เมื่อแคชบนเซิร์ฟเวอร์ เราจะใช้ URL เป็นตัวระบุ และแคชข้อมูลของ entity ทั้งหมดใน response รวมกัน)

แต่วิธีแก้ปัญหานี้มีข้อเสียหลายประการ:

  • แอปพลิเคชันมี JavaScript ที่ต้องรันฝั่ง client เพิ่มขึ้น การเข้าถึงเว็บไซต์ผ่านโทรศัพท์มือถือระดับต่ำจะส่งผลกระทบต่อประสิทธิภาพ
  • แอปพลิเคชันมีความซับซ้อนมากขึ้นและมีชิ้นส่วนที่เคลื่อนไหวมากขึ้น เนื่องจากขณะนี้เราต้องกังวลเกี่ยวกับการ implement ชั้นการแคชด้วย
  • ไม่ใช่ทุกคนที่เข้าใจ JavaScript (เช่น เว็บไซต์อาจเขียนด้วย PHP) แต่ตอนนี้การจัดการกับ JS กลายเป็นความรับผิดชอบด้วย

วิธีแก้ปัญหาที่ดีกว่ามากคือการใช้ HTTP caching มาดูเงื่อนไขเบื้องต้นที่จำเป็นสำหรับการทำงานนี้

การเข้าถึง GraphQL ผ่าน GET

การใช้ HTTP caching หมายความว่าเราจะแคช GraphQL response โดยใช้ URL เป็นตัวระบุ ซึ่งมีนัยสำคัญ 2 ประการ:

  1. เราต้องเข้าถึง endpoint เดียวของ GraphQL ผ่าน GET
  2. เราต้องส่ง query และ variables เป็น URL params

ดังนั้น หาก endpoint เดียวคือ /graphql การดำเนินการ GET สามารถรันกับ URL /graphql?query=...&variables=... ได้

วิธีนี้ใช้ได้กับการดึงข้อมูลจากเซิร์ฟเวอร์ (ผ่านการดำเนินการ query) สำหรับการเปลี่ยนแปลงข้อมูล (ผ่านการดำเนินการ mutation) เราต้องใช้ POST อยู่ ไม่มีปัญหาใดที่นี่ เนื่องจาก mutation จะรันใหม่เสมอ เราไม่สามารถแคชผลลัพธ์ของ mutation ได้ ดังนั้นเราจะไม่ใช้ HTTP caching กับมันอยู่แล้ว

แนวทางนี้ใช้งานได้ (และยังแนะนำในเว็บไซต์ทางการด้วย) แต่มีข้อควรพิจารณาบางประการที่เราต้องใส่ใจ

การเขียน GraphQL queries ผ่าน URL param

GraphQL query โดยปกติจะครอบคลุมหลายบรรทัด ตัวอย่างเช่น:

{
  posts {
    id
    title
  }
}

อย่างไรก็ตาม เราไม่สามารถป้อน string หลายบรรทัดนี้โดยตรงใน URL param ได้

วิธีแก้คือการ encode มัน ตัวอย่างเช่น GraphiQL client จะ encode query ด้านบนแบบนี้:

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

ใช่แล้ว วิธีนี้ใช้งานได้ แต่ดูไม่ค่อยดีเลยใช่ไหม? ใครจะสามารถเข้าใจ query นั้นได้?

หนึ่งในคุณสมบัติของ GraphQL คือ queries ของมันเข้าใจง่ายมาก ด้วยการฝึกฝนเล็กน้อย เมื่อเราเห็น query เราก็เข้าใจมันในทันที แต่เมื่อถูก encode แล้ว สิ่งดีทั้งหมดนั้นก็หายไป และมีเพียงเครื่องจักรเท่านั้นที่สามารถอ่านมันได้ มนุษย์ถูกตัดออกจากสมการ

อีกวิธีหนึ่งคือการแทนที่บรรทัดใหม่ทั้งหมดใน query ด้วยช่องว่าง ซึ่งใช้งานได้เพราะบรรทัดใหม่ไม่เพิ่มความหมายเชิง semantic ให้กับ query จากนั้น query ด้านบนสามารถแสดงเป็น:

?query={ posts { id title } }

วิธีนี้ใช้ได้ดีสำหรับ queries ง่ายๆ แต่ถ้าคุณมี query ที่ยาวมาก เปิดและปิด { } หลายครั้ง และเพิ่ม field arguments และ directives แล้ว มันก็จะยิ่งเข้าใจยากขึ้นเรื่อยๆ

ตัวอย่างเช่น query นี้:

{
  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
    }
  }
}

จะกลายเป็น query บรรทัดเดียวนี้:

{ 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 } } } 

อีกครั้ง การรัน query จะได้ผล แต่เราจะไม่รู้ว่ากำลังรันอะไรอยู่

และถ้า query ยังมี fragments ด้วย ก็ลืมไปได้เลย ไม่มีทางที่เราจะเข้าใจมันได้

Persisted queries มาช่วยแก้ปัญหา

ถ้าการส่ง query ใน URL ไม่เป็นที่น่าพอใจ เรามีทางเลือกอื่นอะไรอีก? ก็คือ ไม่ส่ง query ใน URL เลย!

นี่คือแนวทางที่เรียกว่า "persisted query": เราจัดเก็บ query ไว้บนเซิร์ฟเวอร์ และใช้ตัวระบุ (เช่น numeric ID หรือ string ที่ไม่ซ้ำกันที่ได้จากการใช้ hashing algorithm กับ query เป็น input) เพื่อดึงมัน สุดท้าย เราส่งตัวระบุนี้เป็น URL parameter แทน query

ตัวอย่างเช่น query อาจถูกระบุด้วย ID 2908 (หรือ hash เช่น "50ac3e81") และจากนั้นเราดำเนินการ GET กับ URL /graphql?id=2908 GraphQL server จะดึง query ที่ตรงกับ ID นี้ รันมัน และส่งคืนผลลัพธ์

Gato GraphQL ทำให้ง่ายยิ่งขึ้น: persisted query ถูก implement เป็น custom post type ดังนั้นเราสามารถสร้างและเผยแพร่มันได้เหมือน post ปกติ และ slug ที่เราเลือก (ซึ่งโดยค่าเริ่มต้นจะอิงจาก title ที่เราป้อน) จะกลายเป็นตัวระบุ Persisted queries ทำให้การ implement HTTP caching ง่ายมาก

การคำนวณค่า max-age

HTTP Caching ทำงานโดยการส่ง header Cache-Control ใน response พร้อมค่า max-age ที่ระบุระยะเวลาที่ response ต้องถูกแคช หรือ no-store ที่ระบุว่าไม่ต้องแคช

GraphQL server จะคำนวณค่า max-age สำหรับ query อย่างไร เมื่อ fields ต่างๆ อาจมีค่า max-age ที่แตกต่างกัน?

คำตอบคือ: ดึงค่า max-age สำหรับ fields ทั้งหมดที่ร้องขอใน query และหาว่าค่าต่ำสุดคือเท่าไร นั่นจะเป็น max-age ของ response

ตัวอย่างเช่น สมมติว่าเรามี entity ของ type User ตามพฤติกรรมที่กำหนดให้กับ entity นี้ เราสามารถกำหนดว่า field ที่สอดคล้องกันสามารถแคชได้นานแค่ไหน:

🛠 ID ของมันจะไม่มีวันเปลี่ยน ⇒ เราให้ field id มีค่า max-age 1 ปี

🛠 URL ของมันจะถูกอัปเดตแบบสุ่มมากๆ (ถ้ามีเลย) ⇒ เราให้ field url มีค่า max-age 1 วัน

🛠 ชื่อของบุคคลอาจเปลี่ยนแปลงทุกครั้งคราว (เช่น เพื่อเพิ่มสถานะ หรือเพื่อบอกว่า "Milton (wears a mask)") ⇒ เราให้ field name มีค่า max-age 1 ชั่วโมง

🛠 karma ของผู้ใช้บนเว็บไซต์สามารถเปลี่ยนแปลงได้ตลอดเวลา (เช่น หลังจากที่มีคนกด upvote ความคิดเห็นของพวกเขา) ⇒ เราให้ field karma มีค่า max-age 1 นาที

🛠 หากกำลัง query ข้อมูลจากผู้ใช้ที่ล็อกอินอยู่ response จะไม่สามารถแคชได้เลย (ไม่ว่าจะดึง field ใดก็ตาม) ⇒ max-age ต้องเป็น no-store

ผลที่ได้คือ response ของ GraphQL queries ต่อไปนี้จะมีค่า max-age ดังนี้ (สำหรับตัวอย่างนี้ เราละเว้น max-age สำหรับ field Root.users แต่ในทางปฏิบัติจะถูกพิจารณาด้วย):

Queryค่า max-age
{
  users {
    id
  }
}
1 ปี
{
  users {
    id
    url
  }
}
1 วัน
{
  users {
    id
    url
    name
  }
}
1 ชั่วโมง
{
  users {
    id
    url
    name
    karma
  }
}
1 นาที
{
  me {
    id
    url
    name
    karma
  }
}
no-store (ไม่แคช)

การสร้าง Cache Control List

เมื่อเราระบุ max-age สำหรับแต่ละ field แล้ว เราป้อนข้อมูลนี้ผ่าน Cache Control List:

การกำหนด cache control policy

Gato GraphQL จะคำนวณค่า max-age ของ response โดยอัตโนมัติ และส่งกลับมาเป็น HTTP header Cache-Control