Tutorial: Write data

Serverless DatabaseData Structure Write data

To write data into a Webcom application database, three methods are available:

Method Description
set This method (over)writes data at a data node of the database.
update
(orĀ merge)
This method updates one or more child nodes of a given data node by merging them into this data node while preserving already existing child nodes.
push This method adds a child node to a given data node, with a unique and chronological ID. It is particularly useful to build lists of data (e.g. chat messages within a chat room).

Setting data

The set method writes new data into the data node on which it is applied. If it already contains data, all data at this node (including any child nodes) will be overwritten.

As an example related to an address book application, let's start by saving some contact details. Each contact contains a unique userName, as well as a firstName, a lastName, a phoneNumber, an email and a birthday.

First, we get the root data node of our Webcom application database, where to save our contact details. Our application is called "webcom-addressbook":

var adbk = new Webcom("webcom-addressbook");
Webcom adbk = new Webcom(new WebcomApp("webcom-addressbook"));

On iOS environments, the name of the Webcom application is commonly set within the Info.plist file.
In addition, we need a "manager", which is typically defined locally to a view controller.

let adbk = WebcomApplication.default
let manager = adbk.datasyncService.createManager()

Then we eventually use the set method to create an object under the "contacts" node.

In our application, this object is intended to represent a contact within our address book and consists in a child node named by the userName of this contact, which in turn has one child node for each attribute of the contact: firstName, lastName, phoneNumber, email and birthday. The object representing the contact must actually be serializable to a JSON object whose properties are key-value pairs for all the attributes listed above.

The set() method accepts strings, numbers, booleans, null, arrays or JSON objects. Passing null actually removes the data at the node on which it is called. In our example we simply pass the JSON object representing our contact details:

const maccaNode = adbk.child("contacts/macca"); // get a reference on the data node representing the "macca" contact
const macca = { // our JSON object representing the contact details for "macca"
    birthday: "June 18, 1942",
    firstName: "Paul",
    lastName: "McCartney",
    phoneNumber: "020 1234 6541",
    email: "paulo@apple.com"
};
maccaNode.set(macca);

The set() method accepts low-level types such as strings, integers (int and long), doubles, floats, null, booleans, arrays and maps. In addition, and more interestingly, it also accepts Plain Old Java Objects (or POJO).

class Contact { // our POJO class
    String birthday, firstName, lastName, phoneNumber, email, address;
    // default constructor
    Contact() { }
    // constuctor
    Contact(String birthday, String firstName, String lastName, String phoneNumber, String email, String address) {
        this.birthday = birthday;
        this.firstName = firstName;
        this.lastName = lastName;
        this.phoneNumber = phoneNumber;
        this.email = email;
        this.address = address;
    }
    // getters
    String getBirthday() { return birthday; }
    String getFirstName() { return firstName; }
    String getLastName() { return lastName; }
    String getPhoneNumber() { return phoneNumber; }
    String getEmail() { return email; }
    String getAddress() { return address; }
    // setters
    Contact setBirthday(String birthday) {
        this.birthday = birthday;
        return this;
    }
    Contact setFirstName(String firstName) {
        this.firstName = firstName;
        return this;
    }
    Contact setLastName(String lastName) {
        this.lastName = lastName;
        return this;
    }
    Contact setPhoneNumber(String phoneNumber) {
        this.phoneNumber = phoneNumber;
        return this;
    }
    Contact setEmail(String email) {
        this.email = email;
        return this;
    }
    Contact setAddress(String address) {
        this.address = address;
        return this;
    }
};
Contact macca = new Contact("June 18, 1942", "Paul", "McCartney", "020 1234 6541", "paulo@apple.com", null);
Webcom maccaNode = adbk.child("contacts/macca"); // get a reference on the data node representing the "macca" contact
maccaNode.set(macca);

The set() method accepts primitive types such as strings, numbers, booleans, arrays and dictionaries. In addition, and more interestingly, it also accepts classes that conform to the Codable protocol (and so automatically manage serialization and deserialization).

