34,333
edits
Changes
Created page with "Clearly a database system is of little use if the data it stores cannot be read. Now that the basics of writing data to a Firebase Realtime database have been outlined, this c..."
Clearly a database system is of little use if the data it stores cannot be read. Now that the basics of writing data to a Firebase Realtime database have been outlined, this chapter will explore the reading of data from a database tree. As will become clear early in this chapter, reading data from a Firebase Realtime Database differs from most database systems in that it involves the passive approach of listening for data value changes as opposed to actively making database retrieval requests.
== Detecting Value Changes ==
When working with Firebase realtime databases, there is no way to specifically read the value assigned to a particular database node. Instead, an app must register listeners in order to be informed when a value within a database changes. When changes are made to the database, a listener method is called and passed a snapshot containing relevant tree data.
Value change listeners are represented by the ValueEventListener class and are added by calling either the addValueEventListener() or addListenerForSingleValueEvent() method of an existing database reference object. Once added, the listener methods will be called when changes are detected either at the path specified by the database reference object, or on any descendent nodes of that path.
If the addValueEventListener() method is used to add the listener, the app will be notified every time the data changes in the specified subtree. The addListenerForSingleValueEvent(), on the other hand, only reports the first time that a value change occurs and then removes itself from the database reference. In both cases, the listener will be called once when first added to the database reference in addition to any calls made as the result of value changes.
For the purpose of an example, we will assume that an app needs to be notified of any changes made to the value assigned to the message1 node from the example described in the previous chapter. To achieve this, a value event listener would need to be added to the database reference as follows:
<pre>
dbRef = database.getReference("/test/data/message1");
dbRef.addValueEventListener(changeListener);
</pre>
The above code references a listener named changeListener which now needs to be implemented to contain callback methods to handle both value changes and errors encountered during a database operation:
<pre>
ValueEventListener changeListener = new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
}
@Override
public void onCancelled(DatabaseError databaseError) {
}
};
</pre>
Having detected that a value has changed, code now needs to be added to obtain the new value. This involves the extraction of data from the DataSnapshot object passed by Firebase to the onDataChange() listener method.
When a listener is no longer needed it should be detached from the database reference object as follows:
<pre>
dbRef.removeEventListener(changeListener);
</pre>
== Working with a DataSnapshot Object ==
Value change events are configured to notify an app of changes to the data taking place at a particular path within the database tree. It is important to note that this includes changes to any descendent nodes (in other words the subtree beginning at the designated path). A listener added to a database reference path of /data/test would, therefore, also detect a change that occurred in a node with a path of /test/data/messages/content1.
When a data snapshot is delivered to the listener, it will contain the nodes for the entire subtree in which the value change occurred. It is also important to keep in mind that the data snapshot may contain value changes that have occurred at multiple node locations within the subtree. Given these potential value change scenarios, it is necessary to be able to obtain information from the DataSnapshot instance.
The simplest form of snapshot is one where only one value could have changed. In fact, this is the case for the example used so far in this chapter. With the listener added to the /test/data/message1 path, the only possible value change is for the message1 node (unless, of course, additional descendent nodes are being added). This new value can simply be extracted from the snapshot as follows:
<pre>
String change = dataSnapshot.getValue(String.class);
</pre>
Database operations are rarely, if ever, this simple. Figure 21-1, for example, shows another database tree as rendered within the Firebase console.
[[File:]]
Figure 21-1
Suppose that a value change event listener has been added to the database reference path of /test/data as follows:
<pre>
dbRef = database.getReference("/test/data");
dbRef.addValueEventListener(changeListener);
</pre>
As configured, any changes that take place in the tree beneath the /test/data level will trigger a call to the onDataChange() method of the change event listener. The data snapshot passed to this method will contain the entire subtree located beneath the /test/data node.
This presents the challenge of obtaining information about the snapshot and extracting data. Fortunately the DataSnapshot class provides a number of methods that can be used to interrogate and navigate the snapshot tree.
Before looking at these methods, the concept of the root node within a data snapshot needs to be explained. Continuing with the same example, the path of /test/data is considered to represent the root of the tree and, as such, all operations performed on the snapshot will be relative to this location in the tree. It is possible to identify the path to the root of the snapshot by calling the getRef() method of the snapshot object. This will return a DatabaseReference object from which the path string can be obtained. For example:
<pre>
String path = dataSnapshot.getRef().toString();
</pre>
When executed, the path returned will resemble the following:
<pre>
https://<firebase project id>.firebaseio.com/test/data
</pre>
Whether or not the root node of the snapshot contains child nodes can be found by making a call to the hasChildren() method as follows:
<pre>
if (dataSnapshot.hasChildren()) {
// The root node of the snapshot has children
}
</pre>
Based on the example tree in Figure 21 1 above, the hasChildren() method will return a true value since the /test/data node has two direct descendants (message1 and message2).
A count of the number of direct children of the root can be identified by making a call to the getChildrenCount() method of the snapshot object:
<pre>
long count = dataSnapshot.getChildrenCount();
</pre>
In the example scenario this call will, of course, return a count of two children.
The presence or otherwise of a child node at a particular path within a snapshot can be identified via a call to the hasChild() method. The following code, for example, checks that the snapshot contains a node for the message1 title:
<pre>
if (dataSnapshot.hasChild("message1/title")) {
// The snapshot contains a node at the specified path
}
</pre>
This is one of a number of areas where it is important to remember that the snapshot tree paths are relative to the root path at which the listener is attached. The above code will return a true result given the structure of the example tree. The following, however, will report that the path does not exist since neither the test nor data nodes are stored within the snapshot tree:
<pre>
if (dataSnapshot.hasChild("/test/data/message1/title")) {
// The snapshot contains a node at the specified path
}
</pre>
== Extracting Data from a DataSnapshot Object ==
A range of different options are available for extracting data from a DataSnapshot object. As outlined in the previous section, the getValue() method may be used to extract a specific value from the tree:
<pre>
String change = dataSnapshot.getValue(String.class);
</pre>
Much as when using the setValue() method to write data, the child() method may also be used to navigate to a specific path within the snapshot tree before accessing the value at that location. The following code, for example, extracts the String value stored for the message1 title key:
<pre>
String title =
dataSnapshot.child("message1").child("title").getValue(String.class);
</pre>
All of the direct children of the snapshot root can be requested using the getChildren() method. This returns the immediate child nodes in the form of an Iterable list of DataSnapshot objects which can be looped through using a for construct as follows:
<pre>
for (DataSnapshot child : dataSnapshot.getChildren()) {
Log.i(TAG, child.getKey());
Log.i(TAG, child.getValue(String.class));
}
</pre>
In addition to the values, the above code makes use of the getKey() method to display the keys for the two immediate children in the example tree (i.e. message1 and message2). Because each element returned by the getChildren() method is itself a DataSnapshot instance, code can be written to recursively call the getChildren() method on each child to eventually extract keys and values from the entire snapshot tree.
== Reading Data into a Java Object ==
In addition to retrieving data values as native types (for example String, int, long, Boolean etc.), the data may also be extracted from a data snapshot in the form of a Java object. In the previous chapter, data was written to a database in the form of a Java object based on an example Profile Java class. The following code extracts the data associated with this class from a snapshot in the form of a Profile object before displaying the name, address and phone data:
<pre>
Profile profile = dataSnapshot.getValue(Profile.class);
Log.i(TAG, profile.name);
Log.i(TAG, profile.address);
Log.i(TAG, profile.phone);
</pre>
Data does not have to have been written to the database in the form of a Java object in order to have the data retrieved in object form. The message nodes used as an example throughout this chapter can be extracted into a Java object even though a Java object was not used when writing the data. All that is required is a Java class that matches the structure of the data snapshot. The following class, for example, would be suitable for containing the example message data from a snapshot:
<pre>
import com.google.firebase.database.IgnoreExtraProperties;
@IgnoreExtraProperties
public class Message {
public String title;
public String content;
public Message() {
}
public Message(String title, String content) {
this.title = title;
this.content = content;
}
}
</pre>
This class could then be used in a for loop to output the title and content values of all the messages in a snapshot:
<pre>
for (DataSnapshot child : dataSnapshot.getChildren()) {
Message message = child.getValue(Message.class);
Log.i(TAG, "Title = " + message.title);
Log.i(TAG, "Content = " + message.content);
}
</pre>
== Summary ==
Unlike traditional database systems, data is read from a Firebase Realtime Database by implementing a listener to be called each time a change takes place within a designated database subtree. The onDataChanged() listener callback method is passed a DataSnapshot containing the data for the specified subtree. This object can then be traversed and the modified data values extracted using the child() and getValue() methods of the snapshot object.
== Detecting Value Changes ==
When working with Firebase realtime databases, there is no way to specifically read the value assigned to a particular database node. Instead, an app must register listeners in order to be informed when a value within a database changes. When changes are made to the database, a listener method is called and passed a snapshot containing relevant tree data.
Value change listeners are represented by the ValueEventListener class and are added by calling either the addValueEventListener() or addListenerForSingleValueEvent() method of an existing database reference object. Once added, the listener methods will be called when changes are detected either at the path specified by the database reference object, or on any descendent nodes of that path.
If the addValueEventListener() method is used to add the listener, the app will be notified every time the data changes in the specified subtree. The addListenerForSingleValueEvent(), on the other hand, only reports the first time that a value change occurs and then removes itself from the database reference. In both cases, the listener will be called once when first added to the database reference in addition to any calls made as the result of value changes.
For the purpose of an example, we will assume that an app needs to be notified of any changes made to the value assigned to the message1 node from the example described in the previous chapter. To achieve this, a value event listener would need to be added to the database reference as follows:
<pre>
dbRef = database.getReference("/test/data/message1");
dbRef.addValueEventListener(changeListener);
</pre>
The above code references a listener named changeListener which now needs to be implemented to contain callback methods to handle both value changes and errors encountered during a database operation:
<pre>
ValueEventListener changeListener = new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
}
@Override
public void onCancelled(DatabaseError databaseError) {
}
};
</pre>
Having detected that a value has changed, code now needs to be added to obtain the new value. This involves the extraction of data from the DataSnapshot object passed by Firebase to the onDataChange() listener method.
When a listener is no longer needed it should be detached from the database reference object as follows:
<pre>
dbRef.removeEventListener(changeListener);
</pre>
== Working with a DataSnapshot Object ==
Value change events are configured to notify an app of changes to the data taking place at a particular path within the database tree. It is important to note that this includes changes to any descendent nodes (in other words the subtree beginning at the designated path). A listener added to a database reference path of /data/test would, therefore, also detect a change that occurred in a node with a path of /test/data/messages/content1.
When a data snapshot is delivered to the listener, it will contain the nodes for the entire subtree in which the value change occurred. It is also important to keep in mind that the data snapshot may contain value changes that have occurred at multiple node locations within the subtree. Given these potential value change scenarios, it is necessary to be able to obtain information from the DataSnapshot instance.
The simplest form of snapshot is one where only one value could have changed. In fact, this is the case for the example used so far in this chapter. With the listener added to the /test/data/message1 path, the only possible value change is for the message1 node (unless, of course, additional descendent nodes are being added). This new value can simply be extracted from the snapshot as follows:
<pre>
String change = dataSnapshot.getValue(String.class);
</pre>
Database operations are rarely, if ever, this simple. Figure 21-1, for example, shows another database tree as rendered within the Firebase console.
[[File:]]
Figure 21-1
Suppose that a value change event listener has been added to the database reference path of /test/data as follows:
<pre>
dbRef = database.getReference("/test/data");
dbRef.addValueEventListener(changeListener);
</pre>
As configured, any changes that take place in the tree beneath the /test/data level will trigger a call to the onDataChange() method of the change event listener. The data snapshot passed to this method will contain the entire subtree located beneath the /test/data node.
This presents the challenge of obtaining information about the snapshot and extracting data. Fortunately the DataSnapshot class provides a number of methods that can be used to interrogate and navigate the snapshot tree.
Before looking at these methods, the concept of the root node within a data snapshot needs to be explained. Continuing with the same example, the path of /test/data is considered to represent the root of the tree and, as such, all operations performed on the snapshot will be relative to this location in the tree. It is possible to identify the path to the root of the snapshot by calling the getRef() method of the snapshot object. This will return a DatabaseReference object from which the path string can be obtained. For example:
<pre>
String path = dataSnapshot.getRef().toString();
</pre>
When executed, the path returned will resemble the following:
<pre>
https://<firebase project id>.firebaseio.com/test/data
</pre>
Whether or not the root node of the snapshot contains child nodes can be found by making a call to the hasChildren() method as follows:
<pre>
if (dataSnapshot.hasChildren()) {
// The root node of the snapshot has children
}
</pre>
Based on the example tree in Figure 21 1 above, the hasChildren() method will return a true value since the /test/data node has two direct descendants (message1 and message2).
A count of the number of direct children of the root can be identified by making a call to the getChildrenCount() method of the snapshot object:
<pre>
long count = dataSnapshot.getChildrenCount();
</pre>
In the example scenario this call will, of course, return a count of two children.
The presence or otherwise of a child node at a particular path within a snapshot can be identified via a call to the hasChild() method. The following code, for example, checks that the snapshot contains a node for the message1 title:
<pre>
if (dataSnapshot.hasChild("message1/title")) {
// The snapshot contains a node at the specified path
}
</pre>
This is one of a number of areas where it is important to remember that the snapshot tree paths are relative to the root path at which the listener is attached. The above code will return a true result given the structure of the example tree. The following, however, will report that the path does not exist since neither the test nor data nodes are stored within the snapshot tree:
<pre>
if (dataSnapshot.hasChild("/test/data/message1/title")) {
// The snapshot contains a node at the specified path
}
</pre>
== Extracting Data from a DataSnapshot Object ==
A range of different options are available for extracting data from a DataSnapshot object. As outlined in the previous section, the getValue() method may be used to extract a specific value from the tree:
<pre>
String change = dataSnapshot.getValue(String.class);
</pre>
Much as when using the setValue() method to write data, the child() method may also be used to navigate to a specific path within the snapshot tree before accessing the value at that location. The following code, for example, extracts the String value stored for the message1 title key:
<pre>
String title =
dataSnapshot.child("message1").child("title").getValue(String.class);
</pre>
All of the direct children of the snapshot root can be requested using the getChildren() method. This returns the immediate child nodes in the form of an Iterable list of DataSnapshot objects which can be looped through using a for construct as follows:
<pre>
for (DataSnapshot child : dataSnapshot.getChildren()) {
Log.i(TAG, child.getKey());
Log.i(TAG, child.getValue(String.class));
}
</pre>
In addition to the values, the above code makes use of the getKey() method to display the keys for the two immediate children in the example tree (i.e. message1 and message2). Because each element returned by the getChildren() method is itself a DataSnapshot instance, code can be written to recursively call the getChildren() method on each child to eventually extract keys and values from the entire snapshot tree.
== Reading Data into a Java Object ==
In addition to retrieving data values as native types (for example String, int, long, Boolean etc.), the data may also be extracted from a data snapshot in the form of a Java object. In the previous chapter, data was written to a database in the form of a Java object based on an example Profile Java class. The following code extracts the data associated with this class from a snapshot in the form of a Profile object before displaying the name, address and phone data:
<pre>
Profile profile = dataSnapshot.getValue(Profile.class);
Log.i(TAG, profile.name);
Log.i(TAG, profile.address);
Log.i(TAG, profile.phone);
</pre>
Data does not have to have been written to the database in the form of a Java object in order to have the data retrieved in object form. The message nodes used as an example throughout this chapter can be extracted into a Java object even though a Java object was not used when writing the data. All that is required is a Java class that matches the structure of the data snapshot. The following class, for example, would be suitable for containing the example message data from a snapshot:
<pre>
import com.google.firebase.database.IgnoreExtraProperties;
@IgnoreExtraProperties
public class Message {
public String title;
public String content;
public Message() {
}
public Message(String title, String content) {
this.title = title;
this.content = content;
}
}
</pre>
This class could then be used in a for loop to output the title and content values of all the messages in a snapshot:
<pre>
for (DataSnapshot child : dataSnapshot.getChildren()) {
Message message = child.getValue(Message.class);
Log.i(TAG, "Title = " + message.title);
Log.i(TAG, "Content = " + message.content);
}
</pre>
== Summary ==
Unlike traditional database systems, data is read from a Firebase Realtime Database by implementing a listener to be called each time a change takes place within a designated database subtree. The onDataChanged() listener callback method is passed a DataSnapshot containing the data for the specified subtree. This object can then be traversed and the modified data values extracted using the child() and getValue() methods of the snapshot object.