Tutorial: Read data

Serverless DatabaseData Structure Read data

Reading data from a Webcom application database relies on a subscription mechanism. Within a subscription, data may be finely queried using the query mechanism.

Subscriptions

Four subscription modes are actually provided, which are associated with four subscription events:

Subscription mode Description
value An event is raised each time the value of the subscribed data node changes. This event makes it possible to retrieve the whole value of the data node.
child_added An event is raised each time a new child node is added to the subscribed data node. This event makes it possible to retrieve the value of the newly created child node.
child_changed An event is raised each time the value of a child node of the subscribed data node changes. This event makes it possible to retrieve the value of the updataed child node.
child_removed An event is raised each time a child node of the subscribed data node is removed. This event makes it possible to retrieve the value of the removed child node.

The subscription mechanism expects a callback to be called asynchronously as soon as the corresponding event is raised. The registered callbacks generally receive an object that makes it possible to retrieve the actually raised event and the value of some relevant associated data nodes.

Depending on the chosen SDK, registering (or unregistering) a subscription callback is done using a on or off method on the references representing a data node (JavaScript and Android) or using a “manager” defined in the scope of a view controller that is able to automatically unregister all subscriptions when the associated controller is released (iOS).

Value subscription mode

In our address book example (introduced in the Write data section), let's read the data at the "contacts" node using the “value” subscription mode:

In JavaScript, subscriptions are registered directly on the Webcom object representing the desired data node using the on() method. A subscription is specified by a callback function that will be called each time the corresponding event is raised. The subscription mode is represented by a string: "value" for the “value” mode, "child_added" for the “child added” mode, "child_changed" for the “child changed” mode, "child_removed" for the “child removed” mode.

var contactsNode = new Webcom("webcom-addressbook").child("contacts");

// Attach a subscription to read the "contacts" node
contactsNode.on("value", snapshot => {
  const allContacts = snapshot.val();
  console.log(`The 'contacts' node has ${allContacts.length} child nodes.`);
});

The subscription callback receives as first parameter a DataSnapshot object that provides convenient methods to retrieve the kind of the just raised event as well as the value of the associated data node.

Note: it is possible to subscribe to several event types at a time by passing an array of strings as first parameter of the on() method instead of a single string.

In Java, subscriptions are registered directly on the Webcom object representing the desired data node using the on() method. A subscription is specified by a callback implementing the OnQuery interface, whose onComplete() method will be called each time the corresponding event is raised. The subscription mode is represented by a value of the dedicated Query.Event enum.

Webcom contactsNode = new Webcom(new WebcomApp("webcom-addressbook")).child("contacts");

// Attach a subscription to read the "contacts" node
contacts.on(Query.Event.VALUE, new OnQuery() {
  @Override
  public void onComplete(DataSnapshot snapData, @Nullable String prevName) {
    // code to handle current data
    List<Contact> allContacts = snapData.valueList(Contact.class);
    Log.i("VALUE", "The 'contacts' node has " + allContacts.size() + " child nodes.");
  }
  @Override
  public void onCancel(WebcomError error) {
    // event subscription was canceled because of permission reason
  }
  @Override
  public void onError(WebcomError error) {
    // an error occured
  }
});

The onComplete() receives as first parameter a DataSnapshot object that provides convenient methods to retrieve the kind of the just raised event as well as the value of the associated data node.

Note: it is possible to subscribe to several event types at a time by passing a list of Query.Event values as first parameter of the on() method instead of a single value.

In Swift, subscriptions are managed by DatasyncManager objects. Each manager automatically revokes its registered subscription when released. The idea is to create a manager for each view controller and bind their life-cycles, in order to avoid explicitly managing the revocation of subscriptions, which is generally a tedious task.

A subscription is registered using the subscribe() method of DatasyncNode objects and is managed by the DatasyncManager object that has created this DatasyncNode instance. A subscription is specified by a callback function that will be called each time the corresponding event is raised. The subscription mode is represented by a value of the dedicated DatasyncEventType enum.

let manager = WebcomApplication.default.datasyncService.createManager()
let contactsNode = manager / "contacts"
contactsNode?.subscribe(to: .valueChange) { event in
    guard let allContacts: [Contact] = event.value.decoded() else {
        return
    }
    print("The 'contacts' node has \(allContacts.count) child nodes")
}

