Documents
Overview
Document Structure
In Couchbase Lite the term 'document' refers to an entry in the database; a record, or row in a table if you like.
Each document has an ID (primary key in other databases) by which it can be located. This ID can be automatically generated (as a UUID) or specified programmatically; the only constraints are that it must be unique within the database, and it can’t be changed. The document also has a value which contains the actual application data. This value is stored as a dictionary collection of key-value (k-v) pairs where the values themselves may comprise different types of data such as numbers, strings, arrays or even nested objects — see: Data Types
Data Encoding
The document body is stored in an internal, efficient, binary form (
Fleece
).
This internal form is easily converted into a manageable native dictionary format for manipulation in applications.
Fleece data is stored in the smallest format that will hold the value, whilst maintaining the integrity of the value.
Data Types
The Document
class offers a set of property accessors for various scalar types, including boolean, integers, floating-point and strings.
These accessors take care of converting to/from JSON encoding, and make sure you get the type you’re expecting.
So your document content may well comprise one or more supporting data types such as:
|
|
|
|
In addition to these basic data types Couchbase Lite provides for the following:
|
|
|
JSON
Couchbase Lite also provides for the direct handling of JSON data implemented in most cases by the provision of a toJSON()
method on appropriate API classes (for example, on MutableDocument, Dictionary, Blob and Array) — see Working with JSON Data.
Constructing a Document
An individual document often represents a single instance of an object in application code. A document might be considered equivalent to a row in a relational table; with each of the document’s attributes being equivalent to a column.
Documents can contain nested structures. This allows developers to express many-to-many relationships without requiring a reference or junction table; and is naturally expressive of hierarchical data.
Most apps will work with one or more documents, persisting them to a local database and optionally syncing them, either centrally or to the cloud.
In this section we provide an example of how you might create a hotel
document, which provides basic contact details and price data.
hotel: {
type: string (value = `hotel`)
name: string
address: dictionary {
street: string
city: string
state: string
country: string
code: string
}
phones: array
rate: float
}
Open a Database
First we open your database. If the database does not already exist, Couchbase Lite will create it for us.
// Open or create DB if it doesn't exist
CBLError err;
CBLDatabase* database = CBLDatabase_Open(FLSTR("mydb"), NULL, &err);
if(!database) {
// Error handling. For brevity, this is truncated in the rest of the snippet
// and omitted in other doc code snippets
fprintf(stderr, "Error opening database (%d / %d)\n", err.domain, err.code);
FLSliceResult msg = CBLError_Message(&err);
fprintf(stderr, "%.*s\n", (int)msg.size, (const char *)msg.buf);
FLSliceResult_Release(msg);
return;
}
See: Databases for more information
Create a Document
Now we create a new document to hold our application’s data.
Because we will be adding data to the document we must use its mutable form.
// Create your new document
// The lack of 'const' indicates this document is mutable
CBLDocument* mutableDoc = CBLDocument_Create();
FLMutableDict properties = CBLDocument_MutableProperties(mutableDoc);
For more on using Documents, see: Document Initializers and Mutability.
Create a Dictionary
Here we create a dictionary (address
).
Because we want to add values into the dictionary, we must create it in mutable form.
When the dictionary is retrieved, each element’s value is directly accessible via its own key.
// Create and populate mutable dictionary
FLMutableDict address = FLMutableDict_New();
FLMutableDict_SetString(address, FLSTR("street"), FLSTR("1 Main st."));
FLMutableDict_SetString(address, FLSTR("city"), FLSTR("San Francisco"));
FLMutableDict_SetString(address, FLSTR("state"), FLSTR("CA"));
FLMutableDict_SetString(address, FLSTR("country"), FLSTR("USA"));
FLMutableDict_SetString(address, FLSTR("code"), FLSTR("90210"));
For more on using Dictionaries, see: Using Dictionaries
Create an Array
Since our hotel may have multiple lines we provide an array (phones
) to hold contact numbers.
Again, because we want to add values into the array, we create it in mutable form.
// Create and populate mutable array
FLMutableArray phones = FLMutableArray_New();
FLMutableArray_AppendString(phones, FLSTR("650-000-0000"));
FLMutableArray_AppendString(phones, FLSTR("650-000-0001"));
For more on using Arrays, see: Using Arrays
Populate a Document
Here we add our data to the mutable document we created earlier. Each data item is stored as a key-value pair.
// Initialize and populate the document
// Add document type to document properties (1)
FLMutableDict_SetString(properties, FLSTR("type"), FLSTR("hotel"));
// Add hotel name string to document properties (2)
FLMutableDict_SetString(properties, FLSTR("hotel"), FLSTR(""));
// Add float to document properties (3)
FLMutableDict_SetFloat(properties, FLSTR("room_rate"), 121.75f);
// Add dictionary to document's properties (4)
FLMutableDict_SetDict(properties, FLSTR("address"), dict);
// Add array to document's properties (5)
FLMutableDict_SetArray(properties, FLSTR("phones"), phones);
1 | Add hotel name (string) |
2 | Add average room rate (float) |
3 | Add document type (string) Couchbase recommend using a type attribute to define each logical document type. |
4 | Add address (dictionary)
The address dictionary is added to the document and stored with the key address . We will use this to retrieve it when needed. |
5 | Add phone numbers (array)
The phones arrary is added to the document and stored with the key phones . We will use this to retrieve it when needed. |
Save a Document
With the document now populated, we can persist to our Couchbase Lite database, auto-generating the document id.
// Save the document changes (1)
CBLError err;
CBLDatabase_SaveDocument(database, doc, &err);
Working with Data
Using Dictionaries
// NOTE: No error handling, for brevity (see getting started)
CBLError err;
const CBLDocument *doc = CBLDatabase_GetDocument(db, FLSTR("doc1"), &err);
FLDict properties = CBLDocument_Properties(doc);
// Getting a dictionary from the document's properties
FLValue dictValue = FLDict_Get(properties, FLSTR("address"));
FLDict dict = FLValue_AsDict(dictValue);
// Access a value with a key from the dictionary
FLValue streetVal = FLDict_Get(dict, FLSTR("street"));
FLString street = FLValue_AsString(streetVal);
// Iterate dictionary
FLDictIterator iter;
FLDictIterator_Begin(dict, &iter);
FLValue value;
while (NULL != (value = FLDictIterator_GetValue(&iter))) {
FLString key = FLDictIterator_GetKeyString(&iter);
FLString strValue = FLValue_AsString(value);
printf("Key :: %.*s\n", (int)key.size, (const char *)key.buf);
printf("Value :: %.*s\n", (int)strValue.size, (const char *)strValue.buf);
// ...
FLDictIterator_Next(&iter);
}
// Create a mutable copy.
// kFLDefaultCopy is shallow which means the nested dictionaries and arrays will be
// referenced but not copied. Use kFLDeepCopyImmutables for the deep copy.
FLMutableDict mutableDict = FLDict_MutableCopy(dict, kFLDefaultCopy);
// Release when finish using it
FLMutableDict_Release(mutableDict);
// NOTE: No error handling, for brevity (see getting started)
// Create a new mutable dictionary and populate some keys/values
FLMutableDict dict = FLMutableDict_New();
FLMutableDict_SetString(dict, FLSTR("street"), FLSTR("1 Main st."));
FLMutableDict_SetString(dict, FLSTR("city"), FLSTR("San Francisco"));
// Set the dictionary to document's properties and save the document
CBLDocument *doc = CBLDocument_Create();
FLMutableDict properties = CBLDocument_MutableProperties(doc);
FLMutableDict_SetDict(properties, FLSTR("address"), dict);
CBLError err;
CBLDatabase_SaveDocument(db, doc, &err);
CBLDocument_Release(doc);
// Release when finish using it
FLMutableDict_Release(dict);
Using Arrays
// NOTE: No error handling, for brevity (see getting started)
CBLError err;
const CBLDocument *doc = CBLDatabase_GetDocument(db, FLSTR("doc1"), &err);
FLDict properties = CBLDocument_Properties(doc);
// Getting a phones array from the document's properties
FLValue arrayValue = FLDict_Get(properties, FLSTR("phones"));
FLArray array = FLValue_AsArray(arrayValue);
// Get element count
int count = FLArray_Count(array);
printf("Count :: %d\n", count);
// Access an array element by index
if (!FLArray_IsEmpty(array)) {
FLValue phoneVal = FLArray_Get(array, 0);
FLString phone = FLValue_AsString(phoneVal);
printf("Value :: %.*s\n", (int)phone.size, (const char *)phone.buf);
}
// Iterate array
FLArrayIterator iter;
FLArrayIterator_Begin(array, &iter);
FLValue val;
while (NULL != (val = FLArrayIterator_GetValue(&iter)))
{
FLString str = FLValue_AsString(val);
printf("Value :: %.*s\n", (int)str.size, (const char *)str.buf);
FLArrayIterator_Next(&iter);
}
// NOTE: No error handling, for brevity (see getting started)
// Create a new mutable array and populate data into the array
FLMutableArray phones = FLMutableArray_New();
FLMutableArray_AppendString(phones, FLSTR("650-000-0000"));
FLMutableArray_AppendString(phones, FLSTR("650-000-0001"));
// Set the array to document's properties and save the document
CBLDocument *doc = CBLDocument_Create();
FLMutableDict properties = CBLDocument_MutableProperties(doc);
FLMutableDict_SetArray(properties, FLSTR("phones"), phones);
CBLError err;
CBLDatabase_SaveDocument(db, doc, &err);
CBLDocument_Release(doc);
// Release the created dictionary
FLMutableArray_Release(phones);
Using Blobs
For more on working with blobs — see Blobs
Document Initializers
The following methods/initializers can be used:
-
The CBLDocument_Create() initializer can be used to create a new document where the document ID is randomly generated by the database.
-
The CBLDocument_CreateWithID() initializer can be used to create a new document with a specific ID.
-
The CBLDatabase_GetDocument() method can be used to get a document. If it doesn’t exist in the database, it will return
null
. This method can be used to check if a document with a given ID already exists in the database.
The following code example creates a document and persists it to the database.
// NOTE: No error handling, for brevity (see getting started)
CBLDocument* newTask = CBLDocument_CreateWithID(FLSTR("xyz"));
FLMutableDict properties = CBLDocument_MutableProperties(newTask);
FLMutableDict_SetString(properties, FLSTR("type"), FLSTR("task"));
FLMutableDict_SetString(properties, FLSTR("owner"), FLSTR("todo"));
// Storing time in millisecond, bluntly
FLMutableDict_SetUInt(properties, FLSTR("createdAt"), time(NULL) * 1000);
CBLError err;
CBLDatabase_SaveDocument(db, newTask, &err);
CBLDocument_Release(newTask);
Mutability
Couchbase Lite for C’s document objects CBLDocument*
can be either mutable or immutable.
Reference an immutable document using a const
pointer and a mutable document using a non-const pointer to prevent developers from accidentally calling a mutable-document function on an immutable document.
To make an immutable document mutable, you need to use CBLDocument_MutableCopy()
CBLDocument* CBLDocument_MutableCopy(const CBLDocument* doc _cbl_nonnull)
_cbl_warn_unused _cbl_returns_nonnull;
Changes to the document are persisted to the database when the save
method is called.
// NOTE: No error handling, for brevity (see getting started)
CBLError err;
CBLDocument* mutableDocument = CBLDatabase_GetMutableDocument(db, FLSTR("xyz"), &err);
FLMutableDict properties = CBLDocument_MutableProperties(mutableDocument);
FLMutableDict_SetString(properties, FLSTR("name"), FLSTR("apples"));
CBLDatabase_SaveDocument(db, mutableDocument, &err);
CBLDocument_Release(mutableDocument);
Any user change to the value of reserved keys (_id , _rev or _deleted ) will be detected when a document is saved and will result in an exception (Error Code 5 — CorruptRevisionData ) — see also Document Constraints.
|
Batch operations
If you’re making multiple changes to a database at once, it’s faster to group them together. The following example persists a few documents in batch.
// NOTE: No error handling, for brevity (see getting started)
CBLError err;
CBLDatabase_BeginTransaction(db, &err);
char buffer[7];
for(int i = 0; i < 10; i++) {
CBLDocument* doc = CBLDocument_Create();
FLMutableDict properties = CBLDocument_MutableProperties(doc);
FLMutableDict_SetString(properties, FLSTR("type"), FLSTR("user"));
sprintf(buffer, "user %d", i);
FLMutableDict_SetString(properties, FLSTR("name"), FLStr(buffer));
FLMutableDict_SetBool(properties, FLSTR("admin"), false);
CBLDatabase_SaveDocument(db, doc, &err);
CBLDocument_Release(doc);
printf("Saved user document %s\n", buffer);
}
CBLDatabase_EndTransaction(db, true, &err);
At the local level this operation is still transactional: no other Database
instances, including ones managed by the replicator can make changes during the execution of the block, and other instances will not see partial changes.
But Couchbase Mobile is a distributed system, and due to the way replication works, there’s no guarantee that Sync Gateway or other devices will receive your changes all at once.
Document change events
It is possible to register for document changes.
The following example registers for changes to the document with ID user.john
and prints the verified_account
property when a change is detected.
/*
static void document_listener(void* context, const CBLDatabase* db, FLString id) {
CBLError err;
const CBLDocument* doc = CBLDatabase_GetDocument(db, id, &err);
FLDict properties = CBLDocument_Properties(doc);
FLString verified_account = FLValue_AsString(FLDict_Get(properties, FLSTR("verified_account")));
printf("Status :: %.s\n", (int)verified_account.size, (const char *)verified_account.buf);
CBLDocument_Release(doc);
}
*/
CBLListenerToken token = CBLDatabase_AddDocumentChangeListener(db, FLSTR("user.john"),
document_listener, NULL);
Document Expiration
Document expiration allows users to set the expiration date for a document. When the document expires, it is purged from the database. The purge is not replicated to Sync Gateway.
This example sets the TTL for a document to 5 minutes from the current time.
// Purge the document one day from now
// Overly simplistic for example purposes
// NOTE: API takes milliseconds
// NOTE: No error handling, for brevity (see getting started)
time_t ttl = time(NULL) + 24 * 60 * 60;
ttl = 1000;
CBLError err;
CBLDatabase_SetDocumentExpiration(db, FLSTR("doc123"), ttl, &err);
// Reset expiration
CBLDatabase_SetDocumentExpiration(db, FLSTR("doc1"), 0, &err);
// Query documents that will be expired in less than five minutes
time_t fiveMinutesFromNow = time(NULL) + 5 * 60;
fiveMinutesFromNow *= 1000;
FLMutableDict parameters = FLMutableDict_New();
FLMutableDict_SetInt(parameters, FLSTR("five_minutes"), fiveMinutesFromNow);
CBLQuery query = CBLDatabase_CreateQuery(db, kCBLN1QLLanguage,
FLSTR("SELECT meta().id FROM _ WHERE meta().expiration < $five_minutes"), NULL, &err);
CBLQuery_SetParameters(query, parameters);
FLMutableDict_Release(parameters);
Document Constraints
Couchbase Lite APIs do not explicitly disallow the use of attributes with the underscore prefix at the top level of document. This is to facilitate the creation of documents for use either in local only mode where documents are not synced, or when used exclusively in peer-to-peer sync.
"_id", :"_rev" and "_sequence" are reserved keywords and must not be used as top-level attributes — see Example 10. |
Users are cautioned that any attempt to sync such documents to Sync Gateway will result in an error. To be future proof, you are advised to avoid creating such documents. Use of these attributes for user-level data may result in undefined system behavior.
For more guidance — see: Sync Gateway - data modeling guidelines
Working with JSON Data
- In this section
The toJSON()
typed-accessor means you can easily work with JSON data, native and Couchbase Lite objects.
Arrays
Convert an ArrayObject
to and from JSON using the toJSON()
and toArray
methods — see: Example 3.
Additionally you can:
-
Initialize a 'MutableArrayObject' using data supplied as a JSON string. This is done using the
init(json)
constructor-- see: Example 3 -
Convert an
ArrayFragment
object to a JSON String -
Set data with a JSON string using
setJSON()
// NOTE: No error handling, for brevity (see getting started)
FLString json = FLSTR("[\"Hotel Ned\", \"Hotel Ted\"]");
// Create an array from the JSON string
FLError err;
FLSliceResult jsonData1 = FLData_ConvertJSON(json, &err);
FLArray hotels = FLValue_AsArray(FLValue_FromData(FLSliceResult_AsSlice(jsonData1), kFLTrusted));
// Iterate through the array
FLArrayIterator iter;
FLArrayIterator_Begin(hotels, &iter);
FLValue value;
while (NULL != (value = FLArrayIterator_GetValue(&iter))) {
FLString hotel = FLValue_AsString(value);
printf("Hotel :: %.*s\n", (int)hotel.size, (const char *)hotel.buf);
FLArrayIterator_Next(&iter);
}
// Convert the array to JSON
FLSliceResult jsonData2 = FLValue_ToJSON((FLValue)hotels);
printf("Hotels in JSON :: %.*s\n", (int)jsonData2.size, (const char *)jsonData2.buf);
// Release JSON data after finish using it
FLSliceResult_Release(jsonData1);
FLSliceResult_Release(jsonData2);
1 | Initialize array with JSON string |
2 | Create and save new document using the array |
3 | Get native array object from new doc and print its elements |
4 | Get an array from the document as a JSON string |
Dictionaries
Convert a DictionaryObject
to and from JSON using the toJSON
and toDictionary
methods — see Example 12.
Additionally you can:
-
Initialize a 'MutableDictionaryObject' using data supplied as a JSON string. This is done using the
init(json)
constructor-- see: Example 12 -
Set data with a JSON string using
setJSON()
// NOTE: No error handling, for brevity (see getting started)
FLString json = FLSTR("{\"id\":\"1002\",\"type\":\"hotel\",\"name\":\"Hotel Ned\",\"city\":\"Balmain\",\"country\":\"Australia\"}");
// Create a dictionary from the JSON string
FLError err;
FLSliceResult jsonData1 = FLData_ConvertJSON(json, &err);
FLDict hotel = FLValue_AsDict(FLValue_FromData(FLSliceResult_AsSlice(jsonData1), kFLTrusted));
// Iterate through the dictionary
FLDictIterator iter;
FLDictIterator_Begin(hotel, &iter);
FLValue value;
while (NULL != (value = FLDictIterator_GetValue(&iter))) {
FLString key = FLDictIterator_GetKeyString(&iter);
FLString strValue = FLValue_AsString(value);
printf("%.s :: %.*s\n", (int)key.size, (const char)key.buf, (int)strValue.size, (const char*)strValue.buf);
FLDictIterator_Next(&iter);
}
// Convert the dictionary to JSON
FLSliceResult jsonData2 = FLValue_ToJSON((FLValue)hotel);
printf("Hotel in JSON :: %.*s\n", (int)jsonData2.size, (const char *)jsonData2.buf);
// Release JSON data after finish using it
FLSliceResult_Release(jsonData1);
FLSliceResult_Release(jsonData2);
1 | Set the dictionary using a JSON string |
Documents
Convert a Document
to and from JSON strings using the toJSON()
and setJSON()
methods — see Example 13.
Additionally you can:
-
Initialize a 'MutableDocument' using data supplied as a JSON string. This is done using the
init(json)
and-orinit(id: json:)
constructor-- see: Example 13 -
Set data with a JSON string using
setJSON()
// NOTE: No error handling, for brevity (see getting started)
FLString json = FLSTR("{\"id\":\"1002\",\"type\":\"hotel\",\"name\":\"Hotel Ned\",\"city\":\"Balmain\",\"country\":\"Australia\"}");
// Create a document and set the JSON data to the document
CBLError err;
CBLDocument* newDoc = CBLDocument_CreateWithID(FLSTR("hotel_1002"));
CBLDocument_SetJSON(newDoc, json, &err);
// Save the document to the database
CBLDatabase_SaveDocument(db, newDoc, &err);
// Release created doc after using it
CBLDocument_Release(newDoc);
// Get the document from the database
const CBLDocument* doc = CBLDatabase_GetDocument(db, FLSTR("hotel_1002"), &err);
// Get document body as JSON
FLSliceResult docJson = CBLDocument_CreateJSON(doc);
printf("Document in JSON :: %.*s\n", (int)docJson.size, (const char *)docJson.buf);
// Release JSON data after using it
FLSliceResult_Release(docJson);
// Release doc read from the database after using it
CBLDocument_Release(doc);
1 | Get a document as a JSON string |
2 | Initialize a MutableDocument using the JSON string and save to a separate database |
3 | Retrieve the document created from JSON and print values |
Query Results as JSON
Convert a Query Result
to JSON using its toJSON()
accessor method.
Use FLValue_ToJSON() to transform your result string into a JSON string, which can easily be serialized or used as required in your application. See <
// NOTE: No error handling, for brevity (see getting started)
CBLResultSet* results = CBLQuery_Execute(query, &err);
while(CBLResultSet_Next(results)) {
FLDict result = CBLResultSet_ResultDict(results);
FLStringResult json = FLValue_ToJSON((FLValue)result);
printf("JSON Result :: %.*s\n", (int)json.size, (const char *)json.buf);
FLSliceResult_Release(json);
}
CBLResultSet_Release(results);
1 | Get the Query result as a JSON string — see JSON String Format |
2 | Get a native object from the JSON string |
3 | Populate your custom object from the dictionary created from JSON data |
If your query selects ALL then the JSON format will be:
{
database-name: {
key1: "value1",
keyx: "valuex"
}
}
If your query selects a sub-set of available properties then the JSON format will be:
{
key1: "value1",
keyx: "valuex"
}