-
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 TouchDB (or CouchDB) 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, sorted by ID.
To query an existing view in CouchCocoa, we get a CouchQuery
object for it. The built-in _all_docs
view is accessed directly from the CouchDatabase:
CouchQuery* query = database.getAllDocuments;
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 (CouchQueryRow* row in query.rows) {
NSLog(@"Doc ID = %@", row.key);
}
query.rows
returns an NSEnumerator that can be used with a for...in
loop to iterate over the results. Each result is a CouchQueryRow
object -- you might expect it to be a CouchDocument, 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. In CouchDB these functions live as source code in special documents called "design documents"; TouchDB keeps the design documents for compatibility. Here's how the Grocery Sync example app sets up its by-date view:
CouchDesignDocument* design = [database designDocumentWithName: @"grocery"];
[design defineViewNamed: @"byDate" mapBlock: MAPBLOCK({
id date = [doc objectForKey: @"created_at"];
if (date) emit(date, doc);
}) version: @"1.0"];
The names of the design document and view are arbitrary, but you'll need to use them later on when querying the view. The interesting part here is that MAPBLOCK
expression, which is a block defining the map function. 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 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 TouchDB at runtime.
Now that we've created the view, querying it is very much like querying _all_docs
, except that we get the CouchQuery
object from the design document not the database:
CouchDesignDocument* design = [database designDocumentWithName: @"grocery"];
CouchQuery* query = [design queryViewNamed: @"byDate"];
Every call to queryViewNamed:
creates a new CouchQuery object, ready for you to customize by order, limit, etc. From here on out, you run the query exactly as described above under "Getting All Documents".
For simplicity's sake, the above examples have run the query synchronously. But view queries are one thing you probably don't want to access synchronously, unless your data set is quite small. If the view index is out of date, because documents have been modified since it was last queried, the next query will need to update it. The index does get updated incrementally, but if there have been a lot of changes since the last time you queried the view, the delay can be noticeable. This might make your app's UI lock up for a noticeable time.
So what's the slow call? Turns out, it's the innocent looking query.rows
property getter. A CouchQuery is a type of object called a "future": it's an immediate reply to an asynchronous operation. Until the operation completes, the future is a placeholder, and rather than admit its fakery, if it's asked for something that it doesn't know yet, it blocks until the result is available.
To avoid blocking, you can run the query asynchronously by calling its start
method and running your code as an onCompletion
handler of the resulting operation:
RESTOperation* op = [query start];
[op onCompletion: ^{
if (op.error) {}
[self showErrorAlert: @"Couldn't fetch rows" forOperation: op];
} else {
for (CouchQueryRow* row in query.rows) {
NSLog(@"Doc ID = %@", row.key);
}
}
}];
This way, the call to query.rows
is deferred until the query has finished. At that point the results are available, so the rows
accessor can return them immediately without waiting.
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 CouchQuery 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:
CouchQueryEnumerator* newRows = query.rowsIfChanged;
if (newRows != nil) {
for (CouchQueryRow* row in newRows) {
// ... do something complex with the rows
}
}
Even better than checking for a query update is to be 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 CouchCocoa has a very useful subclass of CouchQuery called CouchLiveQuery
. 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 CouchLiveQuery 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 (CouchQueryRow* 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 CouchUITableSource provides: it's an implementation of UITableViewDataSource
that observes a CouchLiveQuery
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 aCouchLiveQuery
- 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 CouchUITableDelegate
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.
NEXT: Replication