การจัดโครงสร้างข้อมูล
คู่มือนี้ครอบคลุมแนวคิดหลักบางประการเกี่ยวกับสถาปัตยกรรมข้อมูลและแนวทางปฏิบัติแนะนำในการจัดโครงสร้างข้อมูล JSON ใน Firebase Realtime Database
การสร้างฐานข้อมูลที่มีโครงสร้างอย่างเหมาะสมต้องอาศัยการวางแผนล่วงหน้าพอสมควร ที่สำคัญที่สุดคือ คุณต้องวางแผนวิธีบันทึกข้อมูลและดึงข้อมูลในภายหลังเพื่อให้กระบวนการนั้นง่ายที่สุดเท่าที่จะเป็นไปได้
วิธีจัดโครงสร้างข้อมูล: โครงสร้างแบบต้นไม้ JSON
ข้อมูล Firebase Realtime Database ทั้งหมดจะจัดเก็บเป็นออบเจ็กต์ JSON คุณสามารถคิดว่าฐานข้อมูลเป็นต้นไม้ JSON ที่โฮสต์ในระบบคลาวด์ ซึ่งแตกต่างจากฐานข้อมูล SQL ตรงที่ไม่มีตารางหรือระเบียน เมื่อคุณเพิ่มข้อมูลลงในแผนผัง JSON ข้อมูลดังกล่าวจะกลายเป็นโหนดในโครงสร้าง JSON ที่มีอยู่ซึ่งมีคีย์ที่เชื่อมโยง คุณสามารถระบุคีย์ของคุณเอง เช่น รหัสผู้ใช้หรือชื่อเชิงความหมาย หรือจะระบุคีย์ให้คุณโดยใช้เมธอด Push() ก็ได้
ตัวอย่างเช่น ลองพิจารณาแอปพลิเคชันแชทที่อนุญาตให้ผู้ใช้จัดเก็บโปรไฟล์พื้นฐานและข้อมูลรายชื่อติดต่อ โปรไฟล์ผู้ใช้ทั่วไปจะอยู่ในเส้นทาง เช่น
/users/$uid ผู้ใช้ alovelace อาจมีรายการฐานข้อมูลที่มีลักษณะดังนี้
{ "users": { "alovelace": { "name": "Ada Lovelace", "contacts": { "ghopper": true }, }, "ghopper": { "..." }, "eclarke": { "..." } } }
แม้ว่าฐานข้อมูลจะใช้โครงสร้าง JSON แต่ข้อมูลที่จัดเก็บในฐานข้อมูลสามารถแสดงเป็นประเภทเนทีฟบางประเภทที่สอดคล้องกับประเภท JSON ที่มีอยู่เพื่อช่วยให้คุณเขียนโค้ดที่ดูแลรักษาได้ง่ายขึ้น
แนวทางปฏิบัติแนะนำสำหรับโครงสร้างข้อมูล
หลีกเลี่ยงการซ้อนข้อมูล
เนื่องจาก Firebase Realtime Database อนุญาตให้ซ้อนข้อมูลได้ลึกถึง 32 ระดับ คุณจึงอาจคิดว่านี่ควรเป็นโครงสร้างเริ่มต้น อย่างไรก็ตาม เมื่อดึงข้อมูลที่ตำแหน่งหนึ่งในฐานข้อมูล คุณจะดึงโหนดระดับย่อยทั้งหมดของตำแหน่งนั้นด้วย นอกจากนี้ เมื่อคุณให้สิทธิ์อ่านหรือเขียนแก่ผู้ใช้ที่โหนดในฐานข้อมูล คุณยังให้สิทธิ์เข้าถึงข้อมูลทั้งหมดภายใต้โหนดนั้นแก่ผู้ใช้ด้วย ดังนั้น ในทางปฏิบัติแล้ว คุณควรทำให้โครงสร้างข้อมูลแบนราบที่สุดเท่าที่จะเป็นไปได้
ตัวอย่างที่แสดงให้เห็นว่าเหตุใดข้อมูลที่ซ้อนกันจึงไม่ดี ให้พิจารณาโครงสร้างที่ซ้อนกันหลายรายการต่อไปนี้
{ // This is a poorly nested data architecture, because iterating the children // of the "chats" node to get a list of conversation titles requires // potentially downloading hundreds of megabytes of messages "chats": { "one": { "title": "Historical Tech Pioneers", "messages": { "m1": { "sender": "ghopper", "message": "Relay malfunction found. Cause: moth." }, "m2": { ... }, // a very long list of messages } }, "two": { "..." } } }
การออกแบบที่ซ้อนกันนี้ทำให้การทำซ้ำผ่านข้อมูลกลายเป็นปัญหา ตัวอย่างเช่น การแสดงรายการชื่อของการสนทนาในแชทต้องใช้โครงสร้าง chats ทั้งหมด ซึ่งรวมถึงสมาชิกและข้อความทั้งหมดที่ต้องดาวน์โหลดไปยังไคลเอ็นต์
ปรับโครงสร้างข้อมูลให้เป็นแบบแฟลต
หากข้อมูลถูกแยกออกเป็นเส้นทางที่แยกจากกัน หรือที่เรียกว่าการดีนอร์มัลไลเซชัน (Denormalization) ระบบจะดาวน์โหลดข้อมูลได้อย่างมีประสิทธิภาพในการเรียกแยกกันตามความจำเป็น ลองพิจารณาโครงสร้างแบบลดลำดับชั้นต่อไปนี้
{ // Chats contains only meta info about each conversation // stored under the chats's unique ID "chats": { "one": { "title": "Historical Tech Pioneers", "lastMessage": "ghopper: Relay malfunction found. Cause: moth.", "timestamp": 1459361875666 }, "two": { "..." }, "three": { "..." } }, // Conversation members are easily accessible // and stored by chat conversation ID "members": { // we'll talk about indices like this below "one": { "ghopper": true, "alovelace": true, "eclarke": true }, "two": { "..." }, "three": { "..." } }, // Messages are separate from data we may want to iterate quickly // but still easily paginated and queried, and organized by chat // conversation ID "messages": { "one": { "m1": { "name": "eclarke", "message": "The relay seems to be malfunctioning.", "timestamp": 1459361875337 }, "m2": { "..." }, "m3": { "..." } }, "two": { "..." }, "three": { "..." } } }
ตอนนี้คุณสามารถทำซ้ำผ่านรายการห้องแชทได้โดยดาวน์โหลดเพียงไม่กี่ไบต์ต่อการสนทนา ซึ่งจะดึงข้อมูลเมตาสำหรับการแสดงรายการหรือแสดงห้องแชทใน UI ได้อย่างรวดเร็ว ระบบจะดึงข้อความแยกกันและแสดงข้อความเมื่อมาถึง ซึ่งช่วยให้ UI ตอบสนองและทำงานได้อย่างรวดเร็ว
สร้างข้อมูลที่ปรับขนาดได้
เมื่อสร้างแอป การดาวน์โหลดชุดย่อยของรายการมักจะดีกว่า ซึ่งมักเกิดขึ้นในกรณีที่รายการมีเรคคอร์ดหลายพันรายการ เมื่อความสัมพันธ์นี้เป็นแบบคงที่และมีทิศทางเดียว คุณสามารถฝังออบเจ็กต์ย่อยไว้ในออบเจ็กต์หลักได้
บางครั้งความสัมพันธ์นี้อาจมีความผันผวนมากกว่า หรืออาจจำเป็นต้องดีนอร์มัลไลซ์ข้อมูลนี้ หลายครั้งที่คุณสามารถดีนอร์มัลไลซ์ข้อมูลได้โดยใช้การค้นหาเพื่อดึงข้อมูลชุดย่อยตามที่อธิบายไว้ในดึงข้อมูล
แต่แค่นี้ก็อาจยังไม่พอ ตัวอย่างเช่น ลองพิจารณาความสัมพันธ์แบบ 2 ทางระหว่างผู้ใช้และกลุ่ม ผู้ใช้สามารถอยู่ในกลุ่มได้ และกลุ่มจะประกอบด้วยรายชื่อผู้ใช้ เมื่อถึงเวลาที่ต้องตัดสินใจว่าผู้ใช้ควรอยู่ในกลุ่มใด เรื่องต่างๆ ก็จะเริ่มซับซ้อนขึ้น
สิ่งที่จำเป็นคือวิธีที่เหมาะสมในการแสดงรายการกลุ่มที่ผู้ใช้เป็นสมาชิกและดึงข้อมูลเฉพาะสำหรับกลุ่มเหล่านั้น ดัชนีของกลุ่มจะช่วยได้มากในกรณีนี้
// An index to track Ada's memberships { "users": { "alovelace": { "name": "Ada Lovelace", // Index Ada's groups in her profile "groups": { // the value here doesn't matter, just that the key exists "techpioneers": true, "womentechmakers": true } }, // ... }, "groups": { "techpioneers": { "name": "Historical Tech Pioneers", "members": { "alovelace": true, "ghopper": true, "eclarke": true } }, // ... } }
คุณอาจสังเกตเห็นว่าวิธีนี้จะทำให้ข้อมูลบางอย่างซ้ำกันโดยการจัดเก็บความสัมพันธ์ไว้ทั้งในระเบียนของ Ada และในกลุ่ม ตอนนี้ alovelace ได้รับการจัดทำดัชนีภายใต้กลุ่ม และ techpioneers จะแสดงในโปรไฟล์ของ Ada ดังนั้นหากต้องการลบ Ada ออกจากกลุ่ม
ก็ต้องอัปเดตใน 2 ที่
ซึ่งเป็นความซ้ำซ้อนที่จำเป็นสำหรับความสัมพันธ์แบบ 2 ทาง ซึ่งช่วยให้คุณดึงข้อมูลการเป็นสมาชิกของ Ada ได้อย่างรวดเร็วและมีประสิทธิภาพ แม้ว่ารายชื่อผู้ใช้หรือกลุ่มจะเพิ่มขึ้นเป็นล้านๆ รายการ หรือเมื่อกฎความปลอดภัยของ Realtime Database ป้องกันการเข้าถึงระเบียนบางรายการก็ตาม
วิธีนี้เป็นการกลับข้อมูลโดยแสดงรหัสเป็นคีย์และตั้งค่าเป็น "จริง" ซึ่งจะทำให้การตรวจสอบคีย์ทำได้ง่ายเหมือนกับการอ่าน /users/$uid/groups/$group_id และตรวจสอบว่าเป็น null หรือไม่ ดัชนีจะทำงานได้เร็วกว่า
และมีประสิทธิภาพมากกว่าการค้นหาหรือสแกนข้อมูล