קריאה וכתיבה של נתונים באינטרנט

(אופציונלי) יצירת אב טיפוס ובדיקה באמצעות Firebase Local Emulator Suite

לפני שנדבר על האופן שבו האפליקציה קוראת מ-Realtime Database וכותבת אליו, נציג קבוצה של כלים שאפשר להשתמש בהם כדי ליצור אב טיפוס ולבדוק את הפונקציונליות של Realtime Database: Firebase Local Emulator Suite. אם אתם רוצים לנסות מודלים שונים של נתונים, לבצע אופטימיזציה של כללי האבטחה או למצוא את הדרך היעילה ביותר מבחינת עלות לאינטראקציה עם הקצה העורפי, יכול להיות שעבודה מקומית ללא פריסה של שירותים פעילים תהיה רעיון מצוין.

אמולטור של Realtime Database הוא חלק מ-Local Emulator Suite, שמאפשר לאפליקציה שלכם לקיים אינטראקציה עם תוכן ההגדרות של מסד הנתונים המאומלל, וגם עם משאבי הפרויקט המאומללים (פונקציות, מסדי נתונים אחרים וכללי אבטחה).

כדי להשתמש במהדמ של Realtime Database, צריך לבצע כמה שלבים פשוטים:

  1. הוספת שורת קוד להגדרות הבדיקה של האפליקציה כדי להתחבר למהדר.
  2. מריצים את firebase emulators:start ברמה הבסיסית של ספריית הפרויקט המקומית.
  3. ביצוע קריאות מקוד האב טיפוס של האפליקציה באמצעות ערכת ה-SDK של פלטפורמת Realtime Database כרגיל, או באמצעות ה-API ל-REST של Realtime Database.

יש הדרכה מפורטת שכוללת את Realtime Database ו-Cloud Functions. מומלץ גם לעיין במבוא ל-Local Emulator Suite.

קבלת הפניה למסד נתונים

כדי לקרוא או לכתוב נתונים מהמסד הנתונים, צריך מופע של firebase.database.Reference:

Web

import { getDatabase } from "firebase/database";

const database = getDatabase();

Web

var database = firebase.database();

כתיבת נתונים

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

כדי לאחזר נתונים מ-Firebase, צריך לצרף מאזין אסינכררוני ל-firebase.database.Reference. המאזין מופעל פעם אחת עבור המצב הראשוני של הנתונים ופעם נוספת בכל פעם שהנתונים משתנים.

פעולות כתיבה בסיסיות

לפעולות כתיבה בסיסיות, אפשר להשתמש ב-set() כדי לשמור נתונים במיקום הפניה מסוים, ולהחליף את כל הנתונים הקיימים באותו נתיב. לדוגמה, אפליקציית בלוגים חברתית עשויה להוסיף משתמש עם set() באופן הבא:

Web

import { getDatabase, ref, set } from "firebase/database";

function writeUserData(userId, name, email, imageUrl) {
  const db = getDatabase();
  set(ref(db, 'users/' + userId), {
    username: name,
    email: email,
    profile_picture : imageUrl
  });
}

Web

function writeUserData(userId, name, email, imageUrl) {
  firebase.database().ref('users/' + userId).set({
    username: name,
    email: email,
    profile_picture : imageUrl
  });
}

שימוש ב-set() גורם להחליפת הנתונים במיקום שצוין, כולל צמתים צאצאים.

קריאת הנתונים

האזנה לאירועי ערך

כדי לקרוא נתונים בנתיב ולעקוב אחרי שינויים, משתמשים ב-onValue() כדי לצפות באירועים. אפשר להשתמש באירוע הזה כדי לקרוא קובצי snapshot סטטיים של התוכן בנתיב נתון, כפי שהם היו בזמן האירוע. השיטה הזו מופעלת פעם אחת כשהמאזין מצורף, ופעם נוספת בכל פעם שהנתונים, כולל הצאצאים, משתנים. בקריאה החוזרת (callback) של האירוע מועברת תמונת מצב שמכילה את כל הנתונים במיקום הזה, כולל נתוני הצאצאים. אם אין נתונים, קובץ ה-snapshot יחזיר false כשקוראים לפונקציה exists() וגם null כשקוראים לפונקציה val().

הדוגמה הבאה ממחישה אפליקציית בלוגים חברתית שמאחזרת מהמסד נתונים את מספר הכוכבים של פוסט:

Web

import { getDatabase, ref, onValue } from "firebase/database";

const db = getDatabase();
const starCountRef = ref(db, 'posts/' + postId + '/starCount');
onValue(starCountRef, (snapshot) => {
  const data = snapshot.val();
  updateStarCount(postElement, data);
});

Web

var starCountRef = firebase.database().ref('posts/' + postId + '/starCount');
starCountRef.on('value', (snapshot) => {
  const data = snapshot.val();
  updateStarCount(postElement, data);
});