struct Contact: Codable { // our application-specific value type
  var birthday: String? = nil
  var firstName: String
  var lastName: String
  var phoneNumber: String? = nil
  var email: String? = nil
  var address: String? = nil
}
let macca = Contact(birthday: "June 18, 1942", firstName: "Paul", lastName: "McCartney", phoneNumber: "020 1234 6541", email: "paulo@apple.com")
let maccaNode = manager / "contacts/macca" // get a reference on the data node representing the "macca" contact
maccaNode?.set(macca)

As the database relies on a tree-like structure, we could also have saved the contact properties at the "contacts" child node as a map or dictionary associating the "macca" key to the value of the previously specified contact:

const contactsNode = adbk.child("contacts");
contactsNode.set({
  macca: macca
});
Webcom contactsNode = adbk.child("contacts");
contactsNode.set(new HashMap() {
  { put("macca", macca); }
});
let contactsNode = manager / "contacts"
contactsNode?.set(["macca": macca])

Both examples result in the same database with the following content:

{
  "contacts": {
    "macca": {
      "birthday": "June 18, 1942",
      "firstName": "Paul",
      "lastName": "McCartney",
      "phoneNumber": "020 1234 6541",
      "email": "paulo@apple.com"
 	}
  }
}

If some data already existed at the "contacts" node (typically some already existing contacts), then

  • the first piece of code would overwrite only the "macca" sub-node, preserving the data of all other child nodes of the "contact" node.
  • the second piece of code would overwrite the node as a whole, erasing all pre-existent child nodes.

Merging data

The update method makes it possible to update several child nodes of a given data node at a time. When called on a node, it overwrites the passed child nodes of this node and preserves any other already existing child node of this node. This preservation feature differentiates this method from the set one (which overwrite the whole node).

In the previous example, we can complete the "contacts" node instead of overwriting it:

The update() method works the same way as the set() one.

const lennon = { // our JSON object representing the contact details for a new "lennon" child node
   birthday: "October 9, 1940",
   firstName: "John",
   lastName: "Lennon",
   email: "johnandyoko@apple.com"
};
var contactsNode = adbk.child("contacts"); // get a reference on the "contacts" data node
contactsNode.update({
    lennon: lennon
});

The update() method works the same way as the set() one.

Contact lennon = new Contact("October 9, 1940", "John", "Lennon", null, "johnandyoko@apple.com", null); // POJO representing the new "lennon" child node
Webcom contactsNode = adbk.child("contacts"); // get a reference on the "contacts" data node
contactsNode.update(new HashMap() {
    { put("lennon", lennon); }
});

The merge() method works the same way as the set() one.

let lennon = Contact(birthday: "October 9, 1940", firstName: "John", lastName: "Lennon", email: "johnandyoko@apple.com")
let contactsNode = manager / "contacts" // get a reference on the "contacts" data node
contactsNode?.merge(with: ["lennon": lennon])

As a result, when the set and update examples are run sequentially, the database content looks like:

{
  "contacts": {
    "macca": {
      "birthday": "June 18, 1942",
      "firstName": "Paul",
      "lastName": "McCartney",
      "phoneNumber": "020 1234 6541",
      "email": "paulo@apple.com"
 	},
    "lennon": {
      "birthday": "October 9, 1940",
      "firstName": "John",
      "lastName": "Lennon",
      "email": "johnandyoko@apple.com"
 	}
  }
}

Using set and update methods asynchronously

The set and update methods actually work asynchronously and additionally either return a promise or accept a completion callback, which is completed or called when the write has been committed on the Webcom servers.

For example in the previous case, if you would like to know when your contact's data has been committed, you can use the returned promise or add a completion callback. When the data writing fails for some reason, the promise or the callback is rejected or called with an error describing the failure. When the data is successfully committed, the promise or the callback is completed or called with no error.

The JavaScript API works with promises:

contactsNode.set({macca: macca})
    .then(() => contactsNode.update({lennon: lennon})
        .then(() => alert("all contacts have been saved!"))
        .catch(error => alert("lennon contact could not be saved: " + error)))
    .catch(error => alert("macca contact could not be saved: " + error));

The Java API works with callbacks implementing the OnComplete interface.

