Document

In a document database such as Couchbase Lite, the primary entity stored in a database is called a document instead of a "row" or "record". This reflects the fact that a document can store more data, with more structure, than its equivalent in other databases.

In Couchbase Lite (as in Couchbase Server and CouchDB) a document's body takes the form of a JSON object — a collection of key/value pairs where the values can be different types of data such as numbers, strings, arrays or even nested objects. Every document is identified by a document ID, which can be automatically generated (as a UUID) or determined by the application; the only constraints are that it must be unique within the database, and it can't be changed.

In addition, a document can contain attachments, named binary blobs that are useful for storing large media files or other non-textual data. Couchbase Lite supports attachments of unlimited size, although the Sync Gateway currently imposes a 10MB limit for attachments synced to it.

Couchbase Lite keeps track of the change history of every document, as a series of revisions. This is somewhat like a version control system such as Git or Subversion, although its main purpose is not to be able to access old data, but rather to assist the replicator in deciding what data to sync and what documents have conflicts. Every time a document is created or updated, it is assigned a new unique revision ID. The IDs of past revisions are available, and the contents of past revisions may be available, but only if the revision was created locally and the database has not yet been compacted.

To summarize, a document has the following attributes:

Creating, Reading, Updating and Deleting documents (CRUD)


Couchbase Lite of course supports the typical database "CRUD" operations on documents: Create, Read, Update, Delete.

Creating documents

You can create a document with or without giving it an ID. If you don't need or want to define your own ID, call the Database method createDocument, and the ID will be generated randomly in the form of a Universally Unique ID (UUID), which looks like a string of hex digits. The uniqueness ensures that there is no chance of an accidental collision by two client apps independently creating different documents with the same ID, then replicating to the same server.

The following example shows how to create a document with an automatically-assigned UUID:

NSString* owner = [@"profile:" stringByAppendingString: userId];
NSDictionary* properties = @{@"type":       @"list",
                             @"title":      title,
                             @"created_at": currentTimeString,
                             @"owner":      owner,
                             @"members":    @[]};
CBLDocument* document = [database createDocument];
NSError* error;
if (![document putProperties: properties error: &error]) {
    [self handleError: error];
}
let owner = "profile".stringByAppendingString(userId)
let properties = [
    "type": "list",
    "title": title,
    "owner": owner,
    "memebers": []
]
let document = database.createDocument()
var error: NSError?
if document.putProperties(properties, error: &error) == nil {
    self.handleError(error)
}
Map<String, Object> properties = new HashMap<String, Object>();
properties.put("type", "list");
properties.put("title", title);
properties.put("created_at", currentTimeString);
properties.put("owner", "profile:" + userId);
properties.put("members", new
ArrayList<String>());
Document document = database.createDocument();
document.putProperties(properties); 
    
Map<String, Object> properties = new HashMap<String, Object>();
properties.put("type", "list");
properties.put("title", title);
properties.put("created_at", currentTimeString);
properties.put("owner", "profile:" + userId);
properties.put("members", new
ArrayList<String>());
Document document = database.createDocument();
document.putProperties(properties); 
    
var document = database.CreateDocument();
var properties = new Dictionary<string, object>()
    {
        {"type", "list"},
        {"title", "title"},
        {"created_at", DateTime.UtcNow.ToString ("o")},
        {"owner", "profile:" + userId},
        {"members", new List<string>()}
    };
var rev = document.PutProperties(properties);
Debug.Assert(rev != null);

If you do want to choose the document's ID, just call the Database method documentWithID, just as you would to retrieve an existing document. If the document doesn't exist yet, you still get a valid Document object, it just doesn't have any revisions or contents yet. The first time you save the document, it will be added persistently to the database. If a document does already exist with the same ID, saving the document will produce a conflict error.

The following example shows how to create a document with an custom ID:

NSDictionary* properties = @{@"title":      @"Little, Big",
                             @"author":     @"John Crowley",
                             @"published":  1982};
CBLDocument* document = [database documentWithID: @"978-0061120053"];
NSError* error;
if (![document putProperties: properties error: &error]) {
    [self handleError: error];
}
    
