Note: This is a tutorial about working with IDBWrapper, a wrapper for the IndexedDB client side storage. Creating indexes and running queries only works with the new version of IDBWrapper, so if already have IDBWrapper, make sure you fetch a new version from GitHub.
All examples in this tutorial follow along the “Basic Index Example”, which uses IDBWrapper to store customer data such as name and age. You can find in the examples folder, and you can (and should) also open it in your browser, as it has a “query” section that allows you to set query options by using inputs and dropdowns to immediately see the results of your settings.
While the last part of the tutorial covered the basic CRUD methods get/getAll/put/delete, this part is about the real thing: running queries against the store.
To do so, we need to get familiar with two things: Creating indexes, and creating keyRanges.
Creating Indexes
Every store has an implicit index, which is the keyPath of the store (think of it as the primary index). If you want to run queries against other keys of your stored objects, you need to add an index for that property. A store can have unlimited indexes.
To create an index, you need to add an object containing information about the index to the indexes array of the options object in the IDBStore constructor. For example:
The index options object
The name
property is the identifier of the index. If you want to work with the created index later, this name is used to identify the index. This is the only property that is mandatory.
The keyPath
property is the name of the property in your stored data that you want to index. If you omit that, IDBWrapper will assume that it is the same as the provided name, and will use this instead.
The unique
property tells the store whether the indexed property in your data is unique. If you set this to true, it will add a uniqueness constraint to the store which will make it throw if you try to store data that violates that constraint. If you omit that, IDBWrapper will set this to false.
The multiEntry
property is kinda weird. You can read up on it here. However, you can live perfectly fine with setting this to false (or just omitting it, this is set to false by default).
So, the above index options object could have also been written like this:
Handy, isn’t it?
Querying the store
As we now have indexes, we can start iterating over datasets. To do so, IDBWrapper provides an iterate()
method. It takes to arguments: the first is the onItem callback, which is called for each entry in the store that matched the query. The second argument is an options object where you can add more specific instructions for your query. This second argument is optional; if you omit it, IDBWrapper will iterate over all entries in the store and return results ordered by the store’s keyPath, in ascending order.
The onItem callback
The provided callback will be called once for every match. It will receive three arguments: the object that matched the query, a reference to the current cursor object (IDBWrapper uses IndexedDB’s Cursor internally to iterate), and a reference to the current ongoing transaction.
There’s one special situation: if you didn’t pass an onEnd handler in the options objects (see below), the onItem handler will be called one extra time when the transaction is over. In this case, it will receive null as only argument. So, to check when the iteration is over and you won’t get any more data objects, you can either pass an onEnd handler, or check for null
in the onItem handler.
The iterate options object
The index
property contains the name of the index to operate on. If you omit this, IDBWrapper will use the store’s keyPath as index.
In the keyRange
property you can pass a keyRange; forget about that for now, we’ll come to keyRanges in a minute.
The order
property can be set to ‘ASC’ or ‘DESC’, and, you might have guessed so, determines the ordering direction of results. If you omit this, IDBWrapper will use ‘ASC’.
The filterDuplicates
property is an interesting one: If you set this to true (it defaults to false), and have several objects that have the same value in their key, the store will only fetch the first of those. It is not about objects being the same, it’s about their key being the same. For example, in the customers database are a couple of guys having ‘Smith’ as last name. Setting filterDuplicates to true in the above example will make iterate()
call the onItem callback only for the first of those.
The writeAccess
property defaults to false. If you need write access to the store during the iteration, you need to set this to true. But, for now, just forget about this, we’ll come to this later.
In the onEnd
property you can pass a callback that gets called after the iteration is over and the transaction is closed. It does not receive any arguments.
In the onError
property you can pass a custom error handler. In case of an error, it will be called and receives the Error object as only argument.
Let’s start running queries!
There’s a couple of things we can already do at this point. We can, for example:
– get all customers, ordered by their customer id, in a descending manner
– get a list of all different last names our customers have
While this is better than just get()
or getAll()
, we definitely want to be more specific in our queries. Time to meet keyRanges.
KeyRanges
IndexedDB allows you to specify a range of keys to use as condition to check whether a key matches or not. For example, a condition can be (key <= 1000) which would return everything that has a key with a value of less that 1000, including 1000 as well. This range has an “upper bound”. A range can also have a lower bound (key > 25), or a lower and an upper bound (key > 25 && key <= 1000). IDBWrapper provides a makeKeyRange()
method to create a keyRange. Let’s see an example:
This range will match against all keys starting with ‘A’ to ‘L’. The excludeUpper
property is set to true, which means that a key that exactly matches the upper bound will be excluded from the result. The exclude[Lower/Upper]
properties are set to false by default.
We can now use the newly created keyRange with the iterate()
method and pass it in the option object’s keyRange
property:
This will fetch all customers from the store, that have their last name beginning with ‘A’ to ‘L’. As the lastname keys in our example all start with uppercase letters, we can also define the keyRange like this:
With keyRanges, we can do more interesting queries:
– Get all customers who’s last name is ‘Doe’
– Get a list of different last names starting with ‘G’ or higher.
That’s all IndexedDB offers out of the box regarding queries. But, if you are creative, you can do some fancy queries with it. And, in many cases it saves you from fetching all data from the store and manually reducing the result set.
Using KeyRanges elsewhere
KeyRanges can be used instead of a key with many functions that accept a key as argument. While there are useful appliances of this, e.g. customers.delete(customers.makeKeyRange({lower: 100});
, which would delete all customers with a customerid greater than 100, the spec is not very clear about whether this is to be implemented by browsers or not; so I’d rather not rely on this.
Iteration and Transactions
During the iteration, there is an open transaction running. IDBWrapper tries to keep you away from all this transaction stuff so that you don’t have to care about it, but you still need to be aware of this. There are two transaction modes: read-only and read-write. By default, IDBWrapper uses a read-only transaction for iteration. Thing is, in an onItem callback, you are inside a transaction; that means, that if you want to read or write to the store using put/delete/etc (which will try to open yet another transaction) inside of the onItem handler, funny things will happen. This scenario is kind of underspecified and browsers may do different things then.
If you need to modify data during the onItem handler (say, you query the store for all customers older than 50 years and want to add a ‘senior’ property to them), this is how you need to do it:
First, you need to set the writeAccess property to true in the options obect. Then, in your onItem handler, you need to use the cursor object to change data. It offers two methods, update()
and delete()
. Both will return an IDBRequest object. An example:
Examples
Here’s the examples from the tutorial above. All the examples follow along the “Basic Index Example”, that you can find in the examples folder. You can (and should) also open it in your browser, as it has a “query” section that allows you to set query options by using inputs and dropdowns. Start with clicking on “Add random customer” a couple of times to populate the store.
For every example, along with some code, there’s also an explanation on how to set the query form to the example’s settings. That way, you can immediately see the result of the query.
Get all customers, ordered by their customer id, in a descending manner
On the example page, set index to ‘none’, sort order to ‘descending’ and leave everything else empty.
Get a list of all different last names our customers have
On the example page, set index to ‘lastname’ and check the ‘Filter Duplicates’ checkbox.
Get all customers who’s last name is ‘Doe’
On the example page, set index to ‘lastname’, type ‘Doe’ into the ‘Upper Bound’ and ‘Lower Bound’ fields.
Get a list of different last names starting with ‘G’ or higher.
On the example page, set index to ‘lastname’, type ‘G’ into the ‘Lower Bound’ field and check the ‘Filter Duplicates’ checkbox.
Get all customers that have their last name beginning with ‘A’ to ‘L’.
Come on, now you should be able to do this yourself!