(אופציונלי) יצירת אב טיפוס ובדיקה באמצעות Firebase Local Emulator Suite
לפני שנדבר על האופן שבו האפליקציה קוראת מ-Realtime Database וכותבת אליו, נציג קבוצת כלים שבהם אפשר להשתמש כדי ליצור אב טיפוס ולבדוק את הפונקציונליות של Realtime Database: Firebase Local Emulator Suite. אם אתם מנסים מודלים שונים של נתונים, מבצעים אופטימיזציה של כללי האבטחה או מחפשים את הדרך הכי משתלמת לאינטראקציה עם הקצה העורפי, כדאי לעבוד באופן מקומי בלי לפרוס שירותים פעילים.
אמולטור של Realtime Database הוא חלק מ-Local Emulator Suite, שמאפשר לאפליקציה שלכם לקיים אינטראקציה עם התוכן וההגדרות של מסד הנתונים המדומה, וגם עם משאבי הפרויקט המדומים (פונקציות, מסדי נתונים אחרים וכללי אבטחה).
כדי להשתמש באמולטור של Realtime Database, צריך לבצע כמה שלבים פשוטים:
- מוסיפים שורת קוד להגדרת הבדיקה של האפליקציה כדי להתחבר לאמולטור.
- מפעילים את הפקודה
firebase emulators:startמהשורש של ספריית הפרויקט המקומית. - מבצעים קריאות מקוד אב הטיפוס של האפליקציה באמצעות ערכת 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() כדי לצפות באירועים. אפשר להשתמש באירוע הזה כדי לקרוא תמונות מצב סטטיות של התוכן בנתיב נתון, כפי שהיו בזמן האירוע. השיטה הזו מופעלת פעם אחת כשהמאזין מצורף, ושוב בכל פעם שהנתונים משתנים, כולל הצאצאים. הקריאה החוזרת (callback) של האירוע מעבירה תמונת מצב שמכילה את כל הנתונים במיקום הזה, כולל נתוני הצאצאים. אם אין נתונים, תמונת המצב תחזיר את הערך 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 נועד לנהל אינטראקציות עם שרתי מסדי נתונים, גם כשהאפליקציה אונליין וגם כשהיא אופליין.
באופן כללי, כדאי להשתמש בשיטות של אירועי ערך שמתוארות למעלה כדי לקרוא נתונים ולקבל עדכונים על הנתונים מה-backend. שיטות ההאזנה מפחיתות את השימוש והחיוב, והן מותאמות כדי לספק למשתמשים את החוויה הטובה ביותר כשהם עוברים בין מצב אונליין למצב אופליין.
אם אתם צריכים את הנתונים רק פעם אחת, אתם יכולים להשתמש ב-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'; // ... });
עדכון או מחיקה של נתונים
עדכון שדות ספציפיים
כדי לכתוב בו-זמנית לצאצאים ספציפיים של צומת בלי להחליף צמתים אחרים של צאצאים, משתמשים ב-method 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) אופציונלית להשלמה, שנקראת כשהכתיבה בוצעה במסד הנתונים. אם הקריאה נכשלה, מועבר לקריאה החוזרת אובייקט שגיאה שמציין את הסיבה לכישלון.
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).
שמירת נתונים כעסקאות
כשעובדים עם נתונים שעלולים להיפגם כתוצאה משינויים בו-זמניים, כמו מונים מצטברים, אפשר להשתמש בפעולת טרנזקציה. אתם יכולים לתת לפעולה הזו פונקציית עדכון וקריאה חוזרת (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 מסנכרן את הנתונים האלה עם שרתי מסד הנתונים המרוחקים ועם לקוחות אחרים על בסיס "המאמץ המרבי".
כתוצאה מכך, כל הכתיבות למסד הנתונים מפעילות אירועים מקומיים באופן מיידי, לפני שנתונים כלשהם נכתבים בשרת. זה אומר שהאפליקציה תמשיך להגיב בלי קשר לזמן האחזור או לקישוריות של הרשת.
אחרי שהקישוריות נוצרת מחדש, האפליקציה מקבלת את קבוצת האירועים המתאימה כדי שהלקוח יסתנכרן עם מצב השרת הנוכחי, בלי שתצטרכו לכתוב קוד בהתאמה אישית.
נדבר עוד על התנהגות אופליין במידע נוסף על יכולות אונליין ואופליין.