let properties =
[
    "title": "Little, Big",
    "author": "John Crowley",
    "published":  1982
]
let document = database.documentWithID("978-0061120053")
var error: NSError?
if document.putProperties(properties, error: &error) == nil {
    self.handleError(error)
}
No code example is currently available.
No code example is currently available.
var properties = new Dictionary<string, object>
    {
        {"title", "Little, Big"},
        {"author", "John Crowley"},
        {"published", 1982}
    };
var document = database.GetDocument("978-0061120053");
Debug.Assert(document != null);
var rev = document.PutProperties(properties);
Debug.Assert(rev != null);
Tip:It's up to you whether to assign your own IDs or use random UUIDs. If the documents are representing entities that already have unique IDs — like email addresses or employee numbers — then it makes sense to use those, especially if you need to ensure that there can't be two documents representing the same entity. For example, in a library cataloging app, you wouldn't want two librarians to independently create duplicate records for the same book, so you might use the book's ISBN as the document ID to enforce uniqueness.

Reading documents

To retrieve a Document object given its ID, call the Database method documentWithID. As described in the previous section, if there is no document with this ID, this method will return a valid but empty Document object. (If you would rather get a null/nil result for a nonexistent document, call existingDocumentWithID instead.)

Document objects, like document IDs, are unique. That means that there is never more than one Document object in memory that represents the same document. If you call documentWithID multiple times with the same ID, you get the same Document object every time. This helps conserve memory, and it also makes it easy to compare Document object references (pointers) — you can just use == to check whether two references refer to the same document.

Loading a Document object doesn't immediately read its properties from the database. Those are loaded on demand, when you call an accessor method like getProperties (or access the Objective-C property properties). The properties are represented using whatever platform type is appropriate for a JSON object. In Objective-C they're an NSDictionary, in Java a Map<String,Object>.

Here's a simple example of getting a document's properties:

CBLDocument* doc = [database documentWithID: _myDocID];
// We can directly access properties from the document object:
NSString* title = doc[@"title"];
// Or go through its properties dictionary:
NSDictionary* properties = doc.properties;
NSString* owner = properties[@"owner"];
let doc = database.documentWithID(myDocID)
// We can directly access properties from the document object:
let title = doc["title"] as? String
// Or go through its properties dictionary:
let properties = doc.properties;
let owner = properties["owner"] as? String;
Document doc = database.getDocument(myDocId);
// We can directly access properties from the document object:
doc.getProperty("title");
// Or go through its properties dictionary:
Map<String, Object> properties = doc.getProperties();
String owner = (String) properties.get("owner");
Document doc = database.getDocument(myDocId);
// We can directly access properties from the document object:
doc.getProperty("title");
// Or go through its properties dictionary:
Map<String, Object> properties = doc.getProperties();
String owner = (String) properties.get("owner");
var doc = database.GetDocument(myDocId);
// We can directly access properties from the document object:
doc.GetProperty("title");
// Or go through its properties dictionary:
var owner = doc.Properties["owner"];
Note:The getProperties method is actually just a convenient shortcut for getting the Document's currentRevision and then getting its properties — since a document usually has multiple revisions, the properties really belong to a revision. Every existing document has a current revision (in fact that's how you can tell whether a document exists or not.) Almost all the time you'll be accessing a document's current revision, which is why the convenient direct properties accessor exists.

Updating documents

There are two methods that update a document: putProperties and update. We'll cover them both, then explain why they're different.

putProperties is simpler: given a new JSON object, it replaces the document's body with that object. Actually what it does is creates a new revision with those properties and makes it the document's current revision.

CBLDocument* doc = [database documentWithID: _myDocID];
NSMutableDictionary* p = [doc.properties mutableCopy];
p[@"title"] = title;
p[@"notes"] = notes;
NSError* error;
if (![doc putProperties: p error: &error]) {
    [self handleError: error];
}
let doc = database.documentWithID(myDocID)
var properties = doc.properties
properties["titie"] = title
properties["notes"] = notes
var error: NSError?
if doc.putProperties(properties, error: &error) == nil {
    self.handleError(error)
}
No code example is currently available.
No code example is currently available.
var doc = database.GetDocument(myDocId);
var p = new Dictionary<string, object>(doc.Properties)
    {
        {"title", title},
        {"notes", notes}
    };
