อย่าสร้างฐานข้อมูลขึ้นมาใหม่ในฝั่งฟรอนต์เอนด์
(sqlsync.dev)- แอปฟรอนต์เอนด์ที่ซับซ้อนเริ่มจากแคชผลลัพธ์ API แล้วค่อย ๆ ต้องแบกรับทั้ง ดัชนีแบบทำเอง และการทำให้แคชหมดอายุ จนสภาพใกล้เคียงกับการสร้างฐานข้อมูลขนาดเล็กขึ้นมาใหม่ในทุกโปรเจกต์
- ในเฟรมเวิร์กเชิงประกาศอย่าง React เพื่อหลีกเลี่ยงการเรียก API ทุกครั้งที่เรนเดอร์ จึงมักวางแคชไว้ใน state ภายในหรือชั้น Redux และชั้นนี้ก็ค่อย ๆ ทำหน้าที่เป็น ที่เก็บข้อมูลส่วนกลาง
- โครงสร้างที่เก็บตาม ID และโครงสร้างค้นหาตามวันที่ช่วยให้ค้นหาได้เร็วขึ้น แต่การรักษา ความสอดคล้องกัน ระหว่างหลายโครงสร้างอย่าง
CACHEและENTRIES_BY_DATEกลายเป็นภาระในการทดสอบและการรีวิวโค้ด - การเปลี่ยนแปลงแบบมองโลกในแง่ดีช่วยเพิ่มความเร็วที่ผู้ใช้รู้สึกได้โดยอัปเดต UI ทันทีก่อนเซิร์ฟเวอร์ตอบกลับ แต่ก็ตามมาด้วย ต้นทุนด้านความถูกต้องสอดคล้อง เช่น การทำตรรกะซ้ำทั้งฝั่งไคลเอนต์และเซิร์ฟเวอร์ การติดตามการเปลี่ยนแปลงที่กำลังดำเนินอยู่ การ rollback เมื่อเกิดข้อผิดพลาด และการปรับให้ตรงกันหลังรีสตาร์ตแอป
- SQLSync พยายามนำเสนอ แคชที่ทนทาน, ดัชนี, constraint, การเปลี่ยนแปลงแบบมองโลกในแง่ดี และ reactive query ภายในสแตกฟรอนต์เอนด์ ด้วยฐานข้อมูลภายในเครื่องที่ใช้ SQLite และการซิงก์คล้าย Git Rebase
กระบวนการที่แคชฟรอนต์เอนด์เติบโตจนกลายเป็นฐานข้อมูล
- การจัดการข้อมูลฟรอนต์เอนด์อาจเริ่มจาก แคช ง่าย ๆ ที่เก็บผลลัพธ์ API ไว้ในตัวแปรภายในเครื่อง
- เฟรมเวิร์กเชิงประกาศอย่าง React จะเรนเดอร์ tree ซ้ำหลายครั้งระหว่างการโต้ตอบของผู้ใช้
- เพื่อไม่ให้ส่งคำขอ API ทุกครั้งที่เรนเดอร์ สามารถใช้
useStateและuseEffectเก็บผลลัพธ์คำขอหรือข้อผิดพลาดไว้ใน state ของคอมโพเนนต์ได้ - ตัวอย่างถูกทำให้ง่ายเพื่อความชัดเจน และในทางปฏิบัติก็มีทางเลือกในการใช้ไลบรารี API ที่ผ่านการพิสูจน์แล้ว
- แคชสามารถถูกย้ายไปยังชั้นที่สูงขึ้นของ UI tree หรือไปอยู่นอก UI ได้
- Redux เป็นไลบรารีจัดการ state ของ React ที่รวม state เข้าด้วยกันและประสานการเปลี่ยนแปลงแบบ atomic ตามเวลา
- ระบบนิเวศของ Redux ได้ขยายไปสู่เครื่องมือและแพตเทิร์นสำหรับจัดการการแคชข้อมูล API
- รูปแบบการใช้งานเช่นนี้มีเป้าหมายเพื่อรวมตรรกะแคชไว้ส่วนกลาง ประสานการอัปเดต และแชร์ผลลัพธ์แคชระหว่างคอมโพเนนต์
- ยิ่งชั้นแคชเติบโตขึ้น ก็ยิ่งใกล้เคียงกับ ระบบจัดเก็บข้อมูลส่วนกลาง ที่จัดการข้อมูลอย่างมีประสิทธิภาพให้เข้ากับเอนจินเรนเดอร์และการกระทำของผู้ใช้
ดัชนีแบบทำเองและภาระด้านความสอดคล้องกัน
- แม้ในฟรอนต์เอนด์ ก็สามารถเก็บข้อมูลที่ได้รับจากเซิร์ฟเวอร์ไว้ใน object ที่ใช้ ID เป็น key เพื่อค้นหาและแก้ไขได้รวดเร็ว
- ในแอปที่ใช้ REST API มักเกิดการอ่านข้อมูลเป็นชุด แล้วค่อยเสริม object ที่จำเป็นเพิ่มเติมอยู่บ่อยครั้ง
- หากเก็บ object แยกตาม ID จะทำให้ merge ผลลัพธ์ API เข้ากับแคชได้ง่าย
- โครงสร้างนี้เหมาะกับการสร้าง อ่าน แก้ไข และลบโดยอิง ID
- การกรองที่ต้องไล่ดูหลายรายการทำให้ต้องสร้าง ดัชนี แยกต่างหากเพื่อหลีกเลี่ยงการตรวจสอบ entry ทั้งหมด
- หากสร้างโครงสร้าง
ENTRIES_BY_DATEโดยอิงส่วนปี/เดือน/วันของcreatedAtก็จะค้นหา entry ของวันที่เฉพาะเจาะจงได้รวดเร็ว - แต่ต้องคอยรักษาความสอดคล้องกันระหว่าง
CACHEและENTRIES_BY_DATEอย่างต่อเนื่อง - การค้นหาช่วงวันที่ต้องเข้าถึงหลายครั้ง และ array ที่เรียงตามวันที่พร้อมตรรกะค้นหา/อัปเดตที่ซับซ้อนกว่าอาจเป็นโครงสร้างที่ดีกว่า
- หากสร้างโครงสร้าง
- เมื่อมีดัชนีมากขึ้น แต่ละดัชนีก็ต้องมีตรรกะสำหรับสร้าง อัปเดต และค้นหา
- การตรวจสอบความถูกต้องกลายเป็นภาระในการทดสอบและการรีวิวโค้ด
- หากลืมลบหรืออัปเดตในดัชนีใดดัชนีหนึ่ง อาจเกิดบั๊กที่หาได้ยาก
- เวลาอาจถูกใช้ไปกับโครงสร้างพื้นฐานเพื่อจัดการความซับซ้อนนี้ มากกว่าฟีเจอร์ใหม่ของแอปพลิเคชัน
- ดัชนีของฐานข้อมูลจริงซับซ้อนกว่าการเก็บข้อมูลในรูปแบบอื่นอย่างง่าย ๆ มาก
- ประกอบด้วยองค์ประกอบอย่างการเก็บสถิติ การจัดการเวอร์ชันข้อมูล การควบคุมทรานแซกชัน การล็อก และการทำงานร่วมกับ query optimization
ปัญหาความถูกต้องสอดคล้องที่การเปลี่ยนแปลงแบบมองโลกในแง่ดีเพิ่มเข้ามา
- การเปลี่ยนแปลงแบบมองโลกในแง่ดี คือการจำลองผลของงานบางอย่างภายในเครื่องก่อนที่เซิร์ฟเวอร์จะตอบกลับ
- UI สามารถตอบสนองได้ทันทีเหมือนไม่มีความหน่วงของเครือข่าย
- หากเซิร์ฟเวอร์ตัดสินใจต่างจากที่คาดหรือเกิดข้อผิดพลาด UI อาจต้อง rollback การเปลี่ยนแปลงและขอให้ผู้ใช้แก้ปัญหา
- สิ่งนี้จะเป็นเครื่องมือที่ทรงพลังเมื่อไคลเอนต์คาดเดาผลลัพธ์ของเซิร์ฟเวอร์ได้ดี สามารถจัดการข้อผิดพลาดที่ฝั่งไคลเอนต์ได้ และตรรกะถูกซิงก์กันอย่างใกล้ชิด
- โดยทั่วไป optimistic update มีลำดับสี่ขั้นตอน
- UI สร้างงานเขียนข้อมูลขึ้นมา
- ใช้สมมติฐานว่าเซิร์ฟเวอร์จะเห็นด้วย แล้วนำการเปลี่ยนแปลงไปใช้กับแคชภายในเครื่องและเรนเดอร์ UI ใหม่ทันที
- ส่งงานเปลี่ยนแปลงไปยังเซิร์ฟเวอร์แบบ asynchronous
- merge คำตอบจากเซิร์ฟเวอร์เข้ากับแคชภายในเครื่องเพื่อเขียนทับการเปลี่ยนแปลงแบบมองโลกในแง่ดีเดิม และเรนเดอร์ UI ใหม่หากจำเป็น
- การรักษาความสอดคล้องกับเซิร์ฟเวอร์ก่อให้เกิดภาระหลายอย่าง
- ต้องทำตรรกะซ้ำทั้งไคลเอนต์และเซิร์ฟเวอร์เพื่อคาดการณ์ผลลัพธ์
- ต้องติดตามการเปลี่ยนแปลงแต่ละรายการที่กำลังดำเนินอยู่ เพื่อจัดการข้อผิดพลาดแบบ asynchronous หรือความไม่ตรงกันกับเซิร์ฟเวอร์
- เพื่อประสบการณ์ผู้ใช้ที่ดีขึ้น อาจต้องทำให้ส่วนของแคชแบบมองโลกในแง่ดีมีความทนทาน เพื่อปรับการเปลี่ยนแปลงให้ตรงกันหลังรีสตาร์ตแอป
- กระบวนการนี้เพิ่มเวลาในการพัฒนาและต้นทุนการตรวจสอบความถูกต้อง จนอาจทำให้การจัดการข้อมูลมาก่อนการสร้างคุณค่าหรือฟีเจอร์ที่แตกต่างให้ผู้ใช้
ความซับซ้อนของการทำให้แคชหมดอายุแบบ recursive
- ในแอปที่มีข้อมูลจำนวนมาก ข้อมูลเดียวกันอาจปรากฏอยู่ในหลายตำแหน่งของแคช
- ตัวอย่างแคชเก็บ
projects,tasks,usersไว้ด้วยกัน - หลังทำงานเสร็จ หลายส่วนอาจได้รับผลกระทบ เช่น ความคืบหน้าของโปรเจกต์ งานที่มอบหมายให้ผู้ใช้ และข้อมูลงานใหม่
- ตัวอย่างแคชเก็บ
- เพื่อปรับแคชให้ตรงกับเซิร์ฟเวอร์หลังทำงานเสร็จ อาจต้อง round trip หลายครั้ง
- แจ้งเซิร์ฟเวอร์ว่างานเสร็จแล้ว
- รีเฟรชโปรเจกต์เพราะความคืบหน้าของโปรเจกต์เปลี่ยนไป
- ตรวจสอบว่ามีการมอบหมายงานใหม่หรือไม่
- หากมีการมอบหมายใหม่ ให้ดึงข้อมูลงานนั้น
- API ที่ซับซ้อนขึ้นสามารถลดจำนวน round trip ได้ แต่ยังเหลือผลลัพธ์ที่ API หรือตรรกะฝั่งไคลเอนต์ถูกผูกติดกับ โมเดลข้อมูลพื้นฐาน
- GraphQL เป็นหนึ่งในแนวทางสำหรับปัญหานี้ แต่ไม่ใช่ทางออกที่สมบูรณ์
- โครงสร้างที่ UI ต้องรู้ว่าแต่ละการเปลี่ยนแปลงเกี่ยวข้องกับส่วนใดของแคช จะเปราะบางมากขึ้นเมื่อขนาดใหญ่ขึ้น
- ความสัมพันธ์ของข้อมูลและการ aggregation อาจส่งผลต่อหลายส่วนของแคชภายในเครื่อง
- เมื่อทีมวิศวกรรมใหญ่ขึ้น ปัญหาอาจข้ามขอบเขตทีม และให้ความรู้สึกคล้ายตัวแปร global ที่เปลี่ยนแปลงได้ในโปรเจกต์ซอฟต์แวร์ขนาดใหญ่
- เมื่อรวมกับการเปลี่ยนแปลงแบบมองโลกในแง่ดี ไคลเอนต์จะยิ่งต้องทำซ้ำตรรกะแบ็กเอนด์มากขึ้นเพื่อคาดการณ์การเปลี่ยนแปลงของเซิร์ฟเวอร์
- ในตัวอย่าง อาจพยายามลบ task 1 ออกจาก user 1 และคำนวณความคืบหน้าเป็นสัดส่วนใหม่โดยใช้จำนวนงานทั้งหมด
- ยิ่งทำให้ไคลเอนต์คาดการณ์การเปลี่ยนแปลงซ้อนกันภายในเครื่องมากเท่าไร ไคลเอนต์ก็ยิ่งทำซ้ำสแตกแบ็กเอนด์มากขึ้นเท่านั้น
สแตกฐานข้อมูลฟรอนต์เอนด์ที่ SQLSync เสนอ
- SQLSync เป็นสแตกฐานข้อมูลที่ปรับให้เหมาะกับฟรอนต์เอนด์ สร้างบน SQLite
- ตัวอย่าง Todo app ใช้ Rust 60 บรรทัด และ SQL query ไม่กี่รายการที่กระจายอยู่ในคอมโพเนนต์ เพื่อสร้าง data layer ทั้งหมด
- SQLSync ให้แคชที่ทนทาน, ดัชนี/constraint/trigger/query optimization ของ SQLite, การเปลี่ยนแปลงแบบมองโลกในแง่ดี, การทำให้แคชหมดอายุอย่างชาญฉลาด และ reactive query
- ข้อมูลภายในเครื่องถูกเก็บไว้ในฐานข้อมูล SQLite หนึ่งฐานหรือมากกว่า
- สร้างดัชนีได้ง่าย และซิงก์กับข้อมูลโดยอัตโนมัติ
- ฐานข้อมูลสามารถใช้ดัชนีอัตโนมัติเพื่อเร่ง query ได้เหมือนแบ็กเอนด์
- SQL สามารถอธิบายการค้นหาข้อมูลที่ซับซ้อนได้ และยังใช้ฟีเจอร์อย่าง triggers, foreign keys, constraints, full-text search ได้ด้วย
- การเปลี่ยนแปลงแบบมองโลกในแง่ดีจัดการด้วย reducer
- โครงสร้างคล้ายกับ แนวคิดหลักของ Redux
- reducer สามารถเขียนด้วยภาษาใดก็ได้ที่คอมไพล์เป็น WebAssembly ได้
- SQLSync รันการเปลี่ยนแปลงบนไคลเอนต์แบบมองโลกในแง่ดี และรันบนเซิร์ฟเวอร์ตามลำดับที่สอดคล้องกันทั่วทั้งระบบ
- จากนั้นไคลเอนต์จะซิงก์กับเซิร์ฟเวอร์ด้วยกระบวนการที่คล้าย Git Rebase
- สถาปัตยกรรมนี้มีข้อดีคือทำให้ไม่จำเป็นต้องมี การทำให้แคชหมดอายุแบบ recursive
- เขียนตรรกะการเปลี่ยนแปลงข้อมูลทั้งหมดไว้ใน reducer ที่แชร์ระหว่างไคลเอนต์กับเซิร์ฟเวอร์ได้ง่าย
- การเปลี่ยนแปลงข้อมูลทั้งหมดที่เกิดขึ้นระหว่างการเปลี่ยนแปลงจะมองเห็นได้โดยอัตโนมัติ
- เนื่องจากการซิงก์ทำงานเหมือน Git Rebase แม้เซิร์ฟเวอร์จะทำการเปลี่ยนแปลงต่างจากไคลเอนต์ ไคลเอนต์ก็มีหลักประกันว่าจะไปถึงผลลัพธ์ที่สอดคล้องกันแบบเดียวกัน
งานที่เกี่ยวข้อง
- “Building data-centric apps with a reactive relational database” ของ Riffle กล่าวถึงแนวคิดการเก็บ state ทั้งหมดของแอปพลิเคชัน รวมถึง UI state ไว้ในฐานข้อมูลแบบ reactive เพียงแห่งเดียว
- Reactive query ให้โมเดลการคิดที่สะอาดและเข้ากับระบบเชิงประกาศอย่าง React ได้ดี
- แก้ปัญหาการพัฒนาแอปไคลเอนต์ด้วยแนวคิดจากชุมชนฐานข้อมูล
- กล่าวถึงประโยชน์ของการโมเดล state ด้วยโมเดลข้อมูลเชิงสัมพันธ์และดัชนีจริง
- Stepan จาก Instant.db เขียนบทความสองบทความเกี่ยวกับฐานข้อมูลในเบราว์เซอร์
- Database in the Browser, a Spec
- A Graph-Based Firebase
- ทั้งสองบทความกล่าวถึงปัญหาคล้ายกันโดยเน้นความสัมพันธ์ระหว่างสแตกฟรอนต์เอนด์กับแบ็กเอนด์มากกว่า และอธิบายแรงจูงใจที่ทำให้ Instant.db ถูกสร้างขึ้นในฐานะผู้สืบทอดแบบกราฟของ Firebase
- CR-SQLite ของ Matt Wonlaw เป็นส่วนขยายของ SQLite
- ใช้ conflict-free replicated data type (CRDT) และ event log แบบ causal order เพื่อ merge ข้อมูลอย่างสอดคล้องกัน
- ทำให้แอปแบบ peer-to-peer สามารถเก็บข้อมูลใน SQLite และทำงานร่วมกันได้โดยไม่ต้องมีตัวประสานงานส่วนกลาง
- ยังเป็นตัวอย่างของการรัน SQLite ในเบราว์เซอร์ด้วย
- Matt Wonlaw กำลังสำรวจแนวคิดที่เกี่ยวข้องด้วย
- incremental computation
- การปรับปรุงความง่ายในการใช้งาน SQL ผ่าน typed-sql
1 ความคิดเห็น
ความคิดเห็นบน Hacker News
ผมรู้จักโปรเจกต์นี้ดี และคนทำก็เป็นเพื่อนผม เลยจะให้เขามาตอบคำถามที่นี่
เขาเป็น สถาปนิกฐานข้อมูล ที่มีประสบการณ์ SQLsync ถูกสร้างขึ้นเพื่อให้นักพัฒนาฟรอนต์เอนด์สามารถ query และอัปเดตฐานข้อมูลระยะไกลได้ราวกับว่ามันอยู่ในเบราว์เซอร์ทั้งหมด ซึ่งในทางปฏิบัติก็เกือบเป็นแบบนั้นจริง ๆ และด้วย WASM เราสามารถส่งฐานข้อมูล SQLite ทั้งก้อนเข้าไปในเบราว์เซอร์ได้ ประเด็นสำคัญอยู่ที่ อัลกอริทึมแบบ reactive ที่ฉลาดแต่เรียบง่ายสำหรับซิงก์ระหว่างหลายไคลเอนต์
ถ้ามองว่าส่วนใหญ่ของงานพัฒนาคือการซิงก์ข้อมูล React กับ REST API ก็ถือเป็นกระบวนการซิงก์ชนิดหนึ่งได้ และแนวทางนี้เปิดความเป็นไปได้ใหม่ ๆ ไม่จำเป็นต้องสร้างฐานข้อมูลแบบปรับแต่งเองแปลก ๆ จาก tree ของ object ที่ดึงจาก API แล้ว cache ไว้อีกต่อไป แค่ใช้พลังของฐานข้อมูลเชิงสัมพันธ์เพื่ออัปเดตและ query ในเครื่องได้โดยตรง
อย่างไรก็ตาม ในบริษัทเว็บแบบดั้งเดิม การนำมาใช้ทำได้ยากเพราะมีทีมแบ็กเอนด์/ฟรอนต์เอนด์ที่แยกเชี่ยวชาญกันอยู่ เท่ากับเป็นการเอาชั้นฐานข้อมูล แบ็กเอนด์ การส่งข้อมูล และการยืนยันตัวตนออก แล้วแทนด้วยระบบแบบบล็อกเดียว และสถาปนิกระบบส่วนใหญ่ก็มาจากฝั่งแบ็กเอนด์ จึงไม่ค่อยเข้าใจปัญหานี้ มันแตะทั้งสองฝั่งอย่างลึก จึงไม่ค่อยเข้ากับระบบเดิม และสุดท้ายเหมาะกับงานพัฒนาใหม่มากกว่า แบ็กเอนด์ก็ไม่ใช่บริการ AWS หรือ Azure และไม่เป็นมิตรกับ Lambda ด้วย ดังนั้นสถาปนิกประเภทที่ผมเจอส่วนใหญ่คงไม่อยากยุ่ง
วิธีนี้เคยมีอยู่บ้างแล้วในเทคโนโลยีเก่าอย่าง CouchDB+PouchDB มันเหมาะกับบางกรณีใช้งานค่อนข้างดี แต่ระบบ query ยังไม่เป็นอุดมคติ และวิธีการยืนยันตัวตนกับการกำหนดขอบเขตข้อมูลก็แปลกสำหรับคนส่วนใหญ่ กรณีที่ง่ายที่สุดคือเมื่อข้อมูลเป็นของผู้ใช้รายเดียวอย่างสมบูรณ์ และใช้โมเดลฐานข้อมูลแยกตามผู้ใช้ได้ตรง ๆ หากแบ่งข้อมูลอย่างเข้มงวดด้วย CRDT ก็จะลดปัญหา conflict ได้มาก
แต่มีปัญหาเรื่องการขยายระบบ CouchDB ต้องการ CPU สูงมากเมื่อมีผู้ใช้เชื่อมต่อ 10,000–100,000 คน และเทคโนโลยีก็เก่าแล้ว แม้ยังมีการดูแลรักษาอยู่ก็ตาม ในแง่การออกแบบระบบ เมื่อเริ่มแชร์ข้อมูลระหว่างผู้ใช้ ความซับซ้อนจะเพิ่มขึ้นอย่างรวดเร็ว จนกลายเป็นแค่ย้ายตำแหน่งของความซับซ้อนแทนที่จะแก้มัน ทำให้ความเหมาะสมลดลง
แนวทางนี้ก็ดูเหมือนมุ่งไปที่เป้าหมายเดียวกัน แต่มีแนวโน้มสูงว่าจะเจอปัญหาการขยายระบบคล้าย ๆ กัน รอดูว่าจะพัฒนาไปอย่างไร และดูเหมือนเป็นก้าวแรก
จำได้ว่าเมื่อก่อน Chrome เคยพยายามใส่ฐานข้อมูล SQL เข้าไปในเบราว์เซอร์แบบตรงตัว แต่ไปได้ไม่ดีนัก แล้ว localStorage ก็กลายเป็นกระแสหลัก ไม่ได้จะลดทอนประโยชน์ของสิ่งนี้นะ แค่โดยปกติผมมักเลือกสิ่งที่เบราว์เซอร์มีให้ ส่วน WASM และความเป็นไปได้ที่จะนำมันเข้ามาในเบราว์เซอร์เมื่อมันสุกงอมหรือมีฟีเจอร์มากขึ้นนั้น ผมคาดหวังไว้มาก
บริษัทเก่าที่ผมเคยทำงานใช้ซอฟต์แวร์จัดการโปรเจกต์ที่มีกลไก checkout/checkin สำหรับงานแก้ไข ถ้า checkout โปรเจกต์ ก็จะดาวน์โหลดสำเนาสำหรับแก้ในเครื่อง และเมื่อ checkin ก็อัปโหลดกลับไปที่เซิร์ฟเวอร์ ระหว่าง checkout โปรเจกต์จะถูกล็อก ในยุคของแอป live update ทุกคนรู้สึกว่าวิธีนี้ล้าสมัย
แต่หลังจากสร้างเว็บแอป SPA มา 10 ปี วิธีซิงก์ข้อมูลแบบนั้นกลับรู้สึกเหมือนมาก่อนกาล
สุดท้ายก็คือสามารถนำขั้นตอนที่แก้ความไม่สอดคล้องกันระหว่างการอัปเดตหลายรายการพร้อมกันได้อย่างเป็นระบบมาใช้ได้หรือไม่ บางกรณีทำได้ บางกรณีทำไม่ได้ และขึ้นอยู่กับกฎธุรกิจมากกว่าความสามารถทางเทคนิค
หากตามกฎธุรกิจแล้วไม่สามารถสร้างกลไกการแก้ปัญหาได้ แม้จะมีความสามารถทางเทคนิคในการรองรับการอัปเดตพร้อมกัน ก็จำเป็นต้องมี การล็อก เพื่อให้มีการอัปเดตได้ทีละรายการ
แต่ยากที่จะโน้มน้าวว่าจริง ๆ แล้วนี่คือสิ่งที่ต้องการ คนมักหลงอยู่ในภาพลวงตาใหญ่โตว่าทุกอย่างต้องพร้อมใช้งานตลอดเวลา แต่ในความเป็นจริง โดยทั่วไปคนคนหนึ่งเป็นผู้เปลี่ยนแปลงทีละครั้ง และถ้าต้องมีสองคนขึ้นไปทำงานร่วมกัน สุดท้ายก็ต้องคุยหรือสื่อสารกันเพื่อปรับให้ตรงกันอยู่ดี
แม้ในการพัฒนาแบบกระจายศูนย์เต็มรูปแบบอย่าง Git ก็ยังไม่สามารถแก้ conflict ได้เองอย่างวิเศษอัตโนมัติ หากจะเลือกการเปลี่ยนแปลงที่ถูกต้อง ก็ยังต้องสื่อสารกับคนอื่นและเข้าใจบริบทอยู่ดี
บางอย่างต้องการทางออกที่ผ่านการพิสูจน์แล้ว
จำได้ว่าเมื่อก่อนตอนบริษัทเปลี่ยนจาก RCS ไปเป็น CVS เพื่อนร่วมงานคนหนึ่งหงุดหงิดที่ CVS ไม่รองรับ checkout แบบล็อก
https://en.wikipedia.org/wiki/Concurrent_Versions_System
ผมคิดว่ายุทธศาสตร์ล็อกแบบเจ้าของคนเดียวก็จำลองด้วย SQLSync ได้ เพียงแต่อาจไม่จำเป็น ขึ้นอยู่กับแอป หากเป้าหมายคือ ทำงานออฟไลน์ แล้ว merge เมื่อพร้อม SQLSync ก็ให้แพตเทิร์นนี้มาในตัว หากเป้าหมายคือให้มีไคลเอนต์เดียวเท่านั้นที่แก้ไขได้ ก็ต้องใช้แพตเทิร์นการล็อกจากศูนย์กลาง และสิ่งนี้ก็น่าจะสามารถประสานงานผ่าน SQLSync ได้
เรื่องนี้เกี่ยวพันกับหลักการ “สิ่งที่ถูกวัดย่อมถูกบริหารจัดการ” และ ความผิดพลาดจากต้นทุนจม
ปัญหาที่แท้จริงของฐานข้อมูลคือความซับซ้อน ฟีเจอร์แต่ละอย่างโดยทั่วไปค่อนข้างปลอดภัย แต่เมื่อความเสถียร แคช และดัชนีเริ่มเชื่อมโยงกัน ความซับซ้อนจะพุ่งขึ้นอย่างมาก และโดยปกติแล้วการทำ DB เฉพาะโดเมนขึ้นมาเองก็ไม่สมเหตุสมผล
แต่พอถึงจุดที่บริษัทตระหนักว่าได้ลงทุนและทุ่มทรัพยากรจำนวนมากไปกับการทำสามฟีเจอร์นั้นแล้ว ในทางการเมืองก็ยากที่จะเสนอให้รื้อทิ้ง และต้นทุนจริงในการกำจัดหนี้ทางเทคนิคในครั้งเดียวก็สูงด้วย
ผมมองว่าปัญหาที่แท้จริงคือไวยากรณ์ของ SQL ถ้าประสบการณ์การใช้ฐานข้อมูลเชิงสัมพันธ์พื้นฐานนั้นสะดวกเหมือนใช้ไวยากรณ์สไตล์ C ที่คุ้นเคย แทนที่จะเป็นภาษาอังกฤษที่บิดเบี้ยว แรงจูงใจที่จะใช้ DB แทนการทำเองก็คงสูงกว่านี้ ฐานข้อมูล NoSQL เป็นก้าวที่ดีไปในทิศทางนั้น แต่โดยรวมแล้วมักโฟกัสกับ Big Data มากเกินไป มากกว่าประโยชน์ใช้สอยในชีวิตประจำวัน ส่วนอย่าง Redis นั้นยืนระยะได้และถือว่าดี
การทำให้รัน SQL ได้ง่ายขึ้นเป็นแนวทางที่สมเหตุสมผล แต่ฐานข้อมูลที่ดี ๆ เช่น Postgres ซึ่งผมชอบ ใช้ SQL เป็นภาษาหลัก จึงยากที่จะได้ประสิทธิภาพโดยไม่ใช้ภาษานั้น เราต้องการฐานข้อมูลแบบ PostgresPostSQL จริง ๆ ที่โคลน Postgres ได้สมบูรณ์ แต่ parser หลักรองรับภาษาที่มีไวยากรณ์ดีกว่า
ในการเขียนโปรแกรมทั่วไปมีภาษาหลายสิบภาษาใช้งานอยู่และวิวัฒนาการต่อเนื่อง แม้แต่ JavaScript ซึ่งเปลี่ยนได้ยากเพราะเบราว์เซอร์เป็นตัวรันและเราไม่สามารถควบคุมเบราว์เซอร์ของผู้ใช้ได้ ก็ยังวิวัฒนาการผ่าน transpiler และ WebAssembly
แต่ในโลกฐานข้อมูล โดยแท้จริงแล้วแทบมีแค่ SQL ภาษาเดียว มีทางเลือกอื่นอยู่ แต่ในแง่การใช้งานไม่มีอะไรเข้าใกล้ SQL ได้เลย บางที SQL อาจไม่ได้แย่ขนาดนั้นก็ได้
เหตุผลอาจเป็นเพราะ relational model นั้นดีจริง ๆ ความพยายามที่จะออกนอกแนวทางนี้มีแนวโน้มจะใช้ได้แค่ใน niche เท่านั้น สไตล์ declarative ก็ดีมากเช่นกัน และถ้าออกนอกแนวทางนี้ก็ยากที่จะประสบความสำเร็จใหญ่ ๆ สุดท้ายถ้าแค่สร้าง SQL ที่ไวยากรณ์ต่างออกไป สำหรับคนส่วนใหญ่ก็คงไม่ใช่การปรับปรุงที่ใหญ่พอให้เปลี่ยนวิธีทำงาน
แอปพลิเคชันที่เขียนโดยเล็งไปที่ API นี้จะสามารถ implement SQL DB ได้ แค่ parse SQL แล้ว implement query planner ที่ปล่อย query plan ที่เข้ากับ API นี้ออกมา
ผมเป็นผู้เขียนเอง ผมเพิ่งไล่อ่านคำถามส่วนใหญ่แบบคร่าว ๆ และจะคอยกลับมาตรวจเป็นระยะว่ามีคำถามไหนตกหล่นไหม ผมก็สงสัยเหมือนกันว่าใครเคยทำวิธีติดตามการถกเถียงบน HN ให้ดีกว่านี้หรือเปล่า
ดีใจมากกับการพูดคุยจนถึงตอนนี้ บทความแรกไม่ได้โฟกัสว่า SQLSync ทำงานอย่างเป็นรูปธรรมอย่างไร แต่โฟกัสที่แรงจูงใจด้าน วิศวกรรมฟรอนต์เอนด์ ที่ทำให้ผมสร้าง SQLSync ขึ้นมา บทความถัดไปจะพูดถึงวิธีทำงาน
ไม่ควรทำให้ผู้ใช้มี mental model ที่โลกจริงสามารถทำให้พังได้อย่างรุนแรง หรือพังแบบมองไม่เห็น
ผมกังวลว่าวิธีซิงก์ฐานข้อมูลแทนโมเดล client-server อาจเป็นหนึ่งในกรณีนั้น กลไกการซิงก์อาจล่มไม่เป็นท่า หรืออาจมีสมมติฐานลึก ๆ บางอย่างที่ไม่เป็นจริง
ถ้าต้องการ UI ที่เร็ว ผมรู้สึกว่าการสร้างชุด primitive ของ CRDT มาใช้จะปลอดภัยกว่า และส่วนที่เหลือน่าจะคงไว้แค่การส่งฟอร์ม
การซิงก์สถานะระหว่าง client กับ server เป็นปัญหาต้องคำสาป
ถ้ายอมเสีย user experience ไปเล็กน้อย แล้วกลับไปใช้แนวทางที่ใกล้กับโมเดล PHP/server-side rendering ก็สามารถหลีกเลี่ยงปัญหานี้ทั้งก้อนได้ SPA นั้นดี แต่การส่ง multipart form ก็ยังใช้งานได้อยู่ ด้วย JavaScript เพียงเล็กน้อยก็ขัดเกลาส่วนหยาบ ๆ ที่เหลือได้เกือบหมด
ในผลิตภัณฑ์เว็บช่วงหลัง ๆ สถานะฝั่ง client มีเพียง authentication claim จาก IdP บุคคลที่สาม, first-party session ID ใน query parameter และเอกสารปัจจุบันเท่านั้น พูดตรง ๆ ผมเองก็ไม่รู้ว่าอย่างแรกถูกเก็บไว้ที่ไหน นั่นเป็นปัญหาของ Microsoft ไม่ใช่ปัญหาของเรา ส่วนสถานะที่เหลือทั้งหมดอยู่บน server
เราปฏิบัติกับ client เหมือน dumb terminal ที่ทั้งวันมีหน้าที่แค่พิมพ์ input ออกมา ไม่ใช้ first-party cookie หรือ local storage ด้วยซ้ำ วิธีนี้ทำให้ประสบการณ์การพัฒนาสำหรับ iOS/Safari ดีขึ้นมาก
ดังนั้นผมอยากถามว่า ประสบการณ์ที่ต้องการมอบให้จริง ๆ คืออะไร และทำไมมันถึงสมเหตุสมผลพอที่จะแยกสถานะ client กับ server ออกจากกัน
อ้างอิง: https://news.ycombinator.com/item?id=37584049
ดูเหมือนว่าแนวทางที่ใช้ SQLite เป็นฐานแบบ ออฟไลน์/ให้ความสำคัญกับโลคัลก่อน กำลังมาแรงมากช่วงนี้ นี่เป็นบทความที่สามที่อ่านในสัปดาห์นี้ และดูดีทีเดียว
แต่เมื่อเทียบกับ ElectricSQLhttps://electric-sql.com/ และ PowerSynchttps://powersync.com/ แล้วเป็นอย่างไร?
ทั้ง ElectricSQL และ PowerSync ต่างจัดการกับปัญหาที่ยากมากอย่าง การจำลองข้อมูลบางส่วน แนวคิดคือพยายามสร้างวิธีแก้ปัญหาทั่วไปที่ฐานข้อมูลส่วนกลางแบบดั้งเดิมจะซิงก์สองทางเฉพาะสิ่งที่ไคลเอนต์ต้องการ พร้อมทั้งรองรับการเปลี่ยนแปลงแบบมองโลกในแง่ดีและการจัดการความสอดคล้อง/ข้อขัดแย้งที่ตามมา
ข้อเสียคือความซับซ้อนในการติดตั้งใช้งาน ต้องติดตามอย่างแม่นยำว่าไคลเอนต์แต่ละตัวมีชุดย่อยใดของฐานข้อมูลทั้งหมดอยู่บ้าง จึงจะพุชการเปลี่ยนแปลงไปยังเฉพาะชุดย่อยนั้นได้ อีกทั้งหากต้องการระบุว่าจะดาวน์โหลดชุดย่อยใดของสถานะฐานข้อมูล ก็ต้องมี DSL ใหม่ และต้องเรียนรู้รวมถึงปรับแต่งมันใหม่ด้วย ถึงอย่างนั้นก็ดีใจที่พวกเขากำลังแก้ปัญหาที่ยากมากเหล่านี้ และเมื่อ SQLSync พร้อมรองรับการจำลองข้อมูลบางส่วน ก็น่าจะมีแนวปฏิบัติที่ดีถูกรวบรวมไว้แล้ว
ในทางกลับกัน ปัจจุบัน SQLSync รองรับเฉพาะ การซิงก์ฐานข้อมูลทั้งก้อน เท่านั้น ไคลเอนต์ทุกตัวจะเห็นมุมมองที่สอดคล้องกันของฐานข้อมูลทั้งหมด อาจสงสัยได้ทันทีว่านี่เป็นความคิดที่ดีหรือไม่ และมันก็ไม่เหมาะกับบางแอป แต่ถ้าคิดถึงแอปการเงินส่วนบุคคล เป้าหมายหลักคือการซิงก์ระหว่างอุปกรณ์ การสำรองข้อมูลบนคลาวด์ และการใช้งานแบบออฟไลน์ ดังนั้นการเก็บฐานข้อมูลทั้งก้อนไว้ในทุกอุปกรณ์อาจเป็นรูปแบบที่ต้องการมากกว่า โมเดลข้อมูลเชิงเอกสารอย่าง Airtable ก็เป็นอีกตัวอย่างได้ หากแยก Airtable แต่ละชุดเป็นฐานข้อมูลต่างหาก ไคลเอนต์ก็สามารถจัดการเองได้ว่าจะสนใจตารางใด
เมื่อโฟกัสที่การซิงก์ฐานข้อมูลทั้งก้อน เอนจินซิงก์จะเรียบง่ายกว่าวิธีแก้ปัญหาที่รองรับการจำลองข้อมูลบางส่วนมาก หนึ่งในข้อดีคือแบ็กเอนด์เบามาก เดโมปัจจุบัน(https://sqlsync-todo.pages.dev) ทำงานทั้งหมดภายใน Cloudflare Durable Objects โดยใช้พื้นที่จัดเก็บและเวลา CPU เพียงเล็กน้อยมาก
เพื่อให้ SQLSync รองรับกรณีใช้งานเหล่านี้ได้ ยังมีงานต้องทำอีกมาก และยังคงใกล้เคียงกับต้นแบบอยู่ แต่การทดสอบช่วงแรกให้ผลเชิงบวกมาก
ใน แอปมัลติเธเนนต์ขนาดใหญ่ ที่ชุดข้อมูลแต่ละชุดค่อนข้างเล็ก เคยคิดหลายครั้งว่า “แค่ส่งฐานข้อมูลไปยังไคลเอนต์เลยจะเป็นอย่างไร” มันดูเหมือนรูปแบบสถาปัตยกรรมต้องสาปที่หลุดจากมาตรฐานพอสมควร เลยไม่ได้ลงลึกนัก ถ้าได้รู้ว่าตัวเองคิดผิดก็คงดี
หากจะพิสูจน์อย่างจริงจังยังมีงานต้องทำอีกมาก แต่ค่อนข้างตื่นเต้นที่จะผลักดันต่อและดูว่ามันจะพาไปถึงไหน
นี่ดูเหมือนเป็นหนึ่งในปัญหาที่จะหายไปหมดหากเลิกใช้ SPA
หากใช้แนวทางตระกูล Hotwire หรือ htmx คิวรีก็เป็นเพียงคิวรีบนเซิร์ฟเวอร์ และปัญหาในการทำให้คิวรีเหล่านั้นเร็วขึ้นก็เป็นเรื่องที่เข้าใจกันดีกว่ามาก
ลองใช้ร่วมกับ ocaml + web components แล้ว เป็นประสบการณ์ด้าน ประสิทธิภาพการพัฒนา 10/10 ต้องการแค่เครื่องมือ build ตัวเดียวที่คอมไพล์ได้เร็วกว่าเวลา blink และไม่ต้องเดินสาย mapping JSON ระหว่างฟรอนต์เอนด์กับแบ็กเอนด์ จึง productive มากจริง ๆ
โดยส่วนตัวชอบ InertiaJs https://inertiajs.com มากกว่า มันเป็นระบบ เราเตอร์ฟรอนต์เอนด์ แบบหนึ่งที่ซิงก์สถานะกับเซิร์ฟเวอร์ใน “วิธีแบบเก่า”
โดยเฉพาะอย่างยิ่งหากเป็นผลิตภัณฑ์ที่ต้องทำงานได้แม้ในพื้นที่ที่อินเทอร์เน็ตไม่เสถียร
ตอนนี้กำลังเขียนบทความที่คล้ายกันมากเกี่ยวกับ “ฐานข้อมูลแบบฟูลสแต็ก” อยู่ ว่าด้วยแพตเทิร์นที่แอปจำนวนมากสร้างตรรกะของแบ็กเอนด์และฐานข้อมูลซ้ำอีกครั้งในโค้ดไคลเอนต์ฝั่งฟรอนต์เอนด์ แนวทางที่เราแนะนำคือเลือกฐานข้อมูลที่สามารถรันได้ทั้งฝั่งเซิร์ฟเวอร์และไคลเอนต์ แล้วซิงก์ข้อมูลระหว่างกัน
เหตุผลที่เราไม่ใช้ SQLite ในผลิตภัณฑ์ของเรา พูดตรง ๆ คือเพราะ SQL ไม่ใช่เครื่องมือที่เหมาะกับการ query ข้อมูลของแอปพลิเคชัน มันไม่เข้ากับโครงสร้างข้อมูลที่ต้องการในโค้ดไคลเอนต์ได้ง่าย และแทบทุกฐานข้อมูล SQL ไม่มีวิธี subscribe การเปลี่ยนแปลงของ query โดยไม่ต้อง polling query ซ้ำ ๆ
ถ้าคุณชอบไอเดียการมีฐานข้อมูลเต็มรูปแบบบนไคลเอนต์ และต้องการการผสานกับ TypeScript/JavaScript อย่างลึกซึ้ง ลองดู https://github.com/aspen-cloud/triplit ที่เรากำลังสร้างอยู่
https://github.com/cpursley/walex
ผมใช้งานแบบง่ายมาก ๆ คือเมื่อข้อมูลในตารางพื้นฐานเปลี่ยน ก็รัน query ใหม่โดยอัตโนมัติ อาจไม่ได้มีประสิทธิภาพเท่าการอัปเดตผลลัพธ์แบบ incremental แต่ query ของ SQLite โดยปกติเร็วมากอยู่แล้ว ผมเลยมองว่าไม่ใช่ปัญหาใหญ่