אחזור נתונים

במסמך הזה מוסבר איך לאחזר נתונים ממסד נתונים, איך הנתונים מסודרים ואיך לבצע שאילתות פשוטות על הנתונים. אחזור נתונים ב-Admin SDK מיושם בצורה שונה מעט בשפות תכנות שונות.

  1. מאזינים אסינכרוניים: נתונים שמאוחסנים ב-Firebase Realtime Database מאוחזרים על ידי צירוף מאזין אסינכרוני להפניה למסד נתונים. המאזין מופעל פעם אחת עבור המצב הראשוני של הנתונים, ושוב בכל פעם שהנתונים משתנים. יכול להיות שמאזין לאירועים יקבל כמה סוגים שונים של אירועים. אפשר לאחזר נתונים בשיטה הזו באמצעות Java, ‏ Node.js ו-Python Admin SDKs.
  2. חסימת קריאות: נתונים שמאוחסנים ב-Firebase Realtime Database מאוחזרים על ידי הפעלה של שיטת חסימה בהפניה למסד נתונים, שמחזירה את הנתונים שמאוחסנים בהפניה. כל קריאה לשיטה היא פעולה חד-פעמית. כלומר, ערכת ה-SDK לא רושמת קריאות חוזרות (callback) שמאזינות לעדכוני נתונים עתידיים. המודל הזה של אחזור נתונים נתמך ב-Python וב-Go Admin SDKs.

תחילת העבודה

נחזור לדוגמה של הבלוג מהמאמר הקודם כדי להבין איך לקרוא נתונים ממסד נתונים של Firebase. נזכיר שפוסטים בבלוג באפליקציה לדוגמה מאוחסנים בכתובת ה-URL של מסד הנתונים https://docs-examples.firebaseio.com/server/saving-data/fireblog/posts.json. כדי לקרוא את נתוני הפוסטים, אפשר:

Java
public static class Post {

  public String author;
  public String title;

  public Post(String author, String title) {
    // ...
  }

}

// Get a reference to our posts
final FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference ref = database.getReference("server/saving-data/fireblog/posts");

// Attach a listener to read the data at our posts reference
ref.addValueEventListener(new ValueEventListener() {
  @Override
  public void onDataChange(DataSnapshot dataSnapshot) {
    Post post = dataSnapshot.getValue(Post.class);
    System.out.println(post);
  }

  @Override
  public void onCancelled(DatabaseError databaseError) {
    System.out.println("The read failed: " + databaseError.getCode());
  }
});
Node.js
// Get a database reference to our posts
const db = getDatabase();
const ref = db.ref('server/saving-data/fireblog/posts');

// Attach an asynchronous callback to read the data at our posts reference
ref.on('value', (snapshot) => {
  console.log(snapshot.val());
}, (errorObject) => {
  console.log('The read failed: ' + errorObject.name);
}); 
Python
# Import database module.
from firebase_admin import db

# Get a database reference to our posts
ref = db.reference('server/saving-data/fireblog/posts')

# Read the data at the posts reference (this is a blocking operation)
print(ref.get())
Go
// Post is a json-serializable type.
type Post struct {
	Author string `json:"author,omitempty"`
	Title  string `json:"title,omitempty"`
}

// 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 posts
ref := client.NewRef("server/saving-data/fireblog/posts")

// Read the data at the posts reference (this is a blocking operation)
var post Post
if err := ref.Get(ctx, &post); err != nil {
	log.Fatalln("Error reading value:", err)
}

אם מריצים את הקוד שלמעלה, רואים אובייקט שמכיל את כל הפוסטים שלכם שמתועדים במסוף. במקרה של Node.js ו-Java, הפונקציה של ה-listener נקראת בכל פעם שמוסיפים נתונים חדשים להפניה למסד הנתונים, ואין צורך לכתוב קוד נוסף כדי שזה יקרה.

ב-Java וב-Node.js, פונקציית הקריאה החוזרת מקבלת DataSnapshot, שהוא קובץ snapshot של הנתונים. תמונת מצב היא תמונה של הנתונים בהפניה מסוימת למסד נתונים בנקודת זמן מסוימת. קריאה ל-val() / getValue() בתמונת מצב מחזירה ייצוג אובייקט של הנתונים בשפה ספציפית. אם אין נתונים במיקום ההפניה, הערך של התמונה הוא null. השיטה get() ב-Python מחזירה ישירות ייצוג של הנתונים ב-Python. הפונקציה Get() ב-Go מבטלת את ה-marshaling של הנתונים למבנה נתונים נתון.