המאזין מקבל snapshot שמכיל את הנתונים במיקום שצוין במסד הנתונים בזמן האירוע. אפשר לאחזר את הנתונים ב-snapshot באמצעות השיטה val().

קריאת נתונים פעם אחת

קריאת נתונים פעם אחת באמצעות get()‎

ה-SDK נועד לנהל אינטראקציות עם שרתי מסדי נתונים, גם כשהאפליקציה אונליין וגם כשהיא אופליין.

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

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

שימוש מיותר ב-get() עלול להגדיל את השימוש ברוחב הפס ולהוביל לירידה בביצועים. אפשר למנוע זאת באמצעות שימוש במאזין בזמן אמת, כפי שמתואר למעלה.

Web

import { getDatabase, ref, child, get } from "firebase/database";

const dbRef = ref(getDatabase());
get(child(dbRef, `users/${userId}`)).then((snapshot) => {
  if (snapshot.exists()) {
    console.log(snapshot.val());
  } else {
    console.log("No data available");
  }
}).catch((error) => {
  console.error(error);
});

Web

const dbRef = firebase.database().ref();
dbRef.child("users").child(userId).get().then((snapshot) => {
  if (snapshot.exists()) {
    console.log(snapshot.val());
  } else {
    console.log("No data available");
  }
}).catch((error) => {
  console.error(error);
});

קריאת הנתונים פעם אחת עם צופה

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

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

Web

import { getDatabase, ref, onValue } from "firebase/database";
import { getAuth } from "firebase/auth";

const db = getDatabase();
const auth = getAuth();

const userId = auth.currentUser.uid;
return onValue(ref(db, '/users/' + userId), (snapshot) => {
  const username = (snapshot.val() && snapshot.val().username) || 'Anonymous';
  // ...
}, {
  onlyOnce: true
});

Web

var userId = firebase.auth().currentUser.uid;
return firebase.database().ref('/users/' + userId).once('value').then((snapshot) => {
  var username = (snapshot.val() && snapshot.val().username) || 'Anonymous';
  // ...
});

עדכון או מחיקה של נתונים

עדכון שדות ספציפיים

כדי לכתוב בו-זמנית לצאצאים ספציפיים של צומת בלי לשכתב צמתים צאצאים אחרים, משתמשים בשיטה update().

כשקוראים ל-update(), אפשר לעדכן ערכים של צאצאים ברמה נמוכה יותר על ידי ציון נתיב למפתח. אם הנתונים מאוחסנים בכמה מיקומים כדי להתאים אותם טוב יותר לעומס, אפשר לעדכן את כל המופעים של הנתונים האלה באמצעות fan-out.

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

Web

import { getDatabase, ref, child, push, update } from "firebase/database";

function writeNewPost(uid, username, picture, title, body) {
  const db = getDatabase();

  // A post entry.
  const postData = {
    author: username,
    uid: uid,
    body: body,
    title: title,
    starCount: 0,
    authorPic: picture
  };

  // Get a key for a new Post.
  const newPostKey = push(child(ref(db), 'posts')).key;

  // Write the new post's data simultaneously in the posts list and the user's post list.
  const updates = {};
  updates['/posts/' + newPostKey] = postData;
  updates['/user-posts/' + uid + '/' + newPostKey] = postData;

  return update(ref(db), updates);
}

Web

function writeNewPost(uid, username, picture, title, body) {
  // A post entry.
  var postData = {
    author: username,
    uid: uid,
    body: body,
    title: title,
    starCount: 0,
    authorPic: picture
  };

  // Get a key for a new Post.
  var newPostKey = firebase.database().ref().child('posts').push().key;

  // Write the new post's data simultaneously in the posts list and the user's post list.
  var updates = {};
  updates['/posts/' + newPostKey] = postData;
  updates['/user-posts/' + uid + '/' + newPostKey] = postData;

  return firebase.database().ref().update(updates);
}

בדוגמה הזו נעשה שימוש ב-push() כדי ליצור פוסט בצומת שמכיל פוסטים של כל המשתמשים ב-/posts/$postid, ולשלוף בו-זמנית את המפתח. לאחר מכן אפשר להשתמש במפתח כדי ליצור רשומה שנייה בפוסטים של המשתמש בכתובת /user-posts/$userid/$postid.

בעזרת הנתיבים האלה אפשר לבצע עדכונים בו-זמנית במספר מיקומים בעץ ה-JSON באמצעות קריאה אחת ל-update(). לדוגמה, כך נוצר הפוסט החדש בשני המיקומים. עדכונים בו-זמניים שמתבצעים באופן הזה הם אטומיים: כל העדכונים מתבצעים בהצלחה או שהם כולם נכשלים.

הוספת קריאה חוזרת (callback) בסיום הרכישה

אם רוצים לדעת מתי הנתונים הוגדרו, אפשר להוסיף קריאה חוזרת (callback) להשלמה. גם set() וגם update() משתמשים בקריאה חוזרת (callback) להשלמה, שמופיעה כשהכתיבה בוצעה למסד הנתונים. אם הקריאה נכשלה, הקריאה החוזרת מועברת אובייקט שגיאה שמציין את הסיבה לכשל.