The subscription callback receives as parameter a DatasyncEvent value that provides convenient methods to retrieve the kind of the just raised event, the value of the associated data node, as well as the corresponding DatasyncSubscription object (which makes it possible to individually revoke the subscription without waiting for the manager to do it).

Note: it is possible to subscribe to several event types at a time by passing a set of DatasyncEventType values as first parameter of the subscribe() method instead of a single value.

In the value subscription mode, the registered callback is called both:

  • at registration time: the corresponding first raised event provides the initial content of the subscribed node.
  • and then each time the data of the subscribed node changes: the subsequent raised events provide the future updates of the content of the subscribed node.

In our example, the callback is initially called with the list of all contacts contained in our address book, and will then be called afterwards every time a new contact is added or an existing contact is modified or removed. As we use the value mode, each raised event will pass to the callback the whole updated list of contacts at the "contacts" data node.

Child Added subscription mode

When you are not interested in getting back the whole content of a data node, you can use one of the “Child *” subscription modes. With the Child Added mode, the subscription callback is called:

  • at registration time: once for each existing child node of the subscribed data node. These initial raised events provide the content of each child node.
  • afterwards: each time a new child node is added to the subscribed data node. These subsequent raised events provide the content of the just added child node.

In JavaScript, we pass the "child_added" subscription mode to the on() method on the desired data node:

// watch new contacts
contactsNode.on("child_added", (snapshot, prevChildKey) => {
  const addedContact = snapshot.val();
  console.log("First name: " + addedContact.firstName);
  console.log("Last name: " + addedContact.lastName);
  console.log("Previous contact: " + prevChildKey);
});

// add a contact
contactsNode.set({
  jimix: {
    birthday: "November 27, 1942",
    firstName: "Jimi",
    lastName: "Hendrix",
    phoneNumber: "045 1234 6541",
    email: "jimix@spaceland.com"
  }
});

Note: in this case, the subscription callback additionally receives as second argument the name (or key) of the child node that immediately precedes the just added one, among the list of children of the subscribed data node.

In Java, we pass the Query.Event.CHILD_ADDED subscription mode to the on() method on the desired data node:

// watch new contacts
contactsNode.on(Query.Event.CHILD_ADDED, new OnQuery() {
  @Override
  public void onComplete(DataSnapshot snapData, @Nullable String prevName) {
    // code to handle current data
    Contact newContact = snapData.value(Contacts.class);
    Log.i("CHILD_ADDED", "First name: " + newContact.getFirstName());
    Log.i("CHILD_ADDED", "Last name: " + newContact.getLastName());
    Log.i("CHILD_ADDED", "Previous contact: " + prevName);
  }
  @Override
  public void onCancel(WebcomError error) {
    // event subscription was canceled because of permission reason
  }
  @Override
  public void onError(WebcomError error) {
    // an error occured
  }
});

// add a contact
Contact jimix = new Contact("November 27, 1942", "Jimi", "Hendrix", "045 1234 6541", "jimix@spaceland.com", null);
contactsNode.set(new HashMap() {
  { put("jimix", jimix);}
});

Note: in this case, the second argument of the onComplete() callback method is passed the name (or key) of the child node that immediately precedes the just added one, among the list of children of the subscribed data node.

In Swift, we pass the DatasyncEventType.childAddition subscription mode to the subscribe() method on the desired data node:

// add a contact
let jimix = Contact(birthday: "November 27, 1942", firstName: "Jimi", lastName: "Hendrix", phoneNumber: "045 1234 6541", email: "jimix@spaceland.com")
contactsNode?.set(["jimix": jimix])

// watch new contacts
contactsNode?.subscribe(to: .childAddition) { event in
    guard let contact: Contact = event.value.decoded() else {
        return
    }
    print("First name:", contact.firstName)
    print("Last name:", contact.lastName)
    print("Previous contact:", event.previousChildName ?? "(none)")
}

Note: in this case, the previousChildName property of the raised event (of type DatasyncEvent) gives the name (or key) of the child node that immediately precedes the just added one, among the list of children of the subscribed data node.

Child Changed subscription mode

With the Child Changed mode, the subscription callback is not called at registration. It is only called each time the content of one of the child nodes of the subscribed data node is updated.

In JavaScript, we pass the "child_changed" subscription mode to the on() method on the desired data node:

// watch updated contacts
contactsNode.on("child_changed", snapshot => {
  const changedContact = snapshot.val();
  console.log(`The contact '${changedContact.firstName} ${changedContact.lastName}' was updated`);
  console.log(`Address: ${changedContact.address}`);
});