contactsNode.set(
    new HashMap() ,
    new OnComplete() {
        @Override
        public void onComplete() {
            contactsNode.update(new HashMap() , new OnComplete() {
                @Override
                public void onComplete() {
                    // all contacts have been saved
                }
                @Override
                public void onError(WebcomError error) {
                    // lennon contact could not be saved.
                }
            });
        }
        @Override
        public void onError(WebcomError error) {
            // macca contact could not be saved.
        }
    }
);

The Swift API works with callbacks:

contactsNode?.set(["macca": macca]) { result in
    switch result {
    case let .failure(error):
        print("macca contact could not be saved:", error.description)
    case .success:
        contactsNode?.merge(with: ["lennon": lennon]) { result in
            switch result {
            case let .failure(error):
                print("lennon contact could not be saved:", error.description)
            case .success:
                print("all contacts have been saved!")
            }
        }
    }
}

Pushing data into lists

In our address book example, we could imagine that the address book is shared between several users. If a user adds a new contact it will be stored in the database. But in a shared address book many users may add contacts at the same time. If two users write simultaneously contacts into the "contacts" node, then one of the contact is likely to be deleted by the other.

To solve this problem, Webcom provides the push method. This method returns a new child node with a unique ID and sets some data on this newly created node (using the previously explained set method). The generated ID is based on a timestamp, so that the IDs of the of created child nodes are ordered chronologically. When using this method, several users can add children to the same data node of a database at the same time without any write conflict.

In the following example we show how to add contacts to our shared address book using the push method :

The push() method works the same way as the set() and update() ones.

const otherContactsNode = adbk.child("otherContacts");
otherContactsNode.push({
  birthday: "February 25, 1943",
  firstName: "George",
  lastName: "Harrisson",
  phoneNumber: "020 1234 8879"
});
otherContactsNode.push({
  birthday: "July 7, 1940",
  firstName: "Ringo",
  lastName: "Starr",
  phoneNumber: "020 1234 4561"
});

The push() method works the same way as the set() and update() ones.

Webcom otherContactsNode = adbk.child("otherContacts");
otherContactsNode.push(new Contact("February 25, 1943", "George", "Harrisson", "020 1234 8879", null, null));
otherContactsNode.push(new Contact("July 7, 1940", "Ringo", "Starr", "020 1234 4561", null, null));

The push() method works the same way as the set() and merge() ones.

let otherContactsNode = manager / "otherContacts"
let contact1 = Contact(birthday: "February 25, 1943", firstName: "George", lastName: "Harrisson", phoneNumber: "020 1234 8879")
otherContactsNode?.push(contact1)
let contact2 = Contact(birthday: "July 7, 1940", firstName: "Ringo", lastName: "Starr", phoneNumber: "020 1234 4561")
otherContactsNode?.push(contact2)

As a result, our database content now looks like (note the unique IDs generated by the push method):

{
  "otherContacts": {
    "-JtJIbH-AMSjUj-e-QAR": {
      "birthday": "February 25, 1943",
      "firstName": "George",
      "lastName": "Harrisson",
      "phoneNumber": "020 1234 8879"
    },
    "-JtJIbHINoEONq8fxNds": {
      "birthday": "July 7, 1940",
      "firstName": "Ringo",
      "lastName": "Starr",
      "phoneNumber": "020 1234 4561"
    }
  }
}

If you need to retrieve the generated ID, the push method returns a reference to the created child node:

// Generate a new contact node
var newContactNode = otherContactsNode.push({
    firstName: "Yoko",
    lastName: "Ono",
    phoneNumber: "020 1234 0999"
});

// Get the unique ID of this node
var newContactID = newContactNode.name();
// Generate a new contact node
Webcom newContactNode = otherContactsNode.push(new Contact(null, "Yoko", "Ono", "020 1234 0999", null, null));
// Get the unique ID of this node
String newContactID = newContactNode.name();
let newContact = Contact(firstName: "Yoko", lastName: "Ono", phoneNumber: "020 1234 0999")
// Generate a new contact node
let newContactNode = otherContactsNode?.push(newContact)
// Get the unique ID of this node
let newContactID = newContactNode?.name