This API is deprecated and will be turned down in January 2019. Migrate to Cloud Firestore or another data store as soon as possible to avoid disruptions to your application.

Data Modeling Guidelines

This page describes some special considerations and caveats in designing efficient, correct data models using the Realtime API. Briefly, these are:

  • Do not attempt to reflect the natural ordering of a collaborative list in the list's index ordering.
  • Compound operations have no effect on conflict resolution.
  • You can ensure atomicity by sacrificing granularity.

The rest of this page provides code samples and analysis to explain how to work within these constraints when building your data model.

Preserving natural ordering

Collaborative lists are ordered collections—the index order of items in a collaborative list is preserved as the list is edited.

In a naturally ordered list, the proper ordering of items in the collection can be calculated by examining the objects in the collection. Sorted lists of all kinds are naturally ordered. However, if the items in a list have a natural ordering, that order will not be preserved in collaboration.

Conversely, artificially ordered collections have some ordering that is not implied by the contents of the objects in the collection. A string is an artificially ordered collection, because the order of characters in a string is not dictated by the characters themselves. Rather, the ordering is imposed by external considerations (the words that a user wanted to represent with that string). Other examples of artificially ordered collections include: items on a meeting agenda, squares on a Monopoly board, messages in a chat log.

The conflict resolution system used by the Realtime API preserves the index order of a collaborative list when conflicting edits are issued by collaborators. It is entirely unaware of any natural ordering of items in the list. Therefore: You must not attempt to reflect the natural ordering of a collaborative list in the list's index ordering. Edits from collaborators can and will break the ordering you are trying to impose.

To see why, consider this code:

// THIS IS FUNDAMENTALLY BROKEN. DO NOT USE!
var myList = model.createList();

function brokenInsert(item) {
  var i = 0;
  while (myList.get(i) < item) {
    i++;
  }
  myList.insert(min(i - 1, 0), item);
}

This method will work just fine, as long as there are no collaborators on the document. But consider what happens when two users insert into a "sorted" list of this kind simultaneously.

Original list contents: [ "A", "B", "E", "F"]

Alice calls: brokenInsert("C")

Bob calls: brokenInsert("D")

You'd expect the list to contain ["A", "B", "C", "D", "E", "F"]. But both users are inserting at the same list index (2), so there are conflicting edits that must be resolved. If Alice's edit gets to the server slightly before Bob's, the conflict resolution code will invoke the Insert vs. Insert resolution rule that states "when two inserts occur at the same location, older edits are shifted over by newer edits." Therefore, after conflict resolution the list will contain ["A", "B", **"D"**, **"C"**, "E", "F"]. The list's index ordering no longer reflects the natural ordering of its contents.

Therefore, if the contents of a collaborative list have a natural ordering, don't attempt to encode that natural ordering in the list's index ordering. Instead, just append any new items to the end of the list and write a method that presents an ordered VIEW of the list:

var myList = model.createList();

function getSortedList() {
  var exportList = myList.asArray();
  exportList.sort();
  return exportList();
}

Note that the implementation above lazily recalculates the entire sorted list view when it is requested. This might be appropriate for an infrequently requested sorted view. If this is too inefficient, it's possible to build a much more efficient sorted view by listening for update events on the collaborative list and partially updating the sorted view as the collaborative list is modified.

List size constraints

In general, it is impossible to control the size of a collaborative list. Collaboration can always cause a list to grow without bound or become empty. Consider this example of code that tries to ensure that a list never has more than three items:

// THIS IS FUNDAMENTALLY BROKEN. DO NOT USE!
var myList = model.createList();
var maxLen = 5;

function brokenPrepend(item) {
  model.beginCompoundOperation();
  myList.insert(0, item);
  if (myList.length > maxLen) {
    myList.remove(myList.length - 1);
  }
  model.endCompoundOperation();
}

This works for one user, but it breaks in collaboration. Consider what happens when two users insert into this kind of list simultaneously:

Original list contents: ["A", "B", "C"]

Alice calls: constrainer.prepend("1")

Bob calls: constrainer.prepend("2")

You might expect the list to contain something like ["1", "2", "A"], but you'll actually end up with ["1", "2", "A", "B"]. This is because although both users are deleting the last index in the list, the conflict resolution system will result in just a single item being deleted from the list.

Note that the use of a compound operation above does not protect us from this conflict resolution problem. Compound operations have no effect on conflict resolution.

If you need to constrain the length of a collaborative list, don't attempt to constrain the list directly. Instead, present a constrained view of the list:

var myList = model.createList();
var maxLen = 5;

function getConstrainedList() {
  var exportList = myList.asArray();
  return exportList.slice(0, maxLen);
}