// update some contact details
contactsNode.child("macca").update({
    "address": "London 2358644689"
});

Note: here, the callback function receives no second argument.

In Java, we pass the Query.Event.CHILD_CHANGED subscription mode to the on() method on the desired data node:

// watch updated contacts
contactsNode.on(Query.Event.CHILD_CHANGED, new OnQuery() {
  @Override
  public void onComplete(DataSnapshot snapData, @Nullable String prevName) {
    // code to handle current data
    Contact changedContact = snapData.value(Contact.class);
    Log.i("CHILD_CHANGED", String.format("The contact '%s %s' was updated.", changedContact.getFirstName(), changedContact.getLastName()));
    Log.i("CHILD_ADDED", "Address: " + changedContact.getAddress());
  }
  @Override
  public void onCancel(WebcomError error) {
    // event subscription was canceled because of permission reason
  }
  @Override
  public void onError(WebcomError error) {
    // an error occured
  }
});

// update some contact details
contactsNode.child("macca").update(new HashMap() {
  { put("address", "London 2358644689"); }
});

Note: here, the second argument of the onComplete() callback method is not set (ie. is passed null).

In Swift, we pass the DatasyncEventType.childChange subscription mode to the subscribe() method on the desired data node:

// watch updated contacts
contactsNode?.subscribe(to: .childChange) { event in
    guard let contact: Contact = event.value.decoded() else {
        return
    }
    print("The contact '\(contact.firstName) \(contact.lastName)' was updated.")
    print("Address:", contact.address ?? "(unknown)")
}

// update some contact details
let maccaNode = contactsNode / "macca"
maccaNode?.merge(with: ["address" : "London 2358644689"])

Note: here, the previousChildName property of the raised event (of type DatasyncEvent) is not set.

Child Removed subscription mode

With the Child Removed mode, the subscription callback is not called at registration. It is only called each time one of the child nodes of the subscribed data node is removed.

In JavaScript, we pass the "child_removed" subscription mode to the on() method on the desired data node:

// watch deleted contacts
contactsNode.on("child_removed", snapshot => {
  const deletedContact = snapshot.val();
  console.log(`The following contact '${deletedContact.lastName}' has been deleted`);
});

// delete a contact
contactsNode.child("macca").remove();

Note: here, the callback function receives no second argument.

In Java, we pass the Query.Event.CHILD_REMOVED subscription mode to the on() method on the desired data node:

// watch deleted contacts
contactsNode.on(Query.Event.CHILD_REMOVED, new OnQuery() {
  @Override
  public void onComplete(DataSnapshot snapData, @Nullable String prevName) {
    // code to handle current data
    Contact deletedContact = snapData.value(Contact.class);
    Log.i("CHILD_CHANGED", String.format("The following contact '%s %s' has been deleted.", deletedContact.getFirstName(), deletedContact.getLastName()));
  }
  @Override
  public void onCancel(WebcomError error) {
    // event subscription was canceled because of permission reason
  }
  @Override
  public void onError(WebcomError error) {
    // an error occured
  }
});

// delete a contact
contactsNode.child("macca").remove();

Note: here, the second argument of the onComplete() callback method is not set (ie. is passed null).

In Swift, we pass the DatasyncEventType.childRemoval subscription mode to the subscribe() method on the desired data node:

// watch deleted contacts
contactsNode?.subscribe(to: .childRemoval) { event in
    guard let contact: Contact = event.value.decoded() else {
        return
    }
    print("The contact '\(contact.firstName) \(contact.lastName)' has been deleted.")
}

// delete a contact
let maccaNode = contactsNode / "macca"
maccaNode?.clear()

Note: here, the previousChildName property of the raised event (of type DatasyncEvent) is not set.

Subscription revocation

In Javascript, subscriptions may be revoked calling the off() method on the previously subscribed data node:

// Remove all callbacks associated to the "value" subscription mode at a given data node
contactsNode.off("value");

// Remove a specific callback at a given data node
contactsNode.off("child_added", myChildAddedCallback);

// Remove all callbacks associated to any subscription mode at a given data node
contactsNode.off();

In Java, subscriptions may be revoked calling the off() method on the previously subscribed data node:

// Remove all callbacks associated to the "value" subscription mode at a given data node
contactsNode.off(Query.Event.VALUE);

// Remove a specific callback at a given data node
contactsNode.off(Query.Event.CHILD_ADDED, myChildAddedCallback);