var rev = doc.PutProperties(p);
Debug.Assert(rev != null);
Note:Multiple updates using putProperties within a single transaction are currently not supported. For more information, see issue #256 and issue #220.

update instead takes a callback function or block (the details vary by language). It loads the current revision's properties, then calls this function, passing it an UnsavedRevision object, whose properties are a mutable copy of the current ones. Your callback code can modify this object's properties as it sees fit; after it returns, the modified revision is saved and becomes the current one.

CBLDocument* doc = [database documentWithID: _myDocID];
NSError* error;
if (![doc update: ^BOOL(CBLUnsavedRevision *newRev) {
    newRev[@"title"] = title;
    newRev[@"notes"] = notes;
} error: &error]) {
    [self handleError: error];
}
let doc = database.documentWithID(myDocID)
var error: NSError?
doc.update({ (newRev) -> Bool in
    newRev["titie"] = title
    newRev["notes"] = notes
}, error: &error)
if error != nil {
    self.handleError(error)
}
Document doc = database.getDocument(myDocId);
doc.update(new Document.DocumentUpdater() {
    @Override
    public boolean update(UnsavedRevision newRevision) {
        Map<String, Object> properties = newRevision.getUserProperties();
        properties.put("title", title);
        properties.put("notes", notes);
        newRevision.setUserProperties(properties);
        return true;
    }
})
Document doc = database.getDocument(myDocId);
doc.update(new Document.DocumentUpdater() {
    @Override
    public boolean update(UnsavedRevision newRevision) {
        Map<String, Object> properties = newRevision.getUserProperties();
        properties.put("title", title);
        properties.put("notes", notes);
        newRevision.setUserProperties(properties);
        return true;
    }
})
var doc = database.GetDocument(myDocId);
doc.Update((UnsavedRevision newRevision) => 
{
    var properties = newRevision.Properties;
    properties["title"] = title;
    properties["notes"] = notes;
    return true;
});