Once again, you are providing a view onto the data that is constrained to your requirement.

Atomicity

Some data models have complex constraints that require a number of values in the data model to change together in lockstep. For example, imagine modeling a two- dimensional array using collaborative lists. In a non-collaborative system, you might write code like this:

// THIS IS FUNDAMENTALLY BROKEN. DO NOT USE!
var values = model.createMap();
values.put('2DArray', model.createList());

function brokenInsertColumn(index, value) {
  my2DArray = values.get('2DArray');
  model.beginCompoundOperation();
  for (var i = 0; i < my2DArray.length; i++) {
    my2DArray.get(i).add(value);
  }
  model.endCompoundOperation();
}

function brokenInsertRow(index, value) {
  my2DArray = values.get('2DArray');
  model.beginCompoundOperation();
  var newRow = model.createList();
  if (my2DArray.length > 0) {
    for (var i = 0; i < my2DArray.get(0).length; i++) {
      newRow.add(value);
    }
  }
  my2DArray.add(newRow);
  model.endCompoundOperation();
}

The code above doesn't work because if someone inserts a row at the same time another user inserts a column, there will be one row in the 2D array that has a different number of elements than the other rows. This happens because the client inserting the new row is unaware of the additional columns inserted by the other collaborator.

However, there is a solution of sorts—replace the entire 2D array in one operation, thus making the updates atomic:

var values = model.createMap();
values.put('2DArray', model.createList());

function cloneArray2d(array2d) {
  var newArray = model.createList();
  for (var i = 0; i < array2d.length; i++) {
    var row = array2d.get(i);
    var newRow = model.createList();
    for (var j = 0; j < row.length; j++) {
      newRow.add(row.get(j));
    }
    newArray.add(newRow);
  }
}

function workingInsertColumn(index, value) {
  my2DArray = cloneArray2d(values.get('2DArray'));
  model.beginCompoundOperation();
  for (var i = 0; i < my2DArray.length; i++) {
    my2DArray.get(i).add(value);
  }
  values.put('2DArray', my2DArray);
  model.endCompoundOperation();
}

function workingInsertRow(index, value) {
  my2DArray = cloneArray2d(values.get('2DArray'));
  model.beginCompoundOperation();
  var newRow = model.createList();
  if (my2DArray.length > 0) {
    for (var i = 0; i < my2DArray.get(0).length; i++) {
      newRow.add(value);
    }
  }
  my2DArray.add(newRow);
  values.put('2DArray', my2DArray);
  model.endCompoundOperation();
}

This implementation works perfectly well in collaboration. It doesn't matter if someone inserts a row while someone else inserts a column. Since each update replaces the entire 2D array, only one of the edits will occur. The other will appear to be reverted.

This illustrates a fundamental tradeoff in collaborative data models: You can ensure atomicity by sacrificing granularity. This reduced granularity may result in a choppy experience when updating user interfaces.

Collaborative math

Occasionally an application will need to fetch a value from the model, perform some calculation on it, and replace the value with the result. For example, imagine a system that allowed users to assign star ratings to movies and could display the average star rating for a movie. One might be tempted to implement the system this way:

// THIS IS FUNDAMENTALLY BROKEN. DO NOT USE!
var values = model.createMap();
values.put('stars', 0);
values.put('voterCount', 0);

function brokenRate(stars) {
  var totalStars = values['stars'];
  var voterCount = values['voterCount'];
  values.put('stars', stars + totalStars);
  values.put('voterCount', voterCount + 1);
}

function brokenGetAverage() {
  return values.get('stars') / values.get('voterCount');
}

This doesn't work because each user's map lookup for the current values of stars and voterCount isn't aware of simultaneous collaborator edits, resulting in dropped votes (or worse). The proper technique for this situation is to record every intermediate value and calculate the aggregate result on request:

var values = model.createMap();
values.put('votes', mode.createList());

function rate(stars) {
  values.get('votes').add(stars);
}

function getAverage() {
  var votes = values.get('votes');
  var total = 0;
  for (var i = 0; i < votes.length; i++) {
    total += votes.get(i);
  }
  return total / votes.length;
}

This implementation works because it plays to the strengths of the conflict resolution system. The conflict resolution system doesn't know how to resolve conflicts in calculating arbitrary mathematic expressions, but it does knows how to resolve conflicting list inserts.

This implementation is doing a bit of unnecessary extra work. You could optimize this further by tracking list insert and delete events and updating a cached value for totalStars as changes are made, thus eliminating the need to calculate the entire total from scratch on every call to getAverage.

Enviar comentarios sobre…