-
Notifications
You must be signed in to change notification settings - Fork 298
Guide: Views
The basic document API will get you pretty far, but real apps need to work with multiple documents. In a typical app, the top-level UI is probably going to show either all the documents or a relevant subset of them -- in other words, the results of a query.
As described above in the Data Model section, querying a Couchbase Lite database involves first creating a view which indexes the keys you're interested in, and then running a query to get the results of the view for the key or range of keys you're interested in. The view is persistent, like a SQL index.
Since there's no fixed schema for the view engine to refer to, and since the interesting bits of a document that we want it to index could be located anywhere in the document (including nested values inside of arrays and sub-objects), the view engine has to let us pick through each document to identify the relevant key (or keys) and values. That's what the view's map function is for: it's an app-defined function that's given a document's contents and returns (or 'emits') zero or more key/value pairs. It's those pairs that get indexed, ordered by key, and can then be queried efficiently, again by key.
For example, if you have an address book in a database, you probably want to query the cards by first or last name, for display or filtering purposes. To do that, you'd create two views: one would grab the first-name field and return it as the key, the other would return the last-name field as the key. (And what if you were originally just storing the full name as one string? Then your functions can detect that, split the full name at the space, and return the first or last name. That's how schema evolution works.)
You might also want to be able to look up people's names from phone numbers, so you can do Caller ID on incoming calls. For this you'd make a view whose keys are phone numbers. Now, a document might have multiple phone numbers in it, like so:
{ "first": "Bob",
"last": "Dobbs"
"phone": {
"home": "408-555-1212",
"cell": "408-555-3774",
"work": "650-555-8333"} }
No problem: the map function just needs to loop over the phone numbers and emit each one. You then have a view index that contains each phone number, even if several of them map to the same document.
To start off with, for simplicity we'll look at a very useful predefined view called _all_docs
. This is equivalent to a view with a map function that simply emits the _id
field as the key: in other words, its index contains all docs in the database, sorted by ID.
To query an existing view, we get a CBLQuery
object for it. The built-in _all_docs
view is accessed directly from the CBLDatabase:
CBLQuery* query = database.getAllDocuments;
This creates a new query object. Before running the query, we can customize it -- this is much like the SQL SELECT
statement's ORDER BY
, OFFSET
and LIMIT
clauses. Let's say we want the ten documents with the highest keys:
query.limit = 10;
query.descending = YES;
(As a side effect we'll get the documents in reverse order, but that's easy to compensate for if it's not appropriate.) Now we can iterate over the results:
for (CBLQueryRow* row in query.rows) {
NSLog(@"Doc ID = %@", row.key);
}
query.rows
evaluates the query and returns an NSEnumerator that can be used with a for...in
loop to iterate over the results. Each result is a CBLQueryRow
object -- you might expect it to be a CBLDocument, but the key/value pairs emitted in views don't necessarily correspond one-to-one to documents, so a document might be present multiple times under different keys. If you want the document that emitted a row, you can get it from its document
property.
Now let's turn to views in general. The first part is creating the view by defining its map (and optionally reduce) function. Here's how the Grocery Sync example app sets up its by-date view:
CBLView* view = [db viewNamed: @"byDate"];
[view setMapBlock: MAPBLOCK({
id date = [doc objectForKey: @"created_at"];
if (date) emit(date, doc);
}) version: @"1.0"];
The name of the view is arbitrary, but you'll need to use it later on when querying the view. The interesting part here is that MAPBLOCK
expression, which is a block defining the map function. Note that if you get an error about "too many arguments provided to function-like macro invocation" this doesn't mean you are evil, it just means the preprocess is confused. Try putting parenthesis around the expression with commas in it. MAPBLOCK
is just a preprocessor macro to simply the declaration of the block; here's what the block looks like without it:
^(NSDictionary* doc, void (^emit)(id key, id value)) {
id date = [doc objectForKey: @"created_at"];
if (date) emit(date, doc);
}
This is a block that takes two parameters:
- An NSDictionary -- this is the contents of the document being indexed.
- A function (well, a block) called
emit
that takes parameterskey
andvalue
. This is the function your code calls to emit a key/value pair into the view's index.
Once you get that, the example map block is pretty straightforward: it looks for a created_at
property in the document, and if it's present it emits it as the key, with the entire document contents as the value. (Emitting the document as the value is fairly common; it makes it slightly faster to read the document at query time, at the expense of some disk space.)
The view index will then consist of the dates of all documents, sorted in order. This is useful for displaying the documents ordered by date (which Grocery Sync does), or for finding all documents created within a certain range of dates.
Note that any document without a created_at
field is ignored and won't appear in the view index. This means you can put other types of documents in the same database (maybe names and addresses of of grocery stores?) without them messing up the display of the shopping list.
IMPORTANT: The view index itself is persistent, but the defineViewNamed:version:
method has to be called every time the app starts, before the view is accessed. This is because the map function isn't persistent, it's an ephemeral block pointer, and it needs to be hooked up to Couchbase Lite at runtime.
Now that we've created the view, querying it is very much like querying _all_docs
, except that we get the CBLQuery
object from the view not the database:
CBLQuery* query = [[db viewNamed: @"byDate"] query];
Every call to query
creates a new CBLQuery object, ready for you to customize -- there are a number of properties you can set to specify key ranges, ordering, etc, as described later in the "View and Query Design" section. Then you run the query (i.e. start enumerating its rows
property) exactly as described above under "Getting All Documents".
It can be useful to know whether the results of a query have changed. You might have generated some complex output, like a fancy graph, from the query rows, and would prefer to save the work of recomputing the graph if nothing's changed. You can accomplish this by keeping the CBLQuery object around, and then later checking its rowsIfChanged
property. This property returns nil
if the results are the same as last time, or a new row enumerator if they're different:
CBLQueryEnumerator* newRows = query.rowsIfChanged;
if (newRows != nil) {
for (CBLQueryRow* row in newRows) {
// ... do something complex with the rows
}
}
Even better than checking for a query update is getting notified when one happens. Users expect apps to be "live" and don't want to have to press a Refresh button to see new data. This is especially true if data might arrive over the network at any time through synchronization: that new data needs to show up right away.
For this reason Couchbase Lite has a very useful subclass of CBLQuery called CBLLiveQuery
. It acts the same, except that the value of its rows
property updates automatically as the database changes, and better yet, the rows
property is observable using Cocoa's Key-Value Observing (KVO) mechanism. That means you can register for immediate notifications when it changes, and use those to drive user-interface updates.
To create a CBLLiveQuery you just ask a regular query object for a live copy of itself. You can then register as an observer:
self.liveQuery = query.asLiveQuery;
[self.liveQuery addObserver: self forKeyPath: @"rows" options: 0 context: NULL];
(Don't forget to remove the observer when cleaning up!) The observation method might look like:
- (void) observeValueForKeyPath: (NSString*)keyPath ofObject: (id)object
change: (NSDictionary*)change context: (void*)context
{
if (object == self.liveQuery) {
for (CBLQueryRow* row in [object rows]) {
// update the UI
}
}
}
And what's even better than a live query? A live query that automatically acts as the data source of a UITableView
. That's what CBLUITableSource
provides: it's an implementation of UITableViewDataSource
that observes a CBLLiveQuery
and syncs the table with the view rows. To use it, you need to:
- Instantiate one (duh) -- one easy way is to put one in the same xib as the table view
- Set its
tableView
property to theUITableView
(this is an IBOutlet so you can wire it up) - Set its
query
property to aCBLLiveQuery
- Set its
labelProperty
property to the name of a property in the view row's value (or in the associated document): the value of this property is the text that will be displayed in the table cell's label.
If you want more control over the label, or want to use a fancier cell with more than just text, you can implement the CBLUITableDelegate
protocol and set that object as the table source's delegate. This gives you a number of optional methods you can implement that will allow you to substitute your own UITableCell, handle errors, etc. See the class documentation for details.
If you run the query without setting any key ranges, the result will be all the emitted rows, in ascending order by key (date, in this example.) To reverse the order, set the query's descending
property.
To get only the row(s) with specific keys, set the query's keys
property to an array of the desired keys:
query.keys = @[ aSpecificDate ];
The order of the keys in the array doesn't matter; the results will be returned in ascending-key order.
To get a range of keys, set the query's startKey
and endKey
properties. (The range is inclusive, that is, the result will include the rows with key equal to endKey
.)
One common source of confusion is combining key ranges with descending order. Note that you're specifying the starting and ending keys, not the minimum and maximum. That implies that, in a descending query, the startKey
should be the maximum value and the endKey
the minimum value.
The real power of views comes when you use compound keys. If your map function emits arrays as keys, they will be sorted as you'd expect: the first elements are compared, and if they're equal the second elements are compared, and so forth. This lets you sort the rows by multiple criteria (like store and item), or group together results sharing a criterion.
For example, if a map function emitted the document's store
and item
properties as a compound key:
emit(@[doc[@"store"], doc[@"item"]], nil);
then the view's index would contain a series of keys ordered like this:
...
["Safeway", "goldfish crackers"]
["Safeway", "tonic water"]
["Trader Joe's", "chocolate chip cookies"]
["Whole Foods", "cruelty-free chakra lotion"]
...
The ordering of compound keys depends entirely on how you want to query them; the broader criteria go to the left of the narrower ones. For some queries you might need a different ordering than for others; if so, you'll need to define a separate view for each ordering. For example, the above ordering is good for finding all the items to buy at a particular store. If instead you want to look up a specific item and see what store(s) to get it at, you'd want the compound keys in the opposite order. So you could define views called "stores" and "items", and query whichever one is appropriate.
The way you specify beginnings and ends of compound-key ranges can be a bit unintuitive. Let's say we have a view whose keys are of the form [store, item]
and we want to find all the items to buy at Safeway. What are the startKey
and endKey
? Clearly their first elements will be @"Safeway"
, but what comes after that? We need a way to specify the minimum and maximum possible keys with a given first element. The answer looks like this:
query.startKey = @[ @"Safeway" ];
query.endKey = @[ @"Safeway", @{} ];
The minimum key with a given first element is just a length-1 array with that element. (This is just like the way that the word "A" sorts before any other word starting with "A".)
The maximum key is ... what the hell is that empty NSDictionary doing there? Couchbase Lite defines a sorting/collation order for all JSON types, and JSON objects (aka dictionaries) sort after everything else. So an empty dictionary is kind of like a "Z" on steroids: it's a placeholder that sorts after any string, number, or array. It looks weird at first, but it's a useful idiom used in queries to represent the end of a range.
NEXT: Replication