במדריך הזה נסביר כמה מהמושגים המרכזיים בארכיטקטורת נתונים, וגם נציג שיטות מומלצות למבנה של נתוני ה-JSON ב-Firebase Realtime Database.
כדי לבנות מסד נתונים מובנה בצורה נכונה, צריך לתכנן מראש. והכי חשוב, צריך לתכנן איך הנתונים יישמרו ואיך ישוחזרו בהמשך, כדי שהתהליך יהיה קל ככל האפשר.
איך הנתונים בנויים: הם מוצגים כעץ JSON
כל הנתונים של Firebase Realtime Database מאוחסנים כאובייקטים של 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 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
, כולל כל החברים וההודעות.
השטחת מבני נתונים
אם הנתונים מפולגים לנתיבים נפרדים, שנקראים גם דה-נורמליזציה, אפשר להוריד אותם ביעילות בשיחות נפרדות, לפי הצורך. כדאי לשקול את המבנה השטוח הזה:
{ // 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": { "..." } } }
עכשיו אפשר לעבור על רשימת החדרים על ידי הורדה של כמה בייטים לכל שיחה, ואז לאחזר במהירות את המטא-נתונים כדי להציג את החדרים בממשק משתמש. אפשר לאחזר הודעות בנפרד ולהציג אותן כשהן מגיעות, כך שממשק המשתמש נשאר מהיר ומגיב.
יצירת נתונים שניתנים להרחבה
כשמפתחים אפליקציות, עדיף להוריד קבוצת משנה של רשימה. זה קורה בדרך כלל אם הרשימה מכילה אלפי רשומות. אם הקשר הזה הוא סטטי וחד-כיווני, אפשר פשוט להציב את אובייקטי הצאצא מתחת לאובייקט האב.
לפעמים, מערכת היחסים הזו דינמית יותר, או שיכול להיות שיהיה צורך לבצע דה-נורמליזציה של הנתונים האלה. במקרים רבים אפשר לבצע דה-נורמליזציה של הנתונים באמצעות שאילתה לאחזור קבוצת משנה של הנתונים, כמו שמתואר במאמר בנושא אחזור נתונים.
אבל יכול להיות שגם זה לא יספיק. לדוגמה, נניח שיש קשר דו-כיווני בין משתמשים לקבוצות. משתמשים יכולים להיות חברים בקבוצה, וקבוצות מורכבות מרשימה של משתמשים. כשמגיע הזמן להחליט לאילו קבוצות משתמש משתייך, המצב מסתבך.
צריך למצוא דרך אלגנטית לרשום את הקבוצות שהמשתמש שייך אליהן ולאחזר רק את הנתונים של הקבוצות האלה. אינדקס של קבוצות יכול לעזור מאוד במקרה הזה:
// 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 } }, // ... } }
יכול להיות שתשימו לב שחלק מהנתונים משוכפלים, כי הקשר נשמר גם ברשומה של עדה וגם בקבוצה. עכשיו alovelace
מאונדקס בקבוצה, ו-techpioneers
מופיע בפרופיל של Ada. לכן, כדי למחוק את Ada מהקבוצה, צריך לעדכן את המידע בשני מקומות.
זהו גיבוי הכרחי לקשרים דו-כיווניים. היא מאפשרת לכם לאחזר במהירות וביעילות את החברות של Ada, גם כשרשימת המשתמשים או הקבוצות גדלה למיליונים או כשכללי האבטחה של Realtime Database מונעים גישה לחלק מהרשומות.
הגישה הזו, שבה הופכים את הנתונים על ידי הצגת המזהים כמפתחות והגדרת הערך כ-true, מאפשרת לבדוק אם מפתח קיים בקלות רבה – פשוט קוראים את /users/$uid/groups/$group_id
ובודקים אם הוא null
. האינדקס מהיר ויעיל בהרבה מביצוע שאילתות או סריקה של הנתונים.