שמירת נתונים

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

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

הוספת קריאה חוזרת (callback) כהשלמה

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

שמירת רשימות של נתונים

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

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