Data stored in a Firebase Realtime Database is retrieved by attaching an asynchronous listener to a database reference. The listener will be triggered once for the initial state of the data and again anytime the data changes. This document will cover the basics of retrieving database data, how data is ordered, and how to perform simple queries on data.
Getting Started
Let's revisit our blogging example from the previous article to understand how we read data from a Firebase database. Recall that the blog posts in our example app are stored at the database URL https://docs-examples.firebaseio.com/server/saving-data/fireblog/posts. To read our post data, we can do the following:
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 var db = firebase.database(); var ref = db.ref("server/saving-data/fireblog/posts"); // Attach an asynchronous callback to read the data at our posts reference ref.on("value", function(snapshot) { console.log(snapshot.val()); }, function (errorObject) { console.log("The read failed: " + errorObject.code); });
If we run this code, we'll see an object containing all of our posts logged to the console. This function will be called anytime new data is added to our database reference, and we don't need to write any extra code to make this happen.
The callback function receives a DataSnapshot
, which is a snapshot of the data. A snapshot is a picture of the data at a particular database reference at a single point in time. Calling val()
/ getValue()
on a snapshot returns the JavaScript object representation of the data. If no data exists at the reference's location, the snapshots value will be null
.
Notice that we used the value
event type in our example above, which reads the entire contents of a Firebase database reference, even if only one piece of data changed. value
is one of the five different event types listed below that we can use to read data from the database.
Read Event Types
Value
The value
event is used to read a static snapshot of the contents at a given database path, as they existed at the time of the read event. It is triggered once with the initial data and again every time the data changes. The event callback is passed a snapshot containing all data at that location, including child data. In our code example above, value
returned all of the blog posts in our app. Everytime a new blog post is added, the callback function will return all of the posts.
Child Added
The child_added
event is typically used when retrieving a list of items from the database. Unlike value
which returns the entire contents of the location, child_added
is triggered once for each existing child and then again every time a new child is added to the specified path. The event callback is passed a snapshot containing the new child's data. For ordering purposes, it is also passed a second argument containing the key of the previous child.
If we wanted to retrieve only the data on each new post added to our blogging app, we could use 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", function(snapshot, prevChildKey) { var newPost = snapshot.val(); console.log("Author: " + newPost.author); console.log("Title: " + newPost.title); console.log("Previous Post ID: " + prevChildKey); });
In this example the snapshot will contain an object with an individual blog post. Because we converted our post to an object by retrieving the value, we have access to the post's author and title properties by calling author
and title
respectively. We also have access to the previous post ID from the second prevChildKey
argument.
Child Changed
The child_changed
event is triggered any time a child node is modified. This includes any
modifications to descendants of the child node. It is typically used in conjunction with child_added
and child_removed
to respond to changes to a list of items. The snapshot passed to the event callback contains the updated data for the child.
We can use child_changed
to read updated data on blog posts when they are edited:
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", function(snapshot) { var changedPost = snapshot.val(); console.log("The updated post title is " + changedPost.title); });
Child Removed
The child_removed
event is triggered when an immediate child is removed. It is typically used in conjunction with child_added
and child_changed
. The snapshot passed to the event callback contains the data for the removed child.
In our blog example, we'll use child_removed
to log a notification about the deleted post to the console:
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 var ref = db.ref("server/saving-data/fireblog/posts"); // Get the data on a post that has been removed ref.on("child_removed", function(snapshot) { var deletedPost = snapshot.val(); console.log("The blog post titled '" + deletedPost.title + "' has been deleted"); });
Child Moved
The child_moved
event is used when working with ordered data, which is covered in the next section.
Event Guarantees
The Firebase database makes several important guarantees regarding events:
Database Event Guarantees |
---|
Events will always be triggered when local state changes. |
Events will always eventually reflect the correct state of the data, even in cases where local operations or timing cause temporary differences, such as in the temporary loss of network connection. |
Writes from a single client will always be written to the server and broadcast out to other users in-order. |
Value events are always triggered last and are guaranteed to contain updates from any other events which occurred before that snapshot was taken. |
Since value events are always triggered last, the following example will always work:
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
var count = 0; ref.on("child_added", function(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", function(snap) { console.log("initial data loaded!", snap.numChildren() === count); });
Detaching Callbacks
Callbacks are removed by specifying the event type and the callback function to be removed, like the following:
Java
// Create and attach listener ValueEventListener listener = new ValueEventListener() { // ... }; ref.addValueEventListener(listener); // Remove listener ref.removeEventListener(listener);
Node.js
ref.off("value", originalCallback);
If we passed a scope context into on()
, it must be passed when detaching the callback:
Java
// Not applicable for Java
Node.js
ref.off("value", originalCallback, this);
If we would like to remove all callbacks at a location, we can do the following:
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();
Reading Data Once
In some cases it may be useful for a callback to be called once and then immediately removed. We've created a helper function to make this easy:
Java
ref.addListenerForSingleValueEvent(new ValueEventListener() { @Override public void onDataChange(DataSnapshot dataSnapshot) { // ... } @Override public void onCancelled(DatabaseError databaseError) { // ... } });
Node.js
ref.once("value", function(data) { // do some stuff once });
Querying Data
With Firebase database queries, we can selectively retrieve data based on various factors. To construct a query in your database, you start by specifying how you want your data to be ordered using one of the ordering functions: orderByChild()
, orderByKey()
, or orderByValue()
. You can then combine these with five other methods to conduct complex queries: limitToFirst()
,
limitToLast()
, startAt()
, endAt()
, and equalTo()
.
Since all of us at Firebase think dinosaurs are pretty cool, we'll use this database of dinosaur facts to demonstrate how you can query data in your Firebase database. Here's a snippet of the dinosaur data:
{ "lambeosaurus": { "height" : 2.1, "length" : 12.5, "weight": 5000 }, "stegosaurus": { "height" : 4, "length" : 9, "weight" : 2500 } }
We can order data in three ways: by child key, by key, or by value. A basic database query starts with one of these ordering functions, each of which are explained below.
Ordering by a specified child key
We can order nodes by a common child key by passing that key to orderByChild()
. For example, to read all dinosaurs ordered by height, we can do
the following:
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
var db = firebase.database(); var ref = db.ref("dinosaurs"); ref.orderByChild("height").on("child_added", function(snapshot) { console.log(snapshot.key + " was " + snapshot.val().height + " meters tall"); });
Any node which does not have the child key we're querying on will be sorted with a value of null
, meaning it will come first in the ordering. For details on how data is ordered, see the How Data is Ordered section.
Queries can also be ordered by deeply nested children, rather than only children one level down. This is useful if you have deeply nested data like this:
{ "lambeosaurus": { "dimensions": { "height" : 2.1, "length" : 12.5, "weight": 5000 } }, "stegosaurus": { "dimensions": { "height" : 4, "length" : 9, "weight" : 2500 } } }
To query the height now, we use the full path to the object rather than a single key:
Java
dinosaursRef.orderByChild("dimensions/height").addChildEventListener(new ChildEventListener() { @Override public void onChildAdded(DataSnapshot dataSnapshot, String prevChildKey) { // ... } // ... });
Node.js
var ref = db.ref("dinosaurs"); ref.orderByChild("dimensions/height").on("child_added", function(snapshot) { console.log(snapshot.key + " was " + snapshot.val().height + " meters tall"); });
Queries can only order by one key at a time. Calling orderByChild()
multiple times on the same query throws an error.
Ordering by key
We can also order nodes by their keys using the orderByKey()
method. The
following example reads all dinosaurs in alphabetical order:
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", function(snapshot) { console.log(snapshot.key); });
Ordering by value
We can order nodes by the value of their child keys using the orderByValue()
method. Let's say the dinosaurs are having a dino sports competition and we're keeping track of their scores in the following format:
{ "scores": { "bruhathkayosaurus" : 55, "lambeosaurus" : 21, "linhenykus" : 80, "pterodactyl" : 93, "stegosaurus" : 5, "triceratops" : 22 } }
To sort the dinosaurs by their score, we could construct the following query:
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
var scoresRef = db.ref("scores"); scoresRef.orderByValue().on("value", function(snapshot) { snapshot.forEach(function(data) { console.log("The " + data.key + " dinosaur's score is " + data.val()); }); });
See the How Data is Ordered section for an explanation on how null
, boolean, string, and object values are sorted when using orderByValue()
.
Complex Queries
Now that we've specified how your data will be ordered, we can use the limit or range methods described below to construct more complex queries.
Limit Queries
The limitToFirst()
and limitToLast()
queries are used to set a
maximum number of children to be synced for a given callback. If we set a limit of 100, we
will initially only receive up to 100 child_added
events. If we have fewer than
100 messages stored in our database, a child_added
event will fire for each
message. However, if we have over 100 messages, we will only receive a child_added
event for 100 of those messages. These will be the first 100 ordered messages if we are using
limitToFirst()
or the last 100 ordered messages if we are using
limitToLast()
. As items change, we will receive child_added
events
for items that enter the query and child_removed
events for items that leave it,
so that the total number stays at 100.
Using our dinosaur facts database and orderByChild()
, we can find the two heaviest
dinosaurs:
Java
dinosaursRef.orderByChild("weight").limitToLast(2).addChildEventListener(new ChildEventListener() { @Override public void onChildAdded(DataSnapshot dataSnapshot, String prevChildKey) { System.out.println(dataSnapshot.getKey()); } // ... });
Node.js
var ref = db.ref("dinosaurs"); ref.orderByChild("weight").limitToLast(2).on("child_added", function(snapshot) { console.log(snapshot.key); });
Our child_added
callback will be triggered exactly two times, unless there are
less than two dinosaurs stored in the database. It will also get fired for every new, heavier dinosaur that gets added to the database.
Similarly, we can find the two shortest dinosaurs by using 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
var ref = db.ref("dinosaurs"); ref.orderByChild("height").limitToFirst(2).on("child_added", function(snapshot) { console.log(snapshot.key); });
Our child_added
callback will be triggered exactly two times, unless there are less than two dinosaurs stored in the database. It will also get fired again if one of the first two dinosaurs is removed from the database, as a new dinosaur will now be the second shortest.
We can also conduct limit queries with orderByValue()
. If we want to create a leaderboard with the top 3 highest scoring dino sports dinosaurs, we could do the following:
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
var scoresRef = db.ref("scores"); scoresRef.orderByValue().limitToLast(3).on("value", function(snapshot) { snapshot.forEach(function(data) { console.log("The " + data.key + " dinosaur's score is " + data.val()); }); });
Range Queries
Using startAt()
, endAt()
, and equalTo()
allows us to
choose arbitrary starting and ending points for our queries. For example, if we wanted to
find all dinosaurs that are at least three meters tall, we can combine orderByChild()
and 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
var ref = db.ref("dinosaurs"); ref.orderByChild("height").startAt(3).on("child_added", function(snapshot) { console.log(snapshot.key); });
We can use endAt()
to find all dinosaurs whose names come before Pterodactyl
lexicographically:
Java
dinosaursRef.orderByKey().endAt("pterodactyl").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().endAt("pterodactyl").on("child_added", function(snapshot) { console.log(snapshot.key); });
We can combine startAt()
and endAt()
to limit both ends of our
query. The following example finds all dinosaurs whose name starts with the letter "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", function(snapshot) { console.log(snapshot.key); });
The equalTo()
method allows us to filter based on exact matches. As is the case
with the other range queries, it will fire for each matching child node. For example, we can
use the following query to find all dinosaurs which are 25 meters tall:
Java
dinosaursRef.orderByChild("height").equalTo(25).addChildEventListener(new ChildEventListener() { @Override public void onChildAdded(DataSnapshot dataSnapshot, String prevChildKey) { System.out.println(dataSnapshot.getKey()); } // ... });
Node.js
var ref = db.ref("dinosaurs"); ref.orderByChild("height").equalTo(25).on("child_added", function(snapshot) { console.log(snapshot.key); });
Range queries are also useful when you need to paginate your data.
Putting it all together
We can combine all of these techniques to create complex queries. For example, we can find the name of the dinosaur that is just shorter than Stegosaurus:
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
var ref = db.ref("dinosaurs"); ref.child("stegosaurus").child("height").on("value", function(stegosaurusHeightSnapshot) { var favoriteDinoHeight = stegosaurusHeightSnapshot.val(); var queryRef = ref.orderByChild("height").endAt(favoriteDinoHeight).limitToLast(2) queryRef.on("value", function(querySnapshot) { if (querySnapshot.numChildren() === 2) { // Data is ordered by increasing height, so we want the first entry querySnapshot.forEach(function(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"); } }); });
How Data is Ordered
This section explains how your data is ordered when using each of the four ordering functions.
orderByChild
When using orderByChild()
, data that contains the specified child key will be ordered as follows:
- Children with a
null
value for the specified child key come first. - Children with a value of
false
for the specified child key come next. If multiple children have a value offalse
, they are sorted lexicographically by key. - Children with a value of
true
for the specified child key come next. If multiple children have a value oftrue
, they are sorted lexicographically by key. - Children with a numeric value come next, sorted in ascending order. If multiple children have the same numerical value for the specified child node, they are sorted by key.
- Strings come after numbers, and are sorted lexicographically in ascending order. If multiple children have the same value for the specified child node, they are ordered lexicographically by key.
- Objects come last, and sorted lexicographically by key in ascending order.
orderByKey
When using orderByKey()
to sort your data, data will be returned in ascending order by key as follows. Keep in mind that keys can only be strings.
- Children with a key that can be parsed as a 32-bit integer come first, sorted in ascending order.
- Children with a string value as their key come next, sorted lexicographically in ascending order.
orderByValue
When using orderByValue()
, children will be ordered by their value. The ordering criteria is the same as in orderByChild()
, except the value of the node is used instead of the value of a specified child key.