שמירת נתונים

במסמך הזה מפורטות ארבע השיטות לכתיבה של נתונים ב-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')
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)
}

הפעולה הזו תעדכן את הנתונים של גרציה כך שיכללו את הכינוי שלה. אם הייתם משתמשים ב-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'
})
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"
    }
  }
}

הוספת שיחה חוזרת בסיום הטיפול

ב-Node.js וב-Java Admin SDKs, אם רוצים לדעת מתי הנתונים הוגדרו, אפשר להוסיף קריאה חוזרת (callback) להשלמה. גם שיטות ההגדרה וגם שיטות העדכון ב-SDKs האלה מקבלות קריאה חוזרת אופציונלית בסיום, שנקראת אחרי שהכתיבה בוצעה במסד הנתונים. אם הקריאה נכשלה מסיבה כלשהי, פונקציית ה-callback תקבל אובייקט שגיאה שמציין את הסיבה לכישלון. ב-SDK של Admin ל-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'
})
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(&currentValue); 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 יש שפת אבטחה שמאפשרת להגדיר לאילו משתמשים תהיה גישה לקריאה ולכתיבה בצמתים שונים של הנתונים. מידע נוסף זמין במאמר אבטחת הנתונים.