Whichever way you save changes, you need to consider the possibility of update conflicts. Couchbase Lite uses Multiversion Concurrency Control (MVCC) to guard against simultaneous changes to a document. (Even if your app code is single-threaded, the replicator runs on a background thread and can be pulling revisions into the database at the same time you're making changes.) Here's the typical sequence of events that creates an update conflict:

  1. Your code reads the document's current properties, and constructs a modified copy to save
  2. Another thread (perhaps the replicator) updates the document, creating a new revision with different properties
  3. Your code updates the document with its modified properties

Clearly, if your update were allowed to proceed, the change from step 2 would be overwritten and lost. Instead, the update will fail with a conflict error. Here's where the two API calls differ:

  1. putProperties simply returns the error to you to handle. You'll need to detect this type of error, and probably handle it by re-reading the new properties and making the change to those, then trying again.
  2. update is smarter: it handles the conflict error itself by re-reading the document, then calling your block again with the updated properties, and retrying the save. It will keep retrying until there is no conflict.
Tip:Of the two techniques, calling update may be a bit harder to understand initially, but it actually makes your code simpler and more reliable. We recommend it. (Just be aware that your callback block can be called multiple times.)

Deleting documents

The delete method (deleteDocument: in Objective-C) deletes a document:

CBLDocument* doc = [database documentWithID: _myDocID];
NSError* error;
if (![doc deleteDocument: &error]) {
    [self handleError: error];
}
let doc = database.documentWithID(myDocID)
var error: NSError?
if !doc.deleteDocument(&error) {
    self.handleError(error)
}
Document task = (Document) database.getDocument("task1");
task.delete(); 
Document task = (Document) database.getDocument("task1");
task.delete(); 
var doc = database.GetDocument(myDocId);
doc.Delete();

Deleting a document actually just creates a new revision (informally called a "tombstone") that has the special _deleted property set to true. This ensures that the deletion will replicate to the server, and then to other clients that pull from that database, just like any other document revision.

Note:It's possible for the delete call to fail with a conflict error, since it's really just a special type of putProperties. In other words, something else may have updated the document at the same time you were trying to delete it. It's up to your app whether it's appropriate to retry the delete operation.

If you need to preserve one or more fields in a document that you want to delete (like a record of who deleted it or when it was deleted) you can avoid the delete method; just update the document and set the UnsavedRevision's deletion property to true, or set JSON properties that include a "_deleted" property with a value of true. You can retain all of the fields, as shown in the following example, or you can remove specified fields so that the tombstone revision contains only the fields that you need.

CBLDocument* doc = [database documentWithID: _myDocID];
NSError* error;
if (![doc update: ^BOOL(CBLUnsavedRevision *newRev) {
    newRev.isDeletion = YES;  // marks this as a 'tombstone'
    newRev[@"deleted_at"] = currentTimeString;
} error: &error]) {
    [self handleError: error];
}
doc.update({ (newRev) -> Bool in
    newRev.isDeletion = true
    newRev["deleted_at"] = currentTimeString
    return true
}, error: &error)
if error != nil {
    self.handleError(error)
}
Document doc = database.getDocument(myDocId);
doc.update(new Document.DocumentUpdater() {
    @Override
    public boolean update(UnsavedRevision newRevision) {
        newRevision.setIsDeletion(true);
        Map<String, Object> properties = newRevision.getUserProperties();
        properties.put("deleted_at", currentTimeString);
        newRevision.setUserProperties(properties);
        return true;
    }
})
Document doc = database.getDocument(myDocId);
doc.update(new Document.DocumentUpdater() {
    @Override
    public boolean update(UnsavedRevision newRevision) {
        newRevision.setIsDeletion(true);
        Map<String, Object> properties = newRevision.getUserProperties();
        properties.put("deleted_at", currentTimeString);
        newRevision.setUserProperties(properties);
        return true;
    }
})
var doc = database.GetDocument(myDocId);
doc.Update((UnsavedRevision newRevision) => 
{
    newRevision.IsDeletion = true;
    newRevision.Properties["deleted_at"] = currentTimeString;
    return true;
});

Document change notifications


You can register for notifications when a particular document is updated or deleted. This is very useful if you're display a user interface element whose content is based on the document: use the notification to trigger a redisplay of the view.

You can use change events for the following purposes:

[[NSNotificationCenter defaultCenter] addObserverForName: kCBLDocumentChangeNotification
            object: myDocument
             queue: nil
        usingBlock: ^(NSNotification *n) {
            CBLDatabaseChange* change = n.userInfo[@"change"];
            NSLog(@"There is a new revision, %@", change.revisionID);
            [self setNeedsDisplay: YES];  // redraw the view
        }
];
NSNotificationCenter.defaultCenter().addObserverForName(kCBLDocumentChangeNotification, object: myDocument, queue: nil) { 
    (notification) -> Void in
        if let change = notification.userInfo!["change"] as? CBLDatabaseChange {
            NSLog("This is a new revision, %@", change.revisionID);
            set.setNeedsDisplay(true)
        }
}
Document doc = database.createDocument();
doc.addChangeListener(new Document.ChangeListener() { 
    @Override 
    public void changed(Document.ChangeEvent event) { 
        DocumentChange docChange = event.getChange();
        String msg = "New revision added: %s. Conflict: %s"; 
        msg = String.format(msg,
        docChange.getAddedRevision(), docChange.isConflict()); 
        Log.d(TAG, msg);
        documentChanged.countDown();
    } 
}); 
doc.createRevision().save();
Document doc = database.createDocument();
doc.addChangeListener(new Document.ChangeListener() { 
    @Override 
    public void changed(Document.ChangeEvent event) { 
        DocumentChange docChange = event.getChange();
        String msg = "New revision added: %s. Conflict: %s"; 
        msg = String.format(msg,
        docChange.getAddedRevision(), docChange.isConflict()); 
        Log.d(TAG, msg);
        documentChanged.countDown();
    } 
}); 
doc.createRevision().save();
var doc = database.CreateDocument();
doc.Change += (sender, e) =>
{
    var change = e.Change;
    var documentId = change.DocumentId;
    var revisionId = change.RevisionId;
    var isConflict = change.IsConflict;
    var addedRev = change.AddedRevision;
};

Conflicts


So far we've been talking about a conflict as an error that occurs when you try to update a document that's been updated since you read it. In this scenario, Couchbase Lite is able to stop the conflict before it happens, giving your code a chance to re-read the document and incorporate the other changes.

However, there's no practical way to prevent a conflict when the two updates are made on different instances of the database. Neither app even knows that the other one has changed the document, until later on when replication propagates their incompatible changes to each other. A typical scenario is:

  1. Molly creates DocumentA; the revision is 1-5ac
  2. DocumentA is synced to Naomi's device; the latest revision is still 1-5ac
  3. Molly updates DocumentA, creating revision 2-54a
  4. Naomi makes a different change to DocumentA, creating revision 2-877
  5. Revision 2-877 is synced to Molly's device, which already has 2-54a, putting the document in conflict
  6. Revision 2-54a is synced to Naomi's device, which already has 2-877, similarly putting the local document in conflict

At this point, even though DocumentA is in a conflicted state, it needs to have a current revision. That is, when your app calls getProperties, Couchbase Lite has to return something. It chooses one of the two conflicting revisions (2-877 and 2-54a) as the "winner". The choice is deterministic, which means that every device that is faced with the same conflict will pick the same winner, without having to communicate. In this case it just compares the revision IDs "2-54a" and "2-877" and picks the higher one, "2-877".

To be precise, Couchbase Lite uses the following rules to handle conflicts:

Note:Couchbase Lite does not automatically merge the contents of conflicts. Automated merging sometimes works, but in many cases it'll give wrong results; only you know your document schemas well enough to decide how conflicts should be merged.

In some cases this simple "one revision wins" rule is good enough. For example, in a grocery list if two people rename the same item, one of them will just see that their change got overwritten, and may do it over again. But usually the details of the document content are more important, so the application will want to detect and resolve conflicts.

Note:Resolving conflicts can also save the space in the database. Conflicting revisions stay in the database indefinitely until resolved, even surviving compactions. Therefore, it makes sense to deal with the conflict by at least deleting the non-winning revision.

Another reason to resolve conflicts is to implement business rules. For example, if two sales associates update the same customer record and it ends up in conflict, you might want the sales manager to resolve the conflict and "hand merge" the two conflicting records so that no information is lost.

There are two alternative ways to resolve a conflict:

The following example shows how to resolve a conflict:

CBLDocument* doc = [database documentWithID: _myDocID];
NSError* error;
NSArray* conflicts = [doc getConflictingRevisions: &error];
if (conflicts.count > 1) {
    // There is more than one current revision, thus a conflict!
    [database inTransaction: ^BOOL{
        // Come up with a merged/resolved document in some way that's
        // appropriate for the app. You could even just pick the body of
        // one of the revisions.
        NSDictionary* mergedProps = [self mergeRevisions: conflicts];
    
        // Delete the conflicting revisions to get rid of the conflict:
        CBLSavedRevision* current = doc.currentRevision;
        for (CBLSavedRevision* rev in conflicts) {
            CBLUnsavedRevision newRev = [rev createRevision];
            if (rev == current) {
                newRev.properties = mergedProps; // add the merged revision
            } else {
                newRev.isDeletion = YES;  // mark other conflicts as deleted
            }
            // saveAllowingConflict allows 'rev' to be updated even if it
            // is not the document's current revision.
            [newRev saveAllowingConflict: &error];
        }
    }];
}
let doc = database.documentWithID(myDocID)
var error: NSError?
if let conflicts = doc.getConflictingRevisions(&error) as? [CBLSavedRevision]{
    if conflicts.count > 1 {
        // There is more than one current revision, thus a conflict!
        database.inTransaction({ () -> Bool in
            // Come up with a merged/resolved document in some way that's
            // appropriate for the app. You could even just pick the body of
            // one of the revisions.
            var mergedProps = self.mergeRevisions(conflicts)
            // Delete the conflicting revisions to get rid of the conflict:
            var current = doc.currentRevision
            for rev in conflicts {
                var newRev = current.createRevision()
                if rev == current {
                    newRev.properties = mergedProps
                } else {
                    newRev.isDeletion = true
                }
                // saveAllowingConflict allows 'rev' to be updated even if it
                // is not the document's current revision.
                newRev.saveAllowingConflict(&error)
            }
        })
    }
}
 
// Create a conflict on purpose 
Document doc = database.createDocument(); 
SavedRevision rev1 = doc.createRevision().save();
SavedRevision rev2a = rev1.createRevision().save(); 
SavedRevision rev2b = rev1.createRevision().save(true);
// rev2a will end up being picked as the winner, but let's manually 
// choose rev2b as the winner. First, delete rev2a, which will 
// cause rev2b to be the current revision. 
SavedRevision deleteRevision = rev2a.deleteDocument();
            
// Finally create a new revision rev3 based on rev2b
SavedRevision rev3 = rev2b.createRevision().save(true); 
                    
 
// Create a conflict on purpose 
Document doc = database.createDocument(); 
SavedRevision rev1 = doc.createRevision().save();
SavedRevision rev2a = rev1.createRevision().save(); 
SavedRevision rev2b = rev1.createRevision().save(true);
// rev2a will end up being picked as the winner, but let's manually 
// choose rev2b as the winner. First, delete rev2a, which will 
// cause rev2b to be the current revision. 
SavedRevision deleteRevision = rev2a.deleteDocument();
            
// Finally create a new revision rev3 based on rev2b
SavedRevision rev3 = rev2b.createRevision().save(true); 
                    
var doc = database.GetDocument(myDocId);
var conflicts = doc.ConflictingRevisions.ToList();
if (conflicts.Count > 1)
{
    // There is more than one current revision, thus a conflict!
    database.RunInTransaction(() => 
    {
        var mergedProps = MergeRevisions(conflicts);
        var current = doc.CurrentRevision;
        foreach(var rev in conflicts)
        {
            var newRev = rev.CreateRevision();
            if (rev == current)
            {
                newRev.SetProperties(mergedProps);
            }
            else
            {
                newRev.IsDeletion = true;
            }
            // saveAllowingConflict allows 'rev' to be updated even if it
            // is not the document's current revision.
            newRev.SaveAllowingConflict();
        }
        return true;
    });
}

Document Conflict FAQ

What if both devices make the same change to the document? Is that a conflict?
No. The revision ID is derived from a digest of the document body. So if two clients make identical changes, they end up with identical revision IDs, and Couchbase Lite (and the Sync Gateway) treat these as the same revision.
I deleted a document, but the it's still in the database, only now its properties are different. What happened?
Sounds like the document was in conflict and you didn't realize it. You deleted the winning revision, but that made the other (losing) revision become the current one. If you delete the document again, it'll actually go away.
How can I get the properties of the common ancestor revision, to do a three-way merge?
You can't always. Couchbase Lite isn't a version-control system and doesn't preserve old revision bodies indefinitely. But if the ancestor revision used to exist in your local database, and you haven't yet compacted the database, you can still get its properties. Get the parentRevision property of the current revision to get the ancestor, then see if its properties are still non-null.
How can I tell if a document has a conflict?
Call its getConflictingRevisions method and see if more than one revision is returned.
How can I tell if there are any conflicts in the database?
Use an all-documents query with the onlyConflicts mode.

Purging documents


Purging a document is different from deleting it; it's more like forgetting it. The purge method removes all trace of a document (and all its revisions and their attachments) from the local database. It has no effect on replication or on remote databases, though.

Purging is mostly a way to save disk space by forgetting about replicated documents that you don't need anymore. It has some slightly weird interactions with replication, though. For example, if you purge a document, and then later the document is updated on the remote server, the next replication will pull the document into your database again.

Special Properties


The body of a document contains a few special properties that store metadata about the document. For the most part you can ignore these since the API provides accessor methods for the same information, but it can still be helpful to know what they are if you encounter them.

Note:A leading underscore always denotes a reserved property—don’t use an underscore prefix for any of your own properties, and don't change the value of any reserved property.