Skip to content

Query Builder

snej edited this page Dec 11, 2014 · 16 revisions

The CBLQueryBuilder class provides a high-level interface for defining database queries, somewhat like Core Data's NSFetchRequest. Instead of thinking about map functions and key ranges, you provide "select", "where" and "order by" expressions; the query builder uses these to define a view and configure the query.

CBLQueryBuilder does not make CBLView obsolete! It doesn't support everything you can do with map/reduce, so there are cases where you'll run into its limits and need to define your own view and queries. But it does take care of most common types of queries.

Compatibility: CBLQueryBuilder is a new class in Couchbase Lite 1.1. (An earlier version was formerly available in version 1.0.3 as an unsupported 'extra', but not built into the library.) It is iOS/Mac specific, since it's based on Cocoa classes like NSPredicate and NSSortDescriptor. Comparable functionality will be available on other platforms too, with APIs that are idiomatic for those platforms.

Overview

A CBLQueryBuilder defines a query or a family of related queries (since it can contain variables whose values can be substituted later.) It has four attributes:

  1. The database to query.
  2. A "where" predicate (boolean-valued function) that chooses which documents match the query.
  3. An array of values to select from the matching documents. Each value is an expression based on properties of the document.
  4. An optional array of sort descriptors that determine the order the results appear.

(This may look something like a SQL SELECT statement, but the details are different.)

Based on these attributes, the query builder will create a view and register its map function. You don't need to worry about this. The query builder will create a pre-configured CBLQuery object for you, and all you need to do is run it and use the result.

Often a query's predicate will depend on specific values that aren't known until runtime, such as a minimum or maximum property value. You can specify these as as placeholder variables; in a predicate string they're $-prefixed identifiers. When creating a query, you provide specific values for these variables.

The best practice is to create query builders ahead of time, keeping references to them in global variables or long-lived instance variables. This reduces the overhead of parsing the attributes and creating the views. If you're creating a query builder each time you run a query, you're probably doing it wrong.

Example

Data model

Let's assume a simple blog database that we want to search for posts tagged with a given keyword. The posts will look like this:

{
    "type": "post",
    "title": "Introducing CBLQueryBuilder",
    "author": "Jens Alfke",
    "date": "2014-12-11",
    "body": "Yo, 'sup. Check out this new class...",
    "tags": ["couchbase lite", "iOS", "queries", "views", "API"]
}

Creating the query builder

When the application's search controller object is initialized, it creates a query builder:

    self.tagQueryBuilder = [[CBLQueryBuilder alloc]
            initWithDatabase: self.database
                      select: @[@"title", @"body", @"author", @"date"]
                       where: @"type == 'post' and tags contains $TAG"
                     orderBy: @[@"-date"]
                       error: &error];

We are limiting the search to documents whose type property is "post", since there are likely to be other types of documents (comments, authors...) Also, we don't know the specific tag to search for, so we leave it as a variable $TAG.

The language used in the where: parameter is Apple's predicate syntax, not SQL or N1QL or any other query language.

The orderBy: array is using the string @"-date" as a shortcut meaning "in descending order of date property". This could also have been specified as an NSSortDescriptor object.

Querying

Then when a tag is given, the controller can perform a query:

- (void) queryForTag: (NSString*)tag {
    NSError* error;
    CBLQueryEnumerator* e = [self.tagQueryBuilder runQueryWithContext: @{@"TAG": tag}
                                                                error: &error];
    if (e) {
        [self displayRows: e.allObjects];
    } else {
        [self handleError: error];
    }
}

To create a query, we have to give a context dictionary that provides a value for each variable, in this case TAG.

Important: The $ prefix used in the predicate is not part of the variable name itself, and isn't used in the dictionary key. If you put it in by accident, you'll get a runtime exception complaining that the variable TAG doesn't have a value.