การจัดโครงสร้างข้อมูล
คู่มือนี้ครอบคลุมแนวคิดหลักบางประการในสถาปัตยกรรมข้อมูลและแนวทางปฏิบัติแนะนำในการจัดโครงสร้างข้อมูล JSON ในฐานข้อมูลเรียลไทม์ของ Firebase
การสร้างฐานข้อมูลที่มีโครงสร้างอย่างเหมาะสมต้องอาศัยความรอบคอบพอสมควร สิ่งสำคัญที่สุดคือ คุณต้องวางแผนว่าจะมีการบันทึกข้อมูลไว้อย่างไร แล้วดึงข้อมูลมาในภายหลังเพื่อให้กระบวนการนั้นง่ายที่สุดเท่าที่จะทำได้
ข้อมูลมีโครงสร้างอย่างไร: เป็นแผนผัง JSON
ข้อมูลทั้งหมดของฐานข้อมูลเรียลไทม์ของ Firebase จะจัดเก็บเป็นออบเจ็กต์ JSON ฐานข้อมูลก็เหมือนต้นไม้ JSON ที่โฮสต์บนระบบคลาวด์ ไม่มีตารางหรือระเบียนเหมือนกับฐานข้อมูล SQL เมื่อคุณเพิ่มข้อมูลลงในโครงสร้าง JSON ข้อมูลนั้นจะกลายเป็นโหนดในโครงสร้าง JSON ที่มีอยู่ซึ่งมีคีย์ที่เชื่อมโยง คุณระบุคีย์ของคุณเอง เช่น รหัสผู้ใช้หรือชื่อความหมาย หรือระบุคีย์ให้คุณโดยใช้เมธอด Push()
ก็ได้
หากคุณสร้างคีย์ของคุณเอง คีย์จะต้องเข้ารหัส UTF-8 โดยต้องมีขนาดไม่เกิน 768 ไบต์ และต้องไม่มีอักขระควบคุม .
, $
, #
, [
, ]
, /
หรือ ASCII ตั้งแต่ 0-31 หรือ 127 คุณไม่สามารถใช้อักขระควบคุม ASCII ในค่าได้เช่นกัน
ตัวอย่างเช่น พิจารณาแอปพลิเคชันสำหรับแชทที่ช่วยให้ผู้ใช้จัดเก็บโปรไฟล์พื้นฐานและข้อมูลติดต่อ โปรไฟล์ผู้ใช้ทั่วไปจะอยู่ที่เส้นทาง เช่น /users/$uid
ผู้ใช้ alovelace
อาจมีรายการฐานข้อมูลที่มีลักษณะดังนี้
{ "users": { "alovelace": { "name": "Ada Lovelace", "contacts": { "ghopper": true }, }, "ghopper": { ... }, "eclarke": { ... } } }
แม้ว่าฐานข้อมูลจะใช้โครงสร้าง JSON แต่ข้อมูลที่จัดเก็บในฐานข้อมูลอาจแสดงเป็นประเภทเนทีฟบางประเภทที่สอดคล้องกับประเภท JSON ที่มีอยู่เพื่อช่วยให้คุณเขียนโค้ดที่ดูแลรักษาได้มากขึ้น
แนวทางปฏิบัติแนะนำสำหรับโครงสร้างข้อมูล
หลีกเลี่ยงการซ้อนข้อมูล
เนื่องจากฐานข้อมูลเรียลไทม์ของ Firebase ทำให้สามารถซ้อนข้อมูลได้สูงสุด 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": { ... } } }
ตอนนี้คุณจะทำซ้ำผ่านรายการห้องแชทได้ด้วยการดาวน์โหลดเพียง 2-3 ไบต์ต่อการสนทนา แล้วดึงข้อมูลเมตาสำหรับข้อมูลหรือการแสดงห้องใน 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
หรือไม่ ดัชนีทำงานเร็วกว่า
และมีประสิทธิภาพกว่าการค้นหาหรือการสแกนข้อมูล