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

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

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

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

כדי להשתמש באמולטור Realtime Database יש כמה שלבים פשוטים:

  1. הוספה של שורת קוד להגדרות הבדיקה של האפליקציה כדי להתחבר לאמולטור.
  2. מהרמה הבסיסית (root) של ספריית הפרויקט המקומית, מריצים את 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. ה-listener מופעל פעם אחת בשביל על המצב הראשוני של הנתונים, ושוב בכל פעם שהנתונים משתנים.

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

כדי לבצע פעולות כתיבה בסיסיות, אפשר להשתמש ב-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 יחזיר את הערך 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 באמצעות ה-method val().

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

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

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

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

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

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

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(), ניתן לעדכן ערכים של ילדים ברמה נמוכה יותר עד שמציין נתיב למפתח. אם הנתונים מאוחסנים במספר מיקומים כדי לשפר את יכולת ההתאמה לעומס, אפשר לעדכן את כל המופעים של הנתונים באמצעות הרחבת נתונים.

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

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

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

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

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

השלבים הבאים