// Remove all callbacks associated to any subscription mode at a given data node
contactsNode.off();

In Swift, all registered subscriptions are managed by the DatasyncManager instance that has provided the subscribed DatasyncNode object. When the DatasyncManager is released, all subscriptions that have been registered on its DatasyncNodes are automatically revoked. In general, it is therefore useless to explicitly revoke subscriptions.

However, if, for some reason, a subscription has to be revoked before its associated DatasyncManager is released, you can call the unsubscribe() method on the DatasyncSubscription object returned by the subscribe method when registering a subscription.

// When subscribing to an event
let subscription = contactsNode?.subscribe(to: .valueChange) { event in
  // ...
}

// Revoke the subscription before the DatasyncManager does
subscription?.unsubscribe()

Queries

Subscriptions may be refined to select only a sub-part of the content of the queried data nodes. For example, it is possible to select only last or first n child nodes of the data node or a subset of child nodes defined by an interval on the names of the child nodes.

The ordering of child nodes with respect to their names, which is used to find out the last or first n ones or a range of them, is formally defined in the Key Ordering section.

Reading the last or first n elements of a list

In JavaScript, you can combine the limit() method with either the startAt() or the endAt() ones on the data node to subscribe before calling the previous on() method:

// Reference to our list of contacts
const contactsNode = new Webcom("webcom-addressbook").child("contacts");

// Subscribe to last 5 elements of the list
contactsNode.limit(5).on("value", snapshot => {
  const last5Contacts = snapshot.val();
  console.log("The last five contacts are" , last5Contacts.map(contact => contact.firsName).join(", "));
});

// or:

// Subscribe to first 5 elements of the list
contactsNode.startAt().limit(5).on("value", snapshot => {
  const first5Contacts = snapshot.val();
  console.log("The first five messages are" , first5Contacts.map(contact => contact.firstName).join(", "));
});

In Java, you can combine limit() method with either the startAt() or the endAt() ones on the data node to subscribe before calling the previous on() method:

// Reference to our list of contacts
Webcom contactsNode = new Webcom(new WebcomApp("webcom-addressbook")).child("contacts");

// Subscribe to last 5 elements of the list
contactsNode.limit(5).on(Query.Event.VALUE, new OnQuery() {
  @Override
  public void onComplete(DataSnapshot snapData, @Nullable String prevName) {
    // code to handle current data
    List<Contact> last5Contacts = snapData.valueList(Contact.class);
    Log.i("QUERY", "Here are the last 5 contacts");
    for (Contact contact in last5Contacts) {
      Log.i("QUERY", contact.getFirstName());
    }
  }
  @Override
  public void onCancel(WebcomError error) {
    // event subscription was canceled because of permission reason
  }
  @Override
  public void onError(WebcomError error) {
    // an error occured
  }
});

// or:

// Subscribe to first 5 elements of the list
contactsNode.startAt().limit(5).on(Query.Event.VALUE, new OnQuery() {
  @Override
  public void onComplete(DataSnapshot snapData, @Nullable String prevName) {
    // code to handle current data
    List<Contact> first5Contacts = snapData.valueList(Contact.class);
    Log.i("QUERY", "Here are the first 5 contacts");
    for (Contact contact in first5Contacts) {
      Log.i("QUERY", contact.getFirstName());
    }
  }
  @Override
  public void onCancel(WebcomError error) {
    // event subscription was canceled because of permission reason
  }
  @Override
  public void onError(WebcomError error) {
    // an error occured
  }
});

In Swift, the subscribe() method accepts a DatasyncConstraint value in the constraint parameter, which makes it possible to refine the queried data.

// Reference to our list of contacts
let manager = WebcomApplication.default.datasyncService.createManager()
let contactsNode = manager / "contacts"

// Subscribe to last 5 elements of the list
contactsNode?.subscribe(to: .valueChange, constraint: .last(limit: 5)) { event in
    guard let last5Contacts: [Contact] = event.value.decoded() else {
        return
    }
    print("Here are the last 5 contacts:", last5Contacts.map { $0.firstName }.joined(separator: ", "))
}

// or:

// Subscribe to first 5 elements of the list
contactsNode?.subscribe(to: .valueChange, constraint: .first(limit: 5)) { event in
    guard let first5Contacts: [Contact] = event.value.decoded() else {
        return
    }
    print("Here are the first 5 contacts:", first5Contacts.map { $0.firstName }.joined(separator: ", "))
}