שימו לב שבחרנו להשתמש בסוג האירוע value בדוגמה שלמעלה. האירוע הזה קורא את כל התוכן של הפניה למסד נתונים של Firebase, גם אם רק חלק אחד מהנתונים השתנה. ‫value הוא אחד מחמשת סוגי האירועים השונים שמופיעים בהמשך, שאפשר להשתמש בהם כדי לקרוא נתונים ממסד הנתונים.

קריאת סוגי אירועים ב-Java וב-Node.js

ערך

האירוע value משמש לקריאת תמונה סטטית של התוכן בנתיב מסוים במסד הנתונים, כפי שהיה בזמן אירוע הקריאה. הוא מופעל פעם אחת עם הנתונים הראשוניים, ועוד פעם בכל פעם שהנתונים משתנים. החזרת האירוע (callback) מעבירה תמונת מצב שמכילה את כל הנתונים במיקום הזה, כולל נתוני צאצא. בדוגמת הקוד שלמעלה, value מחזירה את כל הפוסטים בבלוג באפליקציה. בכל פעם שמוסיפים פוסט חדש בבלוג, פונקציית הקריאה החוזרת מחזירה את כל הפוסטים.

נוסף צאצא

בדרך כלל משתמשים באירוע child_added כשמאחזרים רשימה של פריטים ממסד הנתונים. בניגוד ל-value שמחזירה את כל התוכן במיקום, הפונקציה child_added מופעלת פעם אחת לכל צאצא קיים, ואז שוב בכל פעם שמוסיפים צאצא חדש לנתיב שצוין. הקריאה החוזרת (callback) של האירוע מקבלת תמונת מצב שמכילה את הנתונים של צאצא חדש. לצורך סידור, מועבר גם ארגומנט שני שמכיל את המפתח של הצאצא הקודם.

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

Java
ref.addChildEventListener(new ChildEventListener() {
  @Override
  public void onChildAdded(DataSnapshot dataSnapshot, String prevChildKey) {
    Post newPost = dataSnapshot.getValue(Post.class);
    System.out.println("Author: " + newPost.author);
    System.out.println("Title: " + newPost.title);
    System.out.println("Previous Post ID: " + prevChildKey);
  }

  @Override
  public void onChildChanged(DataSnapshot dataSnapshot, String prevChildKey) {}

  @Override
  public void onChildRemoved(DataSnapshot dataSnapshot) {}

  @Override
  public void onChildMoved(DataSnapshot dataSnapshot, String prevChildKey) {}

  @Override
  public void onCancelled(DatabaseError databaseError) {}
});
Node.js
// Retrieve new posts as they are added to our database
ref.on('child_added', (snapshot, prevChildKey) => {
  const newPost = snapshot.val();
  console.log('Author: ' + newPost.author);
  console.log('Title: ' + newPost.title);
  console.log('Previous Post ID: ' + prevChildKey);
});

בדוגמה הזו, התמונה תכיל אובייקט עם פוסט בבלוג. ה-SDK ממיר את הפוסטים לאובייקטים על ידי אחזור הערך, ולכן יש לכם גישה למאפיינים של מחבר הפוסט ושם הפריט על ידי קריאה ל-author ול-title בהתאמה. יש לכם גם גישה למזהה הפוסט הקודם מהארגומנט השני prevChildKey.

החשבון של הילד או הילדה השתנה

האירוע child_changed מופעל בכל פעם שמשנים צומת צאצא. השינויים האלה כוללים שינויים בצאצאים של צומת הילד. בדרך כלל משתמשים בו בשילוב עם child_added ו-child_removed כדי להגיב לשינויים ברשימת פריטים. תמונת המצב שמועברת לקריאה החוזרת של האירוע מכילה את הנתונים המעודכנים של הרכיב הצאצא.

אפשר להשתמש ב-child_changed כדי לקרוא נתונים מעודכנים על פוסטים בבלוג כשהם נערכים:

Java
ref.addChildEventListener(new ChildEventListener() {
  @Override
  public void onChildAdded(DataSnapshot dataSnapshot, String prevChildKey) {}

  @Override
  public void onChildChanged(DataSnapshot dataSnapshot, String prevChildKey) {
    Post changedPost = dataSnapshot.getValue(Post.class);
    System.out.println("The updated post title is: " + changedPost.title);
  }

  @Override
  public void onChildRemoved(DataSnapshot dataSnapshot) {}

  @Override
  public void onChildMoved(DataSnapshot dataSnapshot, String prevChildKey) {}

  @Override
  public void onCancelled(DatabaseError databaseError) {}
});
Node.js
// Get the data on a post that has changed
ref.on('child_changed', (snapshot) => {
  const changedPost = snapshot.val();
  console.log('The updated post title is ' + changedPost.title);
});

הסרת ילד/ה

האירוע child_removed מופעל כשמסירים צאצא ישיר. בדרך כלל משתמשים בה בשילוב עם child_added ועם child_changed. התמונה שמועברת לקריאה החוזרת של האירוע מכילה את הנתונים של צאצא שהוסר.

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

Java
ref.addChildEventListener(new ChildEventListener() {
  @Override
  public void onChildAdded(DataSnapshot dataSnapshot, String prevChildKey) {}

  @Override
  public void onChildChanged(DataSnapshot dataSnapshot, String prevChildKey) {}

  @Override
  public void onChildRemoved(DataSnapshot dataSnapshot) {
    Post removedPost = dataSnapshot.getValue(Post.class);
    System.out.println("The blog post titled " + removedPost.title + " has been deleted");
  }

  @Override
  public void onChildMoved(DataSnapshot dataSnapshot, String prevChildKey) {}

  @Override
  public void onCancelled(DatabaseError databaseError) {}
});
Node.js
// Get a reference to our posts
const ref = db.ref('server/saving-data/fireblog/posts');

// Get the data on a post that has been removed
ref.on('child_removed', (snapshot) => {
  const deletedPost = snapshot.val();
  console.log('The blog post titled \'' + deletedPost.title + '\' has been deleted');
});

הילד עבר דירה

האירוע child_moved משמש כשעובדים עם נתונים מסודרים, כמו שמוסבר בקטע הבא.

התחייבויות לגבי אירועים

מסד הנתונים של Firebase מספק כמה הבטחות חשובות לגבי אירועים:

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

מכיוון שאירועי ערך מופעלים תמיד אחרונים, הדוגמה הבאה תמיד תפעל:

Java
final AtomicInteger count = new AtomicInteger();

ref.addChildEventListener(new ChildEventListener() {
  @Override
  public void onChildAdded(DataSnapshot dataSnapshot, String prevChildKey) {
    // New child added, increment count
    int newCount = count.incrementAndGet();
    System.out.println("Added " + dataSnapshot.getKey() + ", count is " + newCount);
  }

  // ...
});

// The number of children will always be equal to 'count' since the value of
// the dataSnapshot here will include every child_added event triggered before this point.
ref.addListenerForSingleValueEvent(new ValueEventListener() {
  @Override
  public void onDataChange(DataSnapshot dataSnapshot) {
    long numChildren = dataSnapshot.getChildrenCount();
    System.out.println(count.get() + " == " + numChildren);
  }

  @Override
  public void onCancelled(DatabaseError databaseError) {}
});
Node.js
let count = 0;

ref.on('child_added', (snap) => {
  count++;
  console.log('added:', snap.key);
});

// length will always equal count, since snap.val() will include every child_added event
// triggered before this point
ref.once('value', (snap) => {
  console.log('initial data loaded!', snap.numChildren() === count);
});

ניתוק של שיחות חוזרות

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

Java
// Create and attach listener
ValueEventListener listener = new ValueEventListener() {
    // ...
};
ref.addValueEventListener(listener);

// Remove listener
ref.removeEventListener(listener);
Node.js
ref.off('value', originalCallback);

אם העברתם הקשר של היקף ל-on(), צריך להעביר אותו כשמנתקים את הקריאה החוזרת:

Java
// Not applicable for Java
Node.js
ref.off('value', originalCallback, ctx);

כדי להסיר את כל בקשות ההתקשרות חזרה במיקום מסוים, אפשר לפעול לפי השלבים הבאים:

Java
// No Java equivalent, listeners must be removed individually.
Node.js
// Remove all value callbacks
ref.off('value');

// Remove all callbacks of any type
ref.off();

קריאת נתונים פעם אחת

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

Java
ref.addListenerForSingleValueEvent(new ValueEventListener() {
  @Override
  public void onDataChange(DataSnapshot dataSnapshot) {
    // ...
  }

  @Override
  public void onCancelled(DatabaseError databaseError) {
    // ...
  }
});
Node.js
ref.once('value', (data) => {
  // do some stuff once
});
Python
# Import database module.
from firebase_admin import db

# Get a database reference to our posts
ref = db.reference('server/saving-data/fireblog/posts')

# Read the data at the posts reference (this is a blocking operation)
print(ref.get())
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 posts
ref := client.NewRef("server/saving-data/fireblog/posts")

// Read the data at the posts reference (this is a blocking operation)
var post Post
if err := ref.Get(ctx, &post); err != nil {
	log.Fatalln("Error reading value:", err)
}

שליחת שאילתות לנתונים

בעזרת שאילתות במסד הנתונים של Firebase, אפשר לאחזר נתונים באופן סלקטיבי על סמך מגוון גורמים. כדי ליצור שאילתה במסד הנתונים, מתחילים בציון האופן שבו רוצים שהנתונים יסודרו באמצעות אחת מפונקציות הסדר: orderByChild(),‏ orderByKey() או orderByValue(). אחר כך אפשר לשלב אותן עם חמש שיטות נוספות כדי לבצע שאילתות מורכבות: limitToFirst(),‏ limitToLast(),‏ startAt(),‏ endAt() ו-equalTo().

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

{
  "lambeosaurus": {
    "height" : 2.1,
    "length" : 12.5,
    "weight": 5000
  },
  "stegosaurus": {
    "height" : 4,
    "length" : 9,
    "weight" : 2500
  }
}

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

מיון לפי מפתח צאצא שצוין

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

Java
public static class Dinosaur {

  public int height;
  public int weight;

  public Dinosaur(int height, int weight) {
    // ...
  }

}

final DatabaseReference dinosaursRef = database.getReference("dinosaurs");
dinosaursRef.orderByChild("height").addChildEventListener(new ChildEventListener() {
  @Override
  public void onChildAdded(DataSnapshot dataSnapshot, String prevChildKey) {
    Dinosaur dinosaur = dataSnapshot.getValue(Dinosaur.class);
    System.out.println(dataSnapshot.getKey() + " was " + dinosaur.height + " meters tall.");
  }

  // ...
});
Node.js
const ref = db.ref('dinosaurs');

ref.orderByChild('height').on('child_added', (snapshot) => {
  console.log(snapshot.key + ' was ' + snapshot.val().height + ' meters tall');
});
Python
ref = db.reference('dinosaurs')
snapshot = ref.order_by_child('height').get()
for key, val in snapshot.items():
    print(f'{key} was {val} meters tall')
Go
// Dinosaur is a json-serializable type.
type Dinosaur struct {
	Height int `json:"height"`
	Width  int `json:"width"`
}

ref := client.NewRef("dinosaurs")

results, err := ref.OrderByChild("height").GetOrdered(ctx)
if err != nil {
	log.Fatalln("Error querying database:", err)
}
for _, r := range results {
	var d Dinosaur
	if err := r.Unmarshal(&d); err != nil {
		log.Fatalln("Error unmarshaling result:", err)
	}
	fmt.Printf("%s was %d meteres tall", r.Key(), d.Height)
}

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

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

{
  "lambeosaurus": {
    "dimensions": {
      "height" : 2.1,
      "length" : 12.5,
      "weight": 5000
    }
  },
  "stegosaurus": {
    "dimensions": {
      "height" : 4,
      "length" : 9,
      "weight" : 2500
    }
  }
}

כדי לשלוח שאילתה לגבי הגובה עכשיו, אפשר להשתמש בנתיב המלא לאובייקט במקום במפתח יחיד:

Java
dinosaursRef.orderByChild("dimensions/height").addChildEventListener(new ChildEventListener() {
  @Override
  public void onChildAdded(DataSnapshot dataSnapshot, String prevChildKey) {
    // ...
  }

  // ...
});
Node.js
const ref = db.ref('dinosaurs');
ref.orderByChild('dimensions/height').on('child_added', (snapshot) => {
  console.log(snapshot.key + ' was ' + snapshot.val().height + ' meters tall');
});
Python
ref = db.reference('dinosaurs')
snapshot = ref.order_by_child('dimensions/height').get()
for key, val in snapshot.items():
    print(f'{key} was {val} meters tall')
Go
ref := client.NewRef("dinosaurs")

results, err := ref.OrderByChild("dimensions/height").GetOrdered(ctx)
if err != nil {
	log.Fatalln("Error querying database:", err)
}
for _, r := range results {
	var d Dinosaur
	if err := r.Unmarshal(&d); err != nil {
		log.Fatalln("Error unmarshaling result:", err)
	}
	fmt.Printf("%s was %d meteres tall", r.Key(), d.Height)
}

אפשר להשתמש רק במפתח אחד בכל פעם כדי להגדיר את סדר התוצאות בשאילתות. קריאה ל-orderByChild() מספר פעמים באותה שאילתה תגרום לשגיאה.

סידור לפי מפתח

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

Java
dinosaursRef.orderByKey().addChildEventListener(new ChildEventListener() {
  @Override
  public void onChildAdded(DataSnapshot dataSnapshot, String prevChildKey) {
    System.out.println(dataSnapshot.getKey());
  }

  // ...
});
Node.js
var ref = db.ref('dinosaurs');
ref.orderByKey().on('child_added', (snapshot) => {
  console.log(snapshot.key);
});
Python
ref = db.reference('dinosaurs')
snapshot = ref.order_by_key().get()
print(snapshot)
Go
ref := client.NewRef("dinosaurs")

results, err := ref.OrderByKey().GetOrdered(ctx)
if err != nil {
	log.Fatalln("Error querying database:", err)
}
snapshot := make([]Dinosaur, len(results))
for i, r := range results {
	var d Dinosaur
	if err := r.Unmarshal(&d); err != nil {
		log.Fatalln("Error unmarshaling result:", err)
	}
	snapshot[i] = d
}
fmt.Println(snapshot)

סידור לפי ערך

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

{
  "scores": {
    "bruhathkayosaurus" : 55,
    "lambeosaurus" : 21,
    "linhenykus" : 80,
    "pterodactyl" : 93,
    "stegosaurus" : 5,
    "triceratops" : 22
  }
}

כדי למיין את הדינוזאורים לפי הציון שלהם, אפשר ליצור את השאילתה הבאה:

Java
DatabaseReference scoresRef = database.getReference("scores");
scoresRef.orderByValue().addChildEventListener(new ChildEventListener() {
  @Override
  public void onChildAdded(DataSnapshot dataSnapshot, String prevChildKey) {
    System.out.println("The " + dataSnapshot.getKey() + " score is " + dataSnapshot.getValue());
  }

  // ...
});
Node.js
const scoresRef = db.ref('scores');
scoresRef.orderByValue().on('value', (snapshot) => {
  snapshot.forEach((data) => {
    console.log('The ' + data.key + ' dinosaur\'s score is ' + data.val());
  });
});
Python
ref = db.reference('scores')
snapshot = ref.order_by_value().get()
for key, val in snapshot.items():
    print(f'The {key} dinosaur\'s score is {val}')
Go
ref := client.NewRef("scores")

results, err := ref.OrderByValue().GetOrdered(ctx)
if err != nil {
	log.Fatalln("Error querying database:", err)
}
for _, r := range results {
	var score int
	if err := r.Unmarshal(&score); err != nil {
		log.Fatalln("Error unmarshaling result:", err)
	}
	fmt.Printf("The %s dinosaur's score is %d\n", r.Key(), score)
}

בקטע איך הנתונים מסודרים מוסבר איך ערכים מסוג null, בוליאני, מחרוזת ואובייקט ממוינים כשמשתמשים ב-orderByValue().

שאילתות מורכבות

אחרי שהבנתם איך הנתונים מסודרים, אתם יכולים להשתמש בשיטות limit או range שמתוארות בהמשך כדי לבנות שאילתות מורכבות יותר.

הגבלת שאילתות

השאילתות limitToFirst() ו-limitToLast() משמשות להגדרת מספר מקסימלי של צאצאים לסנכרון עבור קריאה חוזרת נתונה. אם תגדירו מגבלה של 100, תקבלו בהתחלה רק עד 100 אירועים מסוג child_added. אם יש לכם פחות מ-100 הודעות ששמורות במסד הנתונים, יופעל אירוע child_added לכל הודעה. עם זאת, אם יש לכם יותר מ-100 הודעות, תקבלו אירוע child_added רק עבור 100 מההודעות האלה. אם אתם משתמשים ב-limitToFirst(), מוצגות 100 ההודעות הראשונות לפי הסדר, ואם אתם משתמשים ב-limitToLast(), מוצגות 100 ההודעות האחרונות לפי הסדר. כשהפריטים משתנים, תקבלו אירועי child_added לגבי פריטים שנכללים בשאילתה ואירועי child_removed לגבי פריטים שמוסרים ממנה, כך שהמספר הכולל יישאר 100.

בעזרת מסד הנתונים של עובדות על דינוזאורים ו-orderByChild(), אפשר למצוא את שני הדינוזאורים הכי כבדים:

Java
dinosaursRef.orderByChild("weight").limitToLast(2).addChildEventListener(new ChildEventListener() {
  @Override
  public void onChildAdded(DataSnapshot dataSnapshot, String prevChildKey) {
    System.out.println(dataSnapshot.getKey());
  }

  // ...
});
Node.js
const ref = db.ref('dinosaurs');
ref.orderByChild('weight').limitToLast(2).on('child_added', (snapshot) => {
  console.log(snapshot.key);
});
Python
ref = db.reference('dinosaurs')
snapshot = ref.order_by_child('weight').limit_to_last(2).get()
for key in snapshot:
    print(key)
Go
ref := client.NewRef("dinosaurs")

results, err := ref.OrderByChild("weight").LimitToLast(2).GetOrdered(ctx)
if err != nil {
	log.Fatalln("Error querying database:", err)
}
for _, r := range results {
	fmt.Println(r.Key())
}

הפונקציה child_added callback מופעלת בדיוק פעמיים, אלא אם יש פחות משני דינוזאורים שמאוחסנים במסד הנתונים. הפונקציה תופעל גם לכל דינוזאור חדש וכבד יותר שיתווסף למסד הנתונים. ב-Python, השאילתה מחזירה ישירות OrderedDict שמכיל את שני הדינוזאורים הכי כבדים.

באופן דומה, אפשר למצוא את שני הדינוזאורים הכי קצרים באמצעות limitToFirst():

Java
dinosaursRef.orderByChild("weight").limitToFirst(2).addChildEventListener(new ChildEventListener() {
  @Override
  public void onChildAdded(DataSnapshot dataSnapshot, String prevChildKey) {
    System.out.println(dataSnapshot.getKey());
  }

  // ...
});
Node.js
const ref = db.ref('dinosaurs');
ref.orderByChild('height').limitToFirst(2).on('child_added', (snapshot) => {
  console.log(snapshot.key);
});
Python
ref = db.reference('dinosaurs')
snapshot = ref.order_by_child('height').limit_to_first(2).get()
for key in snapshot:
    print(key)
Go
ref := client.NewRef("dinosaurs")

results, err := ref.OrderByChild("height").LimitToFirst(2).GetOrdered(ctx)
if err != nil {
	log.Fatalln("Error querying database:", err)
}
for _, r := range results {
	fmt.Println(r.Key())
}

הפונקציה child_added callback מופעלת בדיוק פעמיים, אלא אם יש פחות משני דינוזאורים שמאוחסנים במסד הנתונים. הפונקציה תופעל שוב גם אם אחד משני הדינוזאורים הראשונים יוסר ממסד הנתונים, כי עכשיו דינוזאור חדש יהיה השני הכי קצר. ב-Python, השאילתה מחזירה ישירות OrderedDict שמכיל את הדינוזאורים הקצרים ביותר.

אפשר גם להריץ שאילתות עם מגבלות באמצעות orderByValue(). אם רוצים ליצור טבלת מובילים עם 3 הדינוזאורים שצברו את הניקוד הכי גבוה במשחקי ספורט, אפשר לעשות את הפעולות הבאות:

Java
scoresRef.orderByValue().limitToFirst(3).addChildEventListener(new ChildEventListener() {
  @Override
  public void onChildAdded(DataSnapshot dataSnapshot, String prevChildKey) {
    System.out.println("The " + dataSnapshot.getKey() + " score is " + dataSnapshot.getValue());
  }

  // ...
});
Node.js
const scoresRef = db.ref('scores');
scoresRef.orderByValue().limitToLast(3).on('value', (snapshot)  =>{
  snapshot.forEach((data) => {
    console.log('The ' + data.key + ' dinosaur\'s score is ' + data.val());
  });
});
Python
scores_ref = db.reference('scores')
snapshot = scores_ref.order_by_value().limit_to_last(3).get()
for key, val in snapshot.items():
    print(f'The {key} dinosaur\'s score is {val}')
Go
ref := client.NewRef("scores")

results, err := ref.OrderByValue().LimitToLast(3).GetOrdered(ctx)
if err != nil {
	log.Fatalln("Error querying database:", err)
}
for _, r := range results {
	var score int
	if err := r.Unmarshal(&score); err != nil {
		log.Fatalln("Error unmarshaling result:", err)
	}
	fmt.Printf("The %s dinosaur's score is %d\n", r.Key(), score)
}

שאילתות טווח

השימוש ב-startAt(), ב-endAt() וב-equalTo() מאפשר לכם לבחור נקודות התחלה וסיום שרירותיות לשאילתות. לדוגמה, אם רוצים למצוא את כל הדינוזאורים שגובהם לפחות שלושה מטרים, אפשר לשלב בין orderByChild() לבין startAt():

Java
dinosaursRef.orderByChild("height").startAt(3).addChildEventListener(new ChildEventListener() {
  @Override
  public void onChildAdded(DataSnapshot dataSnapshot, String prevChildKey) {
    System.out.println(dataSnapshot.getKey());
  }

  // ...
});
Node.js
const ref = db.ref('dinosaurs');
ref.orderByChild('height').startAt(3).on('child_added', (snapshot) => {
  console.log(snapshot.key);
});
Python
ref = db.reference('dinosaurs')
snapshot = ref.order_by_child('height').start_at(3).get()
for key in snapshot:
    print(key)
Go
ref := client.NewRef("dinosaurs")

results, err := ref.OrderByChild("height").StartAt(3).GetOrdered(ctx)
if err != nil {
	log.Fatalln("Error querying database:", err)
}
for _, r := range results {
	fmt.Println(r.Key())
}

אתם יכולים להשתמש ב-endAt() כדי למצוא את כל הדינוזאורים שהשמות שלהם מופיעים לפני Pterodactyl בסדר לקסיקוגרפי:

Java
dinosaursRef.orderByKey().endAt("pterodactyl").addChildEventListener(new ChildEventListener() {
  @Override
  public void onChildAdded(DataSnapshot dataSnapshot, String prevChildKey) {
    System.out.println(dataSnapshot.getKey());
  }

  // ...
});
Node.js
const ref = db.ref('dinosaurs');
ref.orderByKey().endAt('pterodactyl').on('child_added', (snapshot) => {
  console.log(snapshot.key);
});
Python
ref = db.reference('dinosaurs')
snapshot = ref.order_by_key().end_at('pterodactyl').get()
for key in snapshot:
    print(key)
Go
ref := client.NewRef("dinosaurs")

results, err := ref.OrderByKey().EndAt("pterodactyl").GetOrdered(ctx)
if err != nil {
	log.Fatalln("Error querying database:", err)
}
for _, r := range results {
	fmt.Println(r.Key())
}

אפשר לשלב בין startAt() לבין endAt() כדי להגביל את שני הקצוות של השאילתה. בדוגמה הבאה מוצגים כל הדינוזאורים שהשם שלהם מתחיל באות 'ב':

Java
dinosaursRef.orderByKey().startAt("b").endAt("b\uf8ff").addChildEventListener(new ChildEventListener() {
  @Override
  public void onChildAdded(DataSnapshot dataSnapshot, String prevChildKey) {
    System.out.println(dataSnapshot.getKey());
  }

  // ...
});
Node.js
var ref = db.ref('dinosaurs');
ref.orderByKey().startAt('b').endAt('b\uf8ff').on('child_added', (snapshot) => {
  console.log(snapshot.key);
});
Python
ref = db.reference('dinosaurs')
snapshot = ref.order_by_key().start_at('b').end_at('b\uf8ff').get()
for key in snapshot:
    print(key)
Go
ref := client.NewRef("dinosaurs")

results, err := ref.OrderByKey().StartAt("b").EndAt("b\uf8ff").GetOrdered(ctx)
if err != nil {
	log.Fatalln("Error querying database:", err)
}
for _, r := range results {
	fmt.Println(r.Key())
}

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

Java
dinosaursRef.orderByChild("height").equalTo(25).addChildEventListener(new ChildEventListener() {
  @Override
  public void onChildAdded(DataSnapshot dataSnapshot, String prevChildKey) {
    System.out.println(dataSnapshot.getKey());
  }

  // ...
});
Node.js
const ref = db.ref('dinosaurs');
ref.orderByChild('height').equalTo(25).on('child_added', (snapshot) => {
  console.log(snapshot.key);
});
Python
ref = db.reference('dinosaurs')
snapshot = ref.order_by_child('height').equal_to(25).get()
for key in snapshot:
    print(key)
Go
ref := client.NewRef("dinosaurs")

results, err := ref.OrderByChild("height").EqualTo(25).GetOrdered(ctx)
if err != nil {
	log.Fatalln("Error querying database:", err)
}
for _, r := range results {
	fmt.Println(r.Key())
}

שאילתות טווח שימושיות גם כשצריך להוסיף מספור לדפים של הנתונים.

סיכום של כל המידע

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

Java
dinosaursRef.child("stegosaurus").child("height").addValueEventListener(new ValueEventListener() {
  @Override
  public void onDataChange(DataSnapshot stegoHeightSnapshot) {
    Integer favoriteDinoHeight = stegoHeightSnapshot.getValue(Integer.class);
    Query query = dinosaursRef.orderByChild("height").endAt(favoriteDinoHeight).limitToLast(2);
    query.addValueEventListener(new ValueEventListener() {
      @Override
      public void onDataChange(DataSnapshot dataSnapshot) {
        // Data is ordered by increasing height, so we want the first entry
        DataSnapshot firstChild = dataSnapshot.getChildren().iterator().next();
        System.out.println("The dinosaur just shorter than the stegosaurus is: " + firstChild.getKey());
      }

      @Override
      public void onCancelled(DatabaseError databaseError) {
        // ...
      }
    });
  }

  @Override
  public void onCancelled(DatabaseError databaseError) {
    // ...
  }
});
Node.js
  const ref = db.ref('dinosaurs');
  ref.child('stegosaurus').child('height').on('value', (stegosaurusHeightSnapshot) => {
    const favoriteDinoHeight = stegosaurusHeightSnapshot.val();

    const queryRef = ref.orderByChild('height').endAt(favoriteDinoHeight).limitToLast(2);
    queryRef.on('value', (querySnapshot) => {
      if (querySnapshot.numChildren() === 2) {
        // Data is ordered by increasing height, so we want the first entry
        querySnapshot.forEach((dinoSnapshot) => {
          console.log('The dinosaur just shorter than the stegasaurus is ' + dinoSnapshot.key);

          // Returning true means that we will only loop through the forEach() one time
          return true;
        });
      } else {
        console.log('The stegosaurus is the shortest dino');
      }
    });
});
Python
ref = db.reference('dinosaurs')
favotire_dino_height = ref.child('stegosaurus').child('height').get()
query = ref.order_by_child('height').end_at(favotire_dino_height).limit_to_last(2)
snapshot = query.get()
if len(snapshot) == 2:
    # Data is ordered by increasing height, so we want the first entry.
    # Second entry is stegosarus.
    for key in snapshot:
        print(f'The dinosaur just shorter than the stegosaurus is {key}')
        return
else:
    print('The stegosaurus is the shortest dino')
Go
ref := client.NewRef("dinosaurs")

var favDinoHeight int
if err := ref.Child("stegosaurus").Child("height").Get(ctx, &favDinoHeight); err != nil {
	log.Fatalln("Error querying database:", err)
}

query := ref.OrderByChild("height").EndAt(favDinoHeight).LimitToLast(2)
results, err := query.GetOrdered(ctx)
if err != nil {
	log.Fatalln("Error querying database:", err)
}
if len(results) == 2 {
	// Data is ordered by increasing height, so we want the first entry.
	// Second entry is stegosarus.
	fmt.Printf("The dinosaur just shorter than the stegosaurus is %s\n", results[0].Key())
} else {
	fmt.Println("The stegosaurus is the shortest dino")
}

איך הנתונים מסודרים

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

orderByChild

כשמשתמשים ב-orderByChild(), הנתונים שמכילים את מפתח הצאצא שצוין מסודרים באופן הבא:

  1. ילדים עם ערך null למפתח הילד שצוין מופיעים ראשונים.
  2. אחריהם מופיעים ילדים עם ערך של false למפתח הצאצא שצוין. אם לכמה צאצאים יש ערך של false, הם ממוינים לקסיקוגרפית לפי מפתח.
  3. אחריהם מופיעים ילדים עם ערך של true למפתח הצאצא שצוין. אם לכמה צאצאים יש ערך של true, הם ממוינים לקסיקוגרפית לפי מפתח.
  4. אחריהם מופיעים ילדים עם ערך מספרי, ממוינים בסדר עולה. אם לכמה צאצאים יש את אותו ערך מספרי לצומת הצאצא שצוין, הם ממוינים לפי מפתח.
  5. מחרוזות מופיעות אחרי מספרים, והן ממוינות בסדר עולה לפי סדר מילוני. אם לכמה צאצאים יש את אותו ערך לצומת הצאצא שצוין, הם מסודרים בסדר לקסיקוגרפי לפי מפתח.
  6. האובייקטים מופיעים בסוף, וממוינים בסדר עולה לפי מפתח.

orderByKey

כשמשתמשים ב-orderByKey() כדי למיין את הנתונים, הנתונים מוחזרים בסדר עולה לפי מפתח, באופן הבא. חשוב לזכור שמפתחות יכולים להיות רק מחרוזות.

  1. הילדים עם מפתח שאפשר לנתח כמספר שלם בן 32 ביט מופיעים ראשונים, בסדר עולה.
  2. אחריהם מופיעים ילדים עם ערך מחרוזת כמפתח, ממוינים בסדר עולה לפי סדר מילוני.

orderByValue

כשמשתמשים ב-orderByValue(), הילדים מסודרים לפי הערך שלהם. קריטריון הסדר זהה לזה שב-orderByChild(), אלא שהערך של הצומת משמש במקום הערך של מפתח צאצא שצוין.