במסמך הזה מפורטות ארבע השיטות לכתיבת נתונים ב-Firebase Realtime Database: הגדרה, עדכון, דחיפה ותמיכה בטרנזקציות.
דרכים לשמירת נתונים
| הגדרה | כתיבה או החלפה של נתונים בנתיב מוגדר, כמו messages/users/<username> |
| עדכון | לעדכן חלק מהמפתחות בנתיב מוגדר בלי להחליף את כל הנתונים |
| דחיפת הודעות | להוסיף לרשימת נתונים במסד הנתונים. בכל פעם שמוסיפים צומת חדש לרשימה, מסד הנתונים יוצר מפתח ייחודי, כמו messages/users/<unique-user-id>/<username> |
| טרנזקציה | שימוש בטרנזקציות כשעובדים עם נתונים מורכבים שעלולים להיפגם בגלל עדכונים בו-זמניים |
שמירת נתונים
פעולת הכתיבה הבסיסית במסד הנתונים היא קבוצה ששומרת נתונים חדשים בהפניה שצוינה למסד הנתונים, ומחליפה את כל הנתונים הקיימים בנתיב הזה. כדי להבין את set, נבנה אפליקציית בלוגים פשוטה. הנתונים של האפליקציה מאוחסנים בהפניה הזו למסד הנתונים:
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')
המשך
// 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 method במקום ב-push method, כי כבר יש לכם את המפתח ואין צורך ליצור אחד.
קודם כל, יוצרים הפניה למסד הנתונים של נתוני המשתמשים. ואז משתמשים ב-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&qu<ot;); MapSt>ring, User users = n<>ew 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' } })
המשך
// 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' })
המשך
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&q<uot;); MapStri>ng, Object hopperUpdates = n<>ew 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' })
המשך
hopperRef := usersRef.Child("gracehop") if err := hopperRef.Update(ctx, map[string]interface{}{ "nickname": "Amazing Grace", }); err != nil { log.Fatalln("Error updating child:", err) }
הפעולה הזו תעדכן את הנתונים של חן ותכלול את הכינוי שלה. אם הייתם משתמשים כאן ב-set במקום ב-update,
הפעולה הייתה מוחקת גם את full_name וגם את date_of_birth מה-hopperRef.
ה-Firebase Realtime Database תומך גם בעדכונים בכמה נתיבים. כלומר, עכשיו אפשר לעדכן ערכים בכמה מיקומים במסד הנתונים בו-זמנית, תכונה מתקדמת שמאפשרת לבטל את הנורמליזציה של הנתונים. באמצעות עדכונים בכמה נתיבים, אפשר להוסיף כינויים גם ל-Grace וגם ל-Alan בו-זמנית:
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' })
המשך
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' } })
המשך
if err := usersRef.Update(ctx, map[string]interface{}{ "alanisaweso&me": 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"
}
}
}הוספת קריאה חוזרת (callback) להשלמה
ב-SDK לאדמינים ב-Node.js וב-Java, אם אתם רוצים לדעת מתי הנתונים שלכם נשמרו, אתם יכולים להוסיף קריאה חוזרת (callback) להשלמה. שתי השיטות set ו-update בערכות ה-SDK האלה מקבלות קריאה חוזרת (callback) אופציונלית להשלמה, שנקראת כשהכתיבה בוצעה למסד הנתונים. אם הקריאה נכשלה מסיבה כלשהי, אובייקט שגיאה מועבר לקריאה החוזרת (callback) ומציין את הסיבה לכישלון. ב-SDK לאדמינים ב-Python וב-Go, כל שיטות הכתיבה חוסמות. כלומר, שיטות הכתיבה לא מחזירות ערך עד שהכתיבה מתבצעת במסד הנתונים.
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' })
המשך
// 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 equivalentNode.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' })
המשך
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
המשך
// 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')
המשך
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 או אם הוא עדיין לא הוגדל, כי אפשר לקרוא לעסקאות עם null אם לא נכתב ערך ברירת מחדל.
אם הקוד שלמעלה היה פועל בלי פונקציית טרנזקציה ושני לקוחות היו מנסים להגדיל אותו בו-זמנית, שניהם היו כותבים את הערך החדש 1, וכתוצאה מכך הייתה מתבצעת הגדלה אחת במקום שתיים.
קישוריות רשת וכתיבה אופליין
לקוחות Firebase Node.js ו-Java שומרים גרסה פנימית משלהם של כל הנתונים הפעילים. כשנתונים נכתבים, הם נכתבים קודם כל לגרסה המקומית הזו. לאחר מכן, הלקוח מסנכרן את הנתונים האלה עם מסד הנתונים ועם לקוחות אחרים על בסיס "המאמץ המרבי".
כתוצאה מכך, כל הכתיבות למסד הנתונים יפעילו אירועים מקומיים באופן מיידי, עוד לפני שנתונים כלשהם נכתבו במסד הנתונים. כלומר, כשאתם כותבים אפליקציה באמצעות Firebase, האפליקציה תמשיך להגיב בלי קשר לזמן האחזור ברשת או לחיבור לאינטרנט.
אחרי שהקישוריות תתחדש, נקבל את קבוצת האירועים המתאימה כדי שהלקוח "יתעדכן" במצב השרת הנוכחי, בלי שנצטרך לכתוב קוד בהתאמה אישית.
אבטחת נתונים
ל-Firebase Realtime Database יש שפת אבטחה שמאפשרת להגדיר לאילו משתמשים יש גישת קריאה וכתיבה לצמתים שונים של הנתונים. מידע נוסף זמין במאמר אבטחת הנתונים.