בניית מסד הנתונים שלך

במדריך הזה נסביר כמה מהמושגים המרכזיים בארכיטקטורת נתונים, ושיטות מומלצות ליצירת מבנה לנתוני ה-JSON ב-Firebase Realtime Database.

כדי לבנות מסד נתונים עם מבנה נכון, צריך להשקיע לא מעט מחשבה. והכי חשוב, צריך לתכנן איך הנתונים יישמרו ואיך הם יאוחזרו בהמשך, כדי שהתהליך יהיה פשוט ככל האפשר.

איך הנתונים בנויים: עץ JSON

כל הנתונים של Firebase Realtime Database מאוחסנים כאובייקטים של JSON. אפשר לחשוב על מסד הנתונים כעץ JSON שמתארח בענן. בניגוד למסד נתונים של SQL, אין טבלאות או רשומות. כשמוסיפים נתונים לעץ ה-JSON, הם הופכים לצומת במבנה ה-JSON הקיים עם מפתח משויך. אתם יכולים לספק מפתחות משלכם, כמו מזהי משתמשים או שמות סמנטיים, או שהם יכולים להיווצר בשבילכם באמצעות childByAutoId.

לדוגמה, נניח שיש אפליקציית צ'אט שמאפשרת למשתמשים לאחסן פרופיל בסיסי ורשימת אנשי קשר. פרופיל משתמש רגיל נמצא בנתיב כמו /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 מופיע בפרופיל של עדה. כדי למחוק את עדה מהקבוצה, צריך לעדכן את זה בשני מקומות.

זוהי יתירות הכרחית לקשרים דו-כיווניים. היא מאפשרת לאחזר במהירות וביעילות את החברויות של עדה, גם אם רשימת המשתמשים או הקבוצות כוללת מיליוני פריטים, או אם כללי האבטחה של Realtime Database מונעים גישה לחלק מהרשומות.

בגישה הזו, הופכים את הנתונים על ידי רישום המזהים כמפתחות והגדרת הערך כ-true, כך שהבדיקה של המפתח פשוטה כמו קריאת /users/$uid/groups/$group_id ובדיקה אם הוא null. האינדקס מהיר ויעיל הרבה יותר משאילתות או מסריקת הנתונים.

השלבים הבאים