# Indexed DB API

The IndexedDB API gives us the ability to create local document databases that are accessible with JavaScript through either a script that is attached to an HTML file or through a Service Worker.

# IndexedDB Libraries

There are a number of libraries that have been created to allow us to manage and use IndexedDB databases. Too many to cover here in fact.

However, if you only want to use your IndexedDB database like you have used LocalStorage or SessionStorage then a good choice is Jake Archibald's idb-key library (opens new window).

It gives us a small number of methods and works with Promises so we can chain methods with then() methods. It treats all the stores like key-value pairs like LocalStorage does.

Watch this video for a complete overview.

Here are some of the main methods for idb-keyval:

set('mykey', 'my value').then(() => {
  console.log('Success');
});

get('mykey').then((val) => {
  console.log('The value for mykey is', val);
});

setMany([
  ['key1', 'key2'],
  ['hello', 'world'],
]).then(() => {
  console.log('success');
});

getMany(['key1', 'key2']).then((values) => {
  //values is an array of values for those keys
});

delete 'key1'.then(() => {
  console.log('success deleting');
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# Vanilla JS and the IndexedDB API

Once we move beyond simple key value pairs then it's better to switch libraries or write our own scripts with the built-in IndexedDB API.

Here is the first of the 8 videos in the playlist about IndexedDB.

Full IndexedDB Playlist (opens new window)

# Core Concepts

When writing your own code to work with IndexedDB, it is not any harder than using a library, it just means that you have to add your own functions for handling the success, complete, and error events.

MDN IndexedDB Reference section (opens new window)

# Structure

An IndexedDB is a document database. That means that the data tends to be in the form of Objects or Arrays. You can still store simple primitives like Strings, Numbers, and Booleans. With IndexedDB you can also store Binary data like images or other files.

The databases are bound to an origin (domain, protocol, and port).

You are allowed to have multiple databases per origin.

Inside a database you can have one or more stores.

Each store is a collection of Objects.

# Opening and Updating Databases & Stores

The methods used for communicating with the databases and stores have events that can be accessed through callback functions.

To use a database you need to call the indexedDB.open(name, ver) method. It accepts a name String and a version Number as arguments. The method returns an IDBOpenDBRequest object. This object has three events - success, error and upgradeneeded.

The success event fires when the database has successfully finished opening. This is when you would be able to save a global reference to the database in a global variable.

The error event fires when there is ANY problem opening the database.

The upgradeneeded event fires when you call the indexedDB.open() method with a different version number than what exists. This includes the first time you open the database. This is the ONLY place where you are allowed to create stores, delete stores, or create indexes. Basically, any time you want to change the structure of your database, this event handler function is the place to do it.

# Transactions and Requests

Any time you want to work with the data in your stores - Create, Read, Update, or Delete - you do this through a Request. Every Request or group of Requests must to be wrapped inside a Transaction.

A transaction is create by calling the db.transaction(store(s), mode) method on your database object. It takes, as its first argument, either a store name String or an Array of store name Strings. The second argument is the purpose of the transaction. It must be either readonly or readwrite.

The transaction object returned from the db.transaction() method has two events - error and complete. The error event is pretty obvious. The complete event means that ALL the requests inside the transaction have completed successfully. This is usually a good place to put your code that moves on to the next step in your business logic.

The IDBRequest objects inside every transaction have two events - error and success. Again, the error is pretty self explanatory. The success event handler function is where you would get access to the data that was retrieved through a read operation. For create, update, and delete operations you can use the success event to track your progress through the transaction and to inform the user.

# CRUD Methods

CRUD is a common acronym used with databases. It refers to the four main actions that you can do with any database - Create, Read, Update, and Delete. In Relational Databases, you would use the SQL Structured Query Language, and call the commands insert, select, update and delete.

With IndexedDB we have the following commands.

//CREATE
store.add(key, obj); //insert an object. Fail if the key exists
//READ
store.get(key); //get the first object where the key matches
store.getAll(key | keyrange); //get all the objects that match
//UPDATE
store.put(key, obj); //update. Overwrite if key exists. Insert if not.
//DELETE
store.delete(key | keyrange); //delete an object(s)
1
2
3
4
5
6
7
8
9

Notice how each of these commands has a variable store in front?

Each time you want to run one of these commands you need to create a transaction, then get a reference to a specific store and then you can call the specific CRUD method.

Every one of those methods will return a IDBRequest object that represents the request to carry out the CRUD operation.

Here is a more complete example:

let tx = db.transaction('myStore', 'readwrite');
let store = tx.objectStore('myStore');
let req = store.get('someKey');
req.addEventListener('success', (ev) => {
  //ev.target is the same as req
  let result = ev.target.result;
  //result is the object we requested from the store.
});
req.onerror = (err) => {
  //an error happened to the request.
};
1
2
3
4
5
6
7
8
9
10
11

The example above shows both ways you can write the event listener functions. onerror and onsuccess or addEventListener('success', func) and addEventListener('err', func). You can use either.

If the error event fires, it will trigger the transaction error event.

If the success event fires, it will trigger the transaction complete event.

If you call the get method, then the ev.target.result value will be the single object that had the matching key.

If you call the getAll method, then the ev.target.result object will be an Array, regardless of the number of records returned.

If there are no matching keys for get or getAll the success event still fires. This is not an error. This is an empty result set.

# KeyRanges

When you want to bring back one or more records that match a range of keys then you can define an IDBKeyRange object to explain the possible range of values to match against.

Keys in a store can be a Number, an Array, or a String. When you pass the value of a key to the get method it MUST be an exact match for the value and datatype. Think JavaScript ===.

To create a key we can call one of the following methods.

IDBKeyRange.lowerBound(keyval, false);
IDBKeyRange.upperBound(keyval, false);
IDBKeyRange.bound(lowerKeyval, upperKeyval, false, false);
IDBKeyRange.only(keyval); //same as using a single key
1
2
3
4

The upperBound and lowerBound methods let you provide either a max or a min value for the keys to match.

The bound method lets you provide a min and max value for matching keys.

The true and false values represent whether or not you want to omit the value(s) provided in the keyval arguments. false means that you want to include the value.

KeyRange reference (opens new window)

# Indexes

An index can be created whenever you update a database's structure.

Think of it as an extra column that is created with a sorted version of one of the properties. When you want to apply a IDBKeyRange for a getAll or update or delete you need to have things sorted based on the property you are comparing to the key range.

You can create an index for every property but that is wasteful. Only create indexes for the properties that you will use for a getAll or update or delete.

To create an index we use this method inside our upgradeneeded event handler function:

objectStore.createIndex('priceIDX', 'price', { unique: false });
1

The first argument is the label we are giving our new index column.

The second argument is the property that will/does exist in our data.

The third argument is our options object. We need to specify if the values of our property are required to be unique or not. In this case we are saying that the values in the price property are allowed to be duplicates.

Once you have an index, then you can use it in place of a store object for creating the IDBRequests in your transaction. Here is an example that creates a transaction and then makes a request for all the objects from our store that match a price between $50 and $100, inclusive. The example uses a IDBKeyRange as well as the index created above.

let tx = db.transaction('myStore', 'readonly');
let store = tx.objectStore('myStore');
let idx = store.index('priceIDX');
let range = IDBKeyRange.bound(50, 100, false, false);
let req = idx.getAll(range);
req.onsuccess = (ev) => {
  let result = ev.target.result;
  result.forEach((obj) => {
    console.log(obj); //console each of the objects that matched.
  });
};
1
2
3
4
5
6
7
8
9
10
11

# Cursors

A cursor provides another way that we would move through the resulting recordset from a data request. It lets us provide a direction to move through the results - next, nextunique, prev, or prevunique.

It uses an openCursor(key|range, direction) method to create the cursor. The cursor has a success and an error event. Each time the success event fires it means that the next record is available. You will always get one record at a time. The success event will fire one last time to say that the records have all be traversed.

The openCursor(key|range, direction) method can be called on either a store or an index object.

idx.openCursor(range, 'next').onsuccess = (ev) => {
  let cursor = ev.target.result;
  if (cursor) {
    console.log(cursor.value);
    cursor.continue(); //call success again
  } else {
    //end of record set
  }
};
1
2
3
4
5
6
7
8
9

Return to week home page

Last Updated: 3/3/2021, 5:20:47 PM