Web

import { getDatabase, ref, set } from "firebase/database";

const db = getDatabase();
set(ref(db, 'users/' + userId), {
  username: name,
  email: email,
  profile_picture : imageUrl
})
.then(() => {
  // Data saved successfully!
})
.catch((error) => {
  // The write failed...
});

Web

firebase.database().ref('users/' + userId).set({
  username: name,
  email: email,
  profile_picture : imageUrl
}, (error) => {
  if (error) {
    // The write failed...
  } else {
    // Data saved successfully!
  }
});

מחיקת נתונים

הדרך הפשוטה ביותר למחוק נתונים היא להפעיל את remove() על הפניה למיקום של הנתונים האלה.

אפשר גם למחוק על ידי ציון הערך null כערך של פעולת כתיבה אחרת, כמו set() או update(). אפשר להשתמש בשיטה הזו עם update() כדי למחוק כמה צאצאים בקריאה אחת ל-API.

מקבלים Promise

כדי לדעת מתי הנתונים שלכם מועברים לשרת Firebase Realtime Database, תוכלו להשתמש ב-Promise. גם set() וגם update() יכולים להחזיר ערך של Promise שאפשר להשתמש בו כדי לדעת מתי הכתיבה התחייבה במסד הנתונים.

ניתוק של רכיבי מעקב

כדי להסיר את הקריאות החוזרות, אתם צריכים להפעיל את השיטה off() בחומר העזר של מסד הנתונים ב-Firebase.

אפשר להסיר אוזן יחיד על ידי העברתו כפרמטר אל off(). קריאה ל-off() במיקום ללא ארגומנטים תסיר את כל המאזינים מהמיקום הזה.

קריאה ל-off() על מאזין הורה לא מסירה באופן אוטומטי מאזינים שרשומים בצמתים הצאצאים שלו. צריך לקרוא ל-off() גם על כל מאזיני הצאצאים כדי להסיר את פונקציית ה-callback.

שמירת נתונים כעסקאות

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

לדוגמה, באפליקציית הבלוגים החברתית לדוגמה, אפשר לאפשר למשתמשים לסמן פוסט בכוכב ולהסיר את הסימון, ולעקוב אחרי מספר הכוכבים שקיבל הפוסט באופן הבא:

Web

import { getDatabase, ref, runTransaction } from "firebase/database";

function toggleStar(uid) {
  const db = getDatabase();
  const postRef = ref(db, '/posts/foo-bar-123');

  runTransaction(postRef, (post) => {
    if (post) {
      if (post.stars && post.stars[uid]) {
        post.starCount--;
        post.stars[uid] = null;
      } else {
        post.starCount++;
        if (!post.stars) {
          post.stars = {};
        }
        post.stars[uid] = true;
      }
    }
    return post;
  });
}

Web

function toggleStar(postRef, uid) {
  postRef.transaction((post) => {
    if (post) {
      if (post.stars && post.stars[uid]) {
        post.starCount--;
        post.stars[uid] = null;
      } else {
        post.starCount++;
        if (!post.stars) {
          post.stars = {};
        }
        post.stars[uid] = true;
      }
    }
    return post;
  });
}

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

הוספות אטומיות בצד השרת

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

Web

function addStar(uid, key) {
  import { getDatabase, increment, ref, update } from "firebase/database";
  const dbRef = ref(getDatabase());

  const updates = {};
  updates[`posts/${key}/stars/${uid}`] = true;
  updates[`posts/${key}/starCount`] = increment(1);
  updates[`user-posts/${key}/stars/${uid}`] = true;
  updates[`user-posts/${key}/starCount`] = increment(1);
  update(dbRef, updates);
}

Web

function addStar(uid, key) {
  const updates = {};
  updates[`posts/${key}/stars/${uid}`] = true;
  updates[`posts/${key}/starCount`] = firebase.database.ServerValue.increment(1);
  updates[`user-posts/${key}/stars/${uid}`] = true;
  updates[`user-posts/${key}/starCount`] = firebase.database.ServerValue.increment(1);
  firebase.database().ref().update(updates);
}

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

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

עבודה עם נתונים במצב אופליין

אם לקוח יתנתק מהרשת, האפליקציה תמשיך לפעול בצורה תקינה.

כל לקוח שמחובר למסד נתונים של Firebase שומר גרסה פנימית משלו של כל הנתונים הפעילים. כשכותבים נתונים, הם נכתבים קודם בגרסה המקומית הזו. לאחר מכן, לקוח Firebase מסנכרן את הנתונים האלה עם שרתי מסדי הנתונים המרוחקים ועם לקוחות אחרים על בסיס 'לפי יכולת'.

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

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

נסביר בהרחבה על התנהגות אופליין בקטע מידע נוסף על היכולות אונליין ואופליין.

השלבים הבאים