Reading a range of keys

Before querying a range of child node, please read the Key Ordering section to understand how the names of child nodes (or keys) are sorted within a data node.

In JavaScript, you can combine the startAt() and endAt() methods on the data node to subscribe before calling the on() method:

// Reference to our list of contacts
const contactsNode = new Webcom("webcom-addressbook").child("contacts");

// Subscribe to the elements between "a" and "l" (inclusive)
contactsNode.startAt("a").endAt("l").on("value", snapshot => {
    const rangeContacts = snapshot.val();
    console.log("Contacts between 'a' and 'l' are:", rangeContacts.map(contact => contact.firstName).join(", "))
    // => jimix
});

In Java, you can combine the startAt() and endAt() methods on the data node to subscribe before calling the on() method:

// Reference to our list of contacts
Webcom contactsNode = new Webcom(new WebcomApp("webcom-addressbook")).child("contacts");

// Subscribe to the elements between "a" and "l" (inclusive)
contactsNode.startAt("a").endAt("l").on(Query.Event.VALUE, new OnQuery() {
  @Override
  public void onComplete(DataSnapshot snapData, @Nullable String prevName) {
    // code to handle current data
    List<Contact> rangeContacts = snapData.valueList(Contact.class);
    Log.i("QUERY", "Contacts between 'a' and 'l' are:");
    for (Contact contact in rangeContacts) {
      Log.i("QUERY", contact.getFirstName());
    }
    // => jimix
  }
  @Override
  public void onCancel(WebcomError error) {
    // event subscription was canceled because of permission reason
  }
  @Override
  public void onError(WebcomError error) {
    // an error occured
  }
});

In Swift, simply use the DatasyncConstraint value suiting your range constraint: startAt(), endAt() or between.

// Reference to our list of contacts
let manager = WebcomApplication.default.datasyncService.createManager()
let contactsNode = manager / "contacts"

// Subscribe to the elements between "a" and "l" (inclusive)
contactsNode?.subscribe(to: .valueChange, constraint: .between(startKey: "a", endKey: "l")) { event in
    guard let rangeContacts: [Contact] = event.value.decoded() else {
        return
    }
    print("Contacts between 'a' and 'l' are:", rangeContacts.map { $0.firstName }.joined(separator: ", "))
    // => jimix
}

Reading from a key with a limit

It is for example possible to query the 5 first names in a directory starting at a value. Key order definition

In JavaScript, you can combine the startAt() and limit() methods on the data node to subscribe before calling the on() method:

// Reference to our list of contacts
const contactsNode = new Webcom("webcom-addressbook").child("contacts");

// Subscribe to the 2 elements from "k"
contactsNode.startAt("k").limit(2).on("value", snapshot => {
    const twoFromKContacts = snapshot.val();
    console.log("The 2 contacts from 'k' are:", twoFromKContacts.map(contact => contact.firstName).join(", "))
    // => lennon, macca
});

In Java, you can combine the startAt() and limit() methods on the data node to subscribe before calling the on() method:

// Reference to our list of contacts
Webcom contactsNode = new Webcom(new WebcomApp("webcom-addressbook")).child("contacts");

// Subscribe to the 2 elements from "k"
contactsNode.startAt("k").limit(2).on(Query.Event.VALUE, new OnQuery() {
  @Override
  public void onComplete(DataSnapshot snapData, @Nullable String prevName) {
    // code to handle current data
    List<Contact> twoFromKContacts = snapData.valueList(Contact.class);
    Log.i("QUERY", "The 2 contacts from 'k' are:");
    for (Contact contact in twoFromKContacts) {
      Log.i("QUERY", contact.getFirstName());
    }
    // => lennon, macca
  }
  @Override
  public void onCancel(WebcomError error) {
    // event subscription was canceled because of permission reason
  }
  @Override
  public void onError(WebcomError error) {
    // an error occured
  }
});

In Swift, simply use the startAt() constraint value.

// Reference to our list of contacts
let manager = WebcomApplication.default.datasyncService.createManager()
let contactsNode = manager / "contacts"

// Subscribe to the 2 elements from "k"
contactsNode?.subscribe(to: .valueChange, constraint: .startAt(key: "k", limit: 2)) { event in
    guard let twoFromKContacts: [Contact] = event.value.decoded() else {
        return
    }
    print("The 2 contacts from 'k' are:", twoFromKContacts.map { $0.firstName }.joined(separator: ", "))
    // => lennon, macca
}