במסמך הזה מוסברות ארבע השיטות לכתיבת נתונים אל Firebase Realtime Database: הגדרה, עדכון, שליחה ותמיכה בעסקאות.
דרכים לחסוך בנתונים
הגדרה | כתיבה או החלפה של נתונים בנתיב מוגדר, כמו messages/users/<username> |
עדכון | עדכון חלק מהמפתחות של נתיב מוגדר בלי להחליף את כל הנתונים |
דחיפת הודעות | הוספה לרשימת נתונים במסד הנתונים. בכל פעם שמעבירים צומת חדש לרשימה, מסד הנתונים יוצר מפתח ייחודי, כמו messages/users/<unique-user-id>/<username> |
טרנזקציה | שימוש בטרנזקציות כשעובדים עם נתונים מורכבים שעלולים להיפגם בגלל עדכונים בו-זמניים |
שמירת נתונים
פעולת הכתיבה הבסיסית למסד נתונים היא פעולת הגדרה ששומרת נתונים חדשים בהפניה למסד הנתונים שצוין, ומחליפה את הנתונים הקיימים בנתיב הזה. כדי להבין את ההגדרה, נבנה אפליקציית בלוג פשוטה. הנתונים של האפליקציה מאוחסנים בהפניה למסד הנתונים הבאה:
Java
final FirebaseDatabase database = FirebaseDatabase.getInstance(); DatabaseReference ref = database.getReference("server/saving-data/fireblog");
Node.js
// Import Admin SDK const { getDatabase } = require('firebase-admin/database'); // Get a database reference to our blog const db = getDatabase(); const ref = db.ref('server/saving-data/fireblog');
Python
# Import database module. from firebase_admin import db # Get a database reference to our blog. ref = db.reference('server/saving-data/fireblog')
Go
// Create a database client from App. client, err := app.Database(ctx) if err != nil { log.Fatalln("Error initializing database client:", err) } // Get a database reference to our blog. ref := client.NewRef("server/saving-data/fireblog")
נתחיל בשמירת נתוני משתמשים. נשמור כל משתמש באמצעות שם משתמש ייחודי, וגם נשמור את השם המלא ותאריך הלידה שלו. לכל משתמש יהיה שם משתמש ייחודי, ולכן הגיוני להשתמש כאן בשיטת set במקום בשיטת push, כי המפתח כבר קיים ואין צורך ליצור אותו.
קודם יוצרים הפניה למסד הנתונים של נתוני המשתמשים. לאחר מכן משתמשים ב-set()
/ setValue()
כדי לשמור אובייקט משתמש במסד הנתונים עם שם המשתמש, השם המלא ותאריך הלידה של המשתמש. אפשר להעביר מחרוזת, מספר, ערך בוליאני, null
, מערך או כל אובייקט JSON. העברת הערך null
תגרום להסרת הנתונים במיקום שצוין. במקרה כזה, מעבירים לו אובייקט:
Java
public static class User { public String date_of_birth; public String full_name; public String nickname; public User(String dateOfBirth, String fullName) { // ... } public User(String dateOfBirth, String fullName, String nickname) { // ... } } DatabaseReference usersRef = ref.child("users"); Map<String, User> users = new HashMap<>(); users.put("alanisawesome", new User("June 23, 1912", "Alan Turing")); users.put("gracehop", new User("December 9, 1906", "Grace Hopper")); usersRef.setValueAsync(users);
Node.js
const usersRef = ref.child('users'); usersRef.set({ alanisawesome: { date_of_birth: 'June 23, 1912', full_name: 'Alan Turing' }, gracehop: { date_of_birth: 'December 9, 1906', full_name: 'Grace Hopper' } });
Python
users_ref = ref.child('users') users_ref.set({ 'alanisawesome': { 'date_of_birth': 'June 23, 1912', 'full_name': 'Alan Turing' }, 'gracehop': { 'date_of_birth': 'December 9, 1906', 'full_name': 'Grace Hopper' } })
Go
// User is a json-serializable type. type User struct { DateOfBirth string `json:"date_of_birth,omitempty"` FullName string `json:"full_name,omitempty"` Nickname string `json:"nickname,omitempty"` } usersRef := ref.Child("users") err := usersRef.Set(ctx, map[string]*User{ "alanisawesome": { DateOfBirth: "June 23, 1912", FullName: "Alan Turing", }, "gracehop": { DateOfBirth: "December 9, 1906", FullName: "Grace Hopper", }, }) if err != nil { log.Fatalln("Error setting value:", err) }
כששומרים אובייקט JSON במסד הנתונים, המאפיינים של האובייקט ממופים אוטומטית למיקומי צאצא במסד הנתונים באופן היררכי. עכשיו, אם עוברים לכתובת ה-URL https://docs-examples.firebaseio.com/server/saving-data/fireblog/users/alanisawesome/full_name, יופיע הערך Alan Turing. אפשר גם לשמור נתונים ישירות במיקום צאצא:
Java
usersRef.child("alanisawesome").setValueAsync(new User("June 23, 1912", "Alan Turing")); usersRef.child("gracehop").setValueAsync(new User("December 9, 1906", "Grace Hopper"));
Node.js
const usersRef = ref.child('users'); usersRef.child('alanisawesome').set({ date_of_birth: 'June 23, 1912', full_name: 'Alan Turing' }); usersRef.child('gracehop').set({ date_of_birth: 'December 9, 1906', full_name: 'Grace Hopper' });
Python
users_ref.child('alanisawesome').set({ 'date_of_birth': 'June 23, 1912', 'full_name': 'Alan Turing' }) users_ref.child('gracehop').set({ 'date_of_birth': 'December 9, 1906', 'full_name': 'Grace Hopper' })
Go
if err := usersRef.Child("alanisawesome").Set(ctx, &User{ DateOfBirth: "June 23, 1912", FullName: "Alan Turing", }); err != nil { log.Fatalln("Error setting value:", err) } if err := usersRef.Child("gracehop").Set(ctx, &User{ DateOfBirth: "December 9, 1906", FullName: "Grace Hopper", }); err != nil { log.Fatalln("Error setting value:", err) }
שתי הדוגמאות שלמעלה – כתיבת שני הערכים בו-זמנית כאובייקט וכתיבתם בנפרד למיקומי צאצא – יובילו לשמירת אותם נתונים במסד הנתונים:
{ "users": { "alanisawesome": { "date_of_birth": "June 23, 1912", "full_name": "Alan Turing" }, "gracehop": { "date_of_birth": "December 9, 1906", "full_name": "Grace Hopper" } } }
בדוגמה הראשונה יופעל רק אירוע אחד בלקוחות שצופים בנתונים, ואילו בדוגמה השנייה יופעלו שני אירועים. חשוב לציין שאם כבר היו נתונים ב-usersRef
, הגישה הראשונה תדרוס אותם, אבל הגישה השנייה תשנה רק את הערך של כל צומת צאצא נפרד, ותשאיר את שאר צאצאי usersRef
ללא שינוי.
עדכון נתונים שמורים
אם רוצים לכתוב לכמה צאצאים של מיקום במסד נתונים בו-זמנית בלי להחליף צמתי צאצאים אחרים, אפשר להשתמש בשיטת העדכון כמו שמוצג בהמשך:
Java
DatabaseReference hopperRef = usersRef.child("gracehop"); Map<String, Object> hopperUpdates = new HashMap<>(); hopperUpdates.put("nickname", "Amazing Grace"); hopperRef.updateChildrenAsync(hopperUpdates);
Node.js
const usersRef = ref.child('users'); const hopperRef = usersRef.child('gracehop'); hopperRef.update({ 'nickname': 'Amazing Grace' });
Python
hopper_ref = users_ref.child('gracehop') hopper_ref.update({ 'nickname': 'Amazing Grace' })
Go
hopperRef := usersRef.Child("gracehop") if err := hopperRef.Update(ctx, map[string]interface{}{ "nickname": "Amazing Grace", }); err != nil { log.Fatalln("Error updating child:", err) }
הפעולה הזו תעדכן את הנתונים של Grace כך שיכללו את הכינוי שלה. אם הייתם משתמשים בפקודה set במקום בפקודה update, המערכת הייתה מוחקת את full_name
ואת date_of_birth
מ-hopperRef
.
Firebase Realtime Database תומך גם בעדכונים של כמה נתיבים. המשמעות היא שעכשיו אפשר לעדכן ערכים בכמה מיקומים במסד הנתונים בו-זמנית. זו תכונה מתקדמת שעוזרת לכם לבצע דה-נורמליזציה של הנתונים. בעזרת עדכונים מרובי-נתיבים, אפשר להוסיף כינויים גם לגרייס וגם לאלן בו-זמנית:
Java
Map<String, Object> userUpdates = new HashMap<>(); userUpdates.put("alanisawesome/nickname", "Alan The Machine"); userUpdates.put("gracehop/nickname", "Amazing Grace"); usersRef.updateChildrenAsync(userUpdates);
Node.js
const usersRef = ref.child('users'); usersRef.update({ 'alanisawesome/nickname': 'Alan The Machine', 'gracehop/nickname': 'Amazing Grace' });
Python
users_ref.update({ 'alanisawesome/nickname': 'Alan The Machine', 'gracehop/nickname': 'Amazing Grace' })
Go
if err := usersRef.Update(ctx, map[string]interface{}{ "alanisawesome/nickname": "Alan The Machine", "gracehop/nickname": "Amazing Grace", }); err != nil { log.Fatalln("Error updating children:", err) }
אחרי העדכון הזה, הכינויים של אלן וגרייס נוספו:
{ "users": { "alanisawesome": { "date_of_birth": "June 23, 1912", "full_name": "Alan Turing", "nickname": "Alan The Machine" }, "gracehop": { "date_of_birth": "December 9, 1906", "full_name": "Grace Hopper", "nickname": "Amazing Grace" } } }
שימו לב: אם תנסו לעדכן אובייקטים על ידי כתיבת אובייקטים עם הנתיבים הכלולים, תקבלו התנהגות שונה. בואו נראה מה קורה אם מנסים לעדכן את גרייס ואלן בדרך הזו:
Java
Map<String, Object> userNicknameUpdates = new HashMap<>(); userNicknameUpdates.put("alanisawesome", new User(null, null, "Alan The Machine")); userNicknameUpdates.put("gracehop", new User(null, null, "Amazing Grace")); usersRef.updateChildrenAsync(userNicknameUpdates);
Node.js
const usersRef = ref.child('users'); usersRef.update({ 'alanisawesome': { 'nickname': 'Alan The Machine' }, 'gracehop': { 'nickname': 'Amazing Grace' } });
Python
users_ref.update({ 'alanisawesome': { 'nickname': 'Alan The Machine' }, 'gracehop': { 'nickname': 'Amazing Grace' } })
Go
if err := usersRef.Update(ctx, map[string]interface{}{ "alanisawesome": &User{Nickname: "Alan The Machine"}, "gracehop": &User{Nickname: "Amazing Grace"}, }); err != nil { log.Fatalln("Error updating children:", err) }
התוצאה היא התנהגות שונה, כלומר דריסת הצומת /users
כולו:
{ "users": { "alanisawesome": { "nickname": "Alan The Machine" }, "gracehop": { "nickname": "Amazing Grace" } } }
הוספת התקשרות חזרה לאחר השלמה
ב-Admin SDKs של Node.js ו-Java, אם רוצים לדעת מתי הנתונים נשמרו, אפשר להוסיף קריאה חוזרת להשלמה. שתי השיטות להגדרת נתונים ולעדכון שלהם בערכות ה-SDK האלה מקבלות קריאה חוזרת אופציונלית להשלמה, שמופעלת כשהכתיבה מתבצעת במסד הנתונים. אם השיחה נכשלה מסיבה כלשהי, הפונקציה לטיפול בקריאה חוזרת מקבלת אובייקט שגיאה שמציין למה היא נכשלה. ב-Python וב-Go Admin SDKs, כל שיטות הכתיבה חוסמות. כלומר, שיטות הכתיבה לא מחזירות ערך עד שהכתיבות מועברות למסד הנתונים.
Java
DatabaseReference dataRef = ref.child("data"); dataRef.setValue("I'm writing data", new DatabaseReference.CompletionListener() { @Override public void onComplete(DatabaseError databaseError, DatabaseReference databaseReference) { if (databaseError != null) { System.out.println("Data could not be saved " + databaseError.getMessage()); } else { System.out.println("Data saved successfully."); } } });
Node.js
dataRef.set('I\'m writing data', (error) => { if (error) { console.log('Data could not be saved.' + error); } else { console.log('Data saved successfully.'); } });
שמירת רשימות של נתונים
כשיוצרים רשימות של נתונים, חשוב לזכור שרוב האפליקציות מיועדות לשימוש של כמה משתמשים, ולשנות את מבנה הרשימה בהתאם. נרחיב את הדוגמה שלמעלה ונוסיף פוסטים בבלוג לאפליקציה. יכול להיות שהדבר הראשון שיעלה לכם בראש הוא להשתמש ב-set כדי לאחסן צאצאים עם אינדקסים של מספרים שלמים עם הגדלה אוטומטית, כמו בדוגמה הבאה:
// NOT RECOMMENDED - use push() instead! { "posts": { "0": { "author": "gracehop", "title": "Announcing COBOL, a New Programming Language" }, "1": { "author": "alanisawesome", "title": "The Turing Machine" } } }
אם משתמש מוסיף פוסט חדש, הוא יישמר כ-/posts/2
. השיטה הזו תעבוד אם רק מחבר אחד יוסיף פוסטים, אבל באפליקציית הבלוגים השיתופית שלכם יכול להיות שמשתמשים רבים יוסיפו פוסטים בו-זמנית. אם שני מחברים כותבים ל-/posts/2
בו-זמנית, אחד מהפוסטים יימחק על ידי השני.
כדי לפתור את הבעיה הזו, לקוחות Firebase מספקים פונקציה push()
שיוצרת מפתח ייחודי לכל צאצא חדש. שימוש במפתחות ייחודיים של צאצאים מאפשר לכמה לקוחות להוסיף צאצאים לאותו מיקום בו-זמנית, בלי לדאוג לגבי התנגשויות בכתיבה.
Java
public static class Post { public String author; public String title; public Post(String author, String title) { // ... } } DatabaseReference postsRef = ref.child("posts"); DatabaseReference newPostRef = postsRef.push(); newPostRef.setValueAsync(new Post("gracehop", "Announcing COBOL, a New Programming Language")); // We can also chain the two calls together postsRef.push().setValueAsync(new Post("alanisawesome", "The Turing Machine"));
Node.js
const newPostRef = postsRef.push(); newPostRef.set({ author: 'gracehop', title: 'Announcing COBOL, a New Programming Language' }); // we can also chain the two calls together postsRef.push().set({ author: 'alanisawesome', title: 'The Turing Machine' });
Python
posts_ref = ref.child('posts') new_post_ref = posts_ref.push() new_post_ref.set({ 'author': 'gracehop', 'title': 'Announcing COBOL, a New Programming Language' }) # We can also chain the two calls together posts_ref.push().set({ 'author': 'alanisawesome', 'title': 'The Turing Machine' })
Go
// Post is a json-serializable type. type Post struct { Author string `json:"author,omitempty"` Title string `json:"title,omitempty"` } postsRef := ref.Child("posts") newPostRef, err := postsRef.Push(ctx, nil) if err != nil { log.Fatalln("Error pushing child node:", err) } if err := newPostRef.Set(ctx, &Post{ Author: "gracehop", Title: "Announcing COBOL, a New Programming Language", }); err != nil { log.Fatalln("Error setting value:", err) } // We can also chain the two calls together if _, err := postsRef.Push(ctx, &Post{ Author: "alanisawesome", Title: "The Turing Machine", }); err != nil { log.Fatalln("Error pushing child node:", err) }
המפתח הייחודי מבוסס על חותמת זמן, כך שפריטים ברשימה יסודרו אוטומטית בסדר כרונולוגי. מכיוון שמערכת Firebase יוצרת מפתח ייחודי לכל פוסט בבלוג, לא יתרחשו התנגשויות בכתיבה אם כמה משתמשים יוסיפו פוסט בו-זמנית. הנתונים במסד הנתונים ייראו עכשיו כך:
{ "posts": { "-JRHTHaIs-jNPLXOQivY": { "author": "gracehop", "title": "Announcing COBOL, a New Programming Language" }, "-JRHTHaKuITFIhnj02kE": { "author": "alanisawesome", "title": "The Turing Machine" } } }
ב-JavaScript, Python ו-Go, התבנית של קריאה ל-push()
ואז קריאה מיידית ל-set()
כל כך נפוצה, ש-Firebase SDK מאפשר לשלב אותן על ידי העברת הנתונים להגדרה ישירות ל-push()
באופן הבא:
Java
// No Java equivalent
Node.js
// This is equivalent to the calls to push().set(...) above postsRef.push({ author: 'gracehop', title: 'Announcing COBOL, a New Programming Language' });;
Python
# This is equivalent to the calls to push().set(...) above posts_ref.push({ 'author': 'gracehop', 'title': 'Announcing COBOL, a New Programming Language' })
Go
if _, err := postsRef.Push(ctx, &Post{ Author: "gracehop", Title: "Announcing COBOL, a New Programming Language", }); err != nil { log.Fatalln("Error pushing child node:", err) }
קבלת המפתח הייחודי שנוצר על ידי push()
התקשרות אל push()
תחזיר הפניה לנתיב הנתונים החדש, שבו אפשר להשתמש כדי לקבל את המפתח או להגדיר נתונים. הקוד הבא יחזיר את אותם נתונים כמו בדוגמה שלמעלה, אבל עכשיו תהיה לנו גישה למפתח הייחודי שנוצר:
Java
// Generate a reference to a new location and add some data using push() DatabaseReference pushedPostRef = postsRef.push(); // Get the unique ID generated by a push() String postId = pushedPostRef.getKey();
Node.js
// Generate a reference to a new location and add some data using push() const newPostRef = postsRef.push(); // Get the unique key generated by push() const postId = newPostRef.key;
Python
# Generate a reference to a new location and add some data using push() new_post_ref = posts_ref.push() # Get the unique key generated by push() post_id = new_post_ref.key
Go
// Generate a reference to a new location and add some data using Push() newPostRef, err := postsRef.Push(ctx, nil) if err != nil { log.Fatalln("Error pushing child node:", err) } // Get the unique key generated by Push() postID := newPostRef.Key
כפי שאפשר לראות, אפשר לקבל את הערך של המפתח הייחודי מההפניה ל-push()
.
בקטע הבא, אחזור נתונים, נלמד איך לקרוא את הנתונים האלה ממסד נתונים של Firebase.
שמירת נתונים טרנזקציוניים
כשעובדים עם נתונים מורכבים שעלולים להיפגם בגלל שינויים מקבילים, כמו מוני אינקרמנטליים, ה-SDK מספק פעולת טרנזקציה.
ב-Java וב-Node.js, צריך לספק לפעולת הטרנזקציה שתי פונקציות קריאה חוזרת: פונקציית עדכון ופונקציית קריאה חוזרת אופציונלית להשלמה. ב-Python וב-Go, פעולת הטרנזקציה חוסמת, ולכן היא מקבלת רק את פונקציית העדכון.
פונקציית העדכון מקבלת את המצב הנוכחי של הנתונים כארגומנט, והיא צריכה להחזיר את המצב החדש הרצוי שרוצים לכתוב. לדוגמה, אם רוצים להגדיל את מספר הלייקים לפוסט ספציפי בבלוג, כותבים טרנזקציה כמו הבאה:
Java
DatabaseReference upvotesRef = ref.child("server/saving-data/fireblog/posts/-JRHTHaIs-jNPLXOQivY/upvotes"); upvotesRef.runTransaction(new Transaction.Handler() { @Override public Transaction.Result doTransaction(MutableData mutableData) { Integer currentValue = mutableData.getValue(Integer.class); if (currentValue == null) { mutableData.setValue(1); } else { mutableData.setValue(currentValue + 1); } return Transaction.success(mutableData); } @Override public void onComplete( DatabaseError databaseError, boolean committed, DataSnapshot dataSnapshot) { System.out.println("Transaction completed"); } });
Node.js
const upvotesRef = db.ref('server/saving-data/fireblog/posts/-JRHTHaIs-jNPLXOQivY/upvotes'); upvotesRef.transaction((current_value) => { return (current_value || 0) + 1; });
Python
def increment_votes(current_value): return current_value + 1 if current_value else 1 upvotes_ref = db.reference('server/saving-data/fireblog/posts/-JRHTHaIs-jNPLXOQivY/upvotes') try: new_vote_count = upvotes_ref.transaction(increment_votes) print('Transaction completed') except db.TransactionAbortedError: print('Transaction failed to commit')
Go
fn := func(t db.TransactionNode) (interface{}, error) { var currentValue int if err := t.Unmarshal(¤tValue); err != nil { return nil, err } return currentValue + 1, nil } ref := client.NewRef("server/saving-data/fireblog/posts/-JRHTHaIs-jNPLXOQivY/upvotes") if err := ref.Transaction(ctx, fn); err != nil { log.Fatalln("Transaction failed to commit:", err) }
בדוגמה שלמעלה, המערכת בודקת אם הערך של המונה הוא null
או אם הוא עדיין לא הוגדל,
כי אפשר להפעיל את הפונקציה transactions עם הערך null
אם לא נכתב ערך ברירת מחדל.
אם הקוד שלמעלה היה מופעל ללא פונקציית טרנזקציה ושני לקוחות ניסו להגדיל אותו בו-זמנית, שניהם היו כותבים 1
כערך החדש, וכך היה מתקבל גידול אחד במקום שניים.
קישוריות רשת וכתיבה במצב אופליין
לקוחות Firebase Node.js ו-Java שומרים גרסה פנימית משלהם של כל נתון פעיל. כשנתונים נכתבים, הם נכתבים קודם בגרסה המקומית הזו. לאחר מכן הלקוח מסנכרן את הנתונים האלה עם מסד הנתונים ועם לקוחות אחרים על בסיס 'השתדלות מרבית'.
כתוצאה מכך, כל פעולת כתיבה למסד הנתונים תפעיל אירועים מקומיים באופן מיידי, עוד לפני שנתונים נכתבו למסד הנתונים. המשמעות היא שכשתכתבו אפליקציה באמצעות Firebase, האפליקציה שלכם תמשיך להגיב בלי קשר לזמן האחזור ברשת או לחיבור לאינטרנט.
אחרי שהקישוריות מתחדשת, אנחנו מקבלים את קבוצת האירועים המתאימה כדי שהלקוח יתעדכן במצב השרת הנוכחי, בלי שנצטרך לכתוב קוד מותאם אישית.
אבטחת הנתונים
ל-Firebase Realtime Database יש שפת אבטחה שמאפשרת להגדיר לאילו משתמשים יש הרשאת קריאה וכתיבה לצמתים שונים של הנתונים. מידע נוסף זמין במאמר אבטחת הנתונים.