אחזור נתונים

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

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

תחילת העבודה

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

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

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

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

ערך

האירוע value משמש לקריאת קובץ snapshot סטטי של התוכן בנתיב נתון של מסד נתונים, כפי שהיה קיים בזמן אירוע הקריאה. הוא מופעל פעם אחת עם הנתונים הראשוניים ופעם נוספת בכל פעם שהנתונים משתנים. בקריאה החוזרת של האירוע מועברת קובץ snapshot שמכיל את כל הנתונים במיקום הזה, כולל נתוני הצאצאים. בדוגמת הקוד שלמעלה, הפונקציה 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);
});

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

הילד השתנה

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

אפשר להשתמש ב-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. קובץ snapshot שמוענק ל-call back של האירוע מכיל את הנתונים של הצאצא שהוסר.

בדוגמה של הבלוג, אפשר להשתמש ב-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 מבטיחים כמה דברים חשובים לגבי אירועים:

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

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

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);
});

ניתוק של קריאות חוזרות (callbacks)

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

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();

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

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

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('{0} was {1} meters tall'.format(key, val))
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('{0} was {1} meters tall'.format(key, val))
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('The {0} dinosaur\'s score is {1}'.format(key, 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 מההודעות האלה. אלה 100 ההודעות הראשונות לפי סדר אם משתמשים ב-limitToFirst(), או 100 ההודעות האחרונות לפי סדר אם משתמשים ב-limitToLast(). ככל שהפריטים משתנים, תקבלו אירועי 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())
}

פונקציית ה-callback‏ child_added מופעלת בדיוק פעמיים, אלא אם יש פחות משני דינוזאורים שמאוחסנים במסד הנתונים. הוא יופעל גם לכל דינוזאור חדש וכבד יותר שיתווסף למסד הנתונים. ב-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())
}

פונקציית ה-callback‏ child_added מופעלת בדיוק פעמיים, אלא אם יש פחות משני דינוזאורים שמאוחסנים במסד הנתונים. הפונקציה תופעל שוב גם אם אחד משני הדינוזאורים הראשונים יוסר ממסד הנתונים, כי דינוזאור חדש יהיה הדינוזאור השני הקצר ביותר. ב-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('The {0} dinosaur\'s score is {1}'.format(key, 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() כדי להגביל את שני הקצוות של השאילתה. בדוגמה הבאה מוצגים כל הדינוזאורים שהשם שלהם מתחיל באות 'b':

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(u'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('The dinosaur just shorter than the stegosaurus is {0}'.format(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(), חוץ מהעובדה שהערך של הצומת משמש במקום הערך של מפתח צאצא שצוין.