คู่มือนี้ครอบคลุมแนวคิดหลักๆ ในสถาปัตยกรรมข้อมูลและแนวทางปฏิบัติแนะนำสำหรับการจัดโครงสร้างข้อมูล JSON ใน Firebase Realtime Database
การสร้างฐานข้อมูลที่โครงสร้างถูกต้องต้องอาศัยการวางแผนล่วงหน้าอย่างมาก ที่สำคัญที่สุด คุณต้องวางแผนวิธีบันทึกและเรียกข้อมูลในภายหลังเพื่อให้กระบวนการนี้ง่ายที่สุด
โครงสร้างข้อมูล: เป็นต้นไม้ JSON
ระบบจะจัดเก็บข้อมูล Firebase Realtime Database ทั้งหมดเป็นออบเจ็กต์ JSON คุณอาจนึกถึงฐานข้อมูลเป็นต้นไม้ JSON ที่โฮสต์ในระบบคลาวด์ โดยไม่มีตารางหรือระเบียน ซึ่งแตกต่างจากฐานข้อมูล SQL เมื่อคุณเพิ่มข้อมูลลงในต้นไม้ JSON ข้อมูลดังกล่าวจะกลายเป็นโหนดในโครงสร้าง JSON ที่มีอยู่ซึ่งมีคีย์ที่เชื่อมโยง คุณสามารถระบุคีย์ของคุณเอง เช่น รหัสผู้ใช้หรือชื่อเชิงความหมาย หรือระบบจะระบุคีย์ให้คุณโดยใช้คำขอ POST
หากคุณสร้างคีย์ของคุณเอง คีย์ต้องเข้ารหัส UTF-8, มีความยาวได้สูงสุด 768 ไบต์ และต้องไม่มี .
, $
, #
, [
, ]
, /
หรืออักขระควบคุม ASCII 0-31 หรือ 127 นอกจากนี้ คุณยังใช้อักขระควบคุม ASCII ในค่าเองไม่ได้ด้วย
ตัวอย่างเช่น ลองพิจารณาแอปพลิเคชันแชทที่อนุญาตให้ผู้ใช้จัดเก็บโปรไฟล์พื้นฐานและรายชื่อติดต่อ โปรไฟล์ผู้ใช้ทั่วไปจะอยู่ที่เส้นทาง เช่น /users/$uid
ผู้ใช้ alovelace
อาจมีรายการฐานข้อมูลที่มีลักษณะดังนี้
{ "users": { "alovelace": { "name": "Ada Lovelace", "contacts": { "ghopper": true }, }, "ghopper": { ... }, "eclarke": { ... } } }
แม้ว่าฐานข้อมูลจะใช้ต้นไม้ JSON แต่ข้อมูลที่จัดเก็บไว้ในฐานข้อมูลจะแสดงเป็นประเภทเนทีฟบางประเภทที่สอดคล้องกับประเภท JSON ที่มีเพื่อช่วยให้คุณเขียนโค้ดที่ดูแลรักษาได้ง่ายขึ้น
แนวทางปฏิบัติแนะนำสำหรับโครงสร้างข้อมูล
หลีกเลี่ยงการฝังข้อมูล
เนื่องจาก Firebase Realtime Database อนุญาตให้ฝังข้อมูลได้ลึกสุด 32 ชั้น คุณจึงอาจคิดว่านี่ควรเป็นโครงสร้างเริ่มต้น อย่างไรก็ตาม เมื่อดึงข้อมูลในตำแหน่งในฐานข้อมูล คุณจะดึงข้อมูลโหนดย่อยทั้งหมดของโหนดนั้นด้วย นอกจากนี้ เมื่อคุณให้สิทธิ์เข้าถึงระดับอ่านหรือเขียนแก่ผู้ใช้ที่โหนดในฐานข้อมูล เท่ากับว่าคุณให้สิทธิ์เข้าถึงข้อมูลทั้งหมดภายใต้โหนดนั้นด้วย ดังนั้นในทางปฏิบัติ โครงสร้างข้อมูลควรเป็นโครงสร้างแบบแบนที่สุด
ตัวอย่างที่แสดงให้เห็นว่าทำไม Structured Data ที่ฝังอยู่จึงไม่ดีคือโครงสร้างที่ฝังอยู่หลายชั้นต่อไปนี้
{ // 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": { ... } } }
การออกแบบที่ซ้อนกันนี้ทำให้เกิดปัญหาในการวนดูข้อมูล ตัวอย่างเช่น การแสดงชื่อการสนทนาใน Chat จะต้องดาวน์โหลดchats
ทั้งต้นไม้ รวมถึงสมาชิกและข้อความทั้งหมดไปยังไคลเอ็นต์
รวมโครงสร้างข้อมูล
หากข้อมูลแยกออกเป็นเส้นทางแยกต่างหากแทน ซึ่งเรียกว่า "การแปลงข้อมูลให้เป็นรูปแบบปกติ" ระบบจะดาวน์โหลดข้อมูลได้อย่างมีประสิทธิภาพในการเรียกใช้แยกต่างหากตามต้องการ ลองดูโครงสร้างแบบแบนนี้
{ // 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กฎด้านความปลอดภัยprevent access to some of the records
วิธีการนี้ซึ่งกลับข้อมูลโดยการระบุรหัสเป็นคีย์และตั้งค่าเป็น "จริง" ทําให้การตรวจสอบคีย์เป็นเรื่องง่ายเพียงอ่าน /users/$uid/groups/$group_id
และตรวจสอบว่า null
หรือไม่ ดัชนีจะเร็วกว่าและมีประสิทธิภาพมากกว่าการค้นหาหรือการสแกนข้อมูล