IndexedDB fundamentals – Plus a indexeddb example – tutorial

IndexedDB is a relatively fresh NOSQL database implemented in modern browsers. In this blog post I’m going to show you how to perform some basic operations with it. The tutorial has been optimised for use with Google’s latest Chrome browser.

In Chrome’s developer tools, IndexedDB can be found in the Resources tab. For the moment, any changes to your IndexedDB databases aren’t automatically visible in the developer tools – you have to either refresh the page or right-click on “IndexedDB” in the Resources tab and choose “Refresh IndexedDB”.

After two short general remarks, we’ll dive right into the code!

Limits
IndexedDB has no hard storage limits on it’s own. However, browser vendors have soft limits. Firefox will ask for permission to store blobs bigger than 50 MB. Google Chrome has various limits for different use cases, for more information about Chrome limits see https://developers.google.com/chrome/whitepapers/storage

Fallback
For older browsers that don’t support IndexedDB, you can use IndexedDBShim. It allows WebSQL (which is no longer developed) to handle IndexedDB API calls when they are not present in the browser. Available at https://github.com/axemclion/IndexedDBShim.

Creating, dropping and retrieving databases

Firefox does not prefix IndexedDB, Chrome had prefixed it for a while but now no longer does it, and Internet Explorer uses an MS vendor prefix. To achieve compatibility across browsers, you might want to use the declaration solution below.


var indexedDB = window.indexedDB || window.webkitIndexedDB || window.msIndexedDB;
var IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange;
var openCopy = indexedDB && indexedDB.open;

var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction;

if (IDBTransaction)
{
IDBTransaction.READ_WRITE = IDBTransaction.READ_WRITE || 'readwrite';
IDBTransaction.READ_ONLY = IDBTransaction.READ_ONLY || 'readonly';
}

Remember IndexedDB is a NoSQL database, so it doesn’t contain tables, but object stores. Object stores contain data in the form <key, value> – where the value can be a simple string or a complex object. To create your first database, simply attempt to open it. Since it doesn’t exist yet, the upgradeneeded event will be triggered – which you handle in order to create your object store(s). Notice that whenever you want to modify your databases object store, you need to do this by increasing the version number of your database – the version upgrade will again trigger the upgradeneeded event, which you can handle to make the necessary modifications. To open a database indicating that you want to create a new version, simply add the version number as the second parameters to your indexedDb.open() call. Dropping a database is straightforward.


/***
* Create database snippet
* */
var request = indexedDB.open('todos');

request.onupgradeneeded = function(e)
{
// e is an instance of IDBVersionChangeEvent
var idb = e.target.result;

if (idb.objectStoreNames.contains('todo'))
{
idb.deleteObjectStore('todo');
}

var store = idb.createObjectStore('todo', {keyPath: 'text', autoIncrement: true});
// createIndex operations possible to be pefromed on store.createIndex
store.createIndex('by_todo', 'todo', {unique: true, multiEntry: false});
};

request.onsuccess = function(e) { /* add, update, delete, ... */ };
request.onerror = function(e) { /* handle error */ };

/***
* Remove database snippet
* */
var dropDatabase = function(name)
{
var request = indexedDB.deleteDatabase(name);
request.onsuccess = function() { /* drop succeeded */ };
request.onerror = function() { /* drop failed */ };
};

If you want to programatically find out which databases are available, you are currently limited to webkit (other browsers don’t offer such a method). Note that IndexedDB follows a same-origin policy – so you won’t be able to read databases from domains other than your own.


indexedDB.webkitGetDatabaseNames().onsuccess = function(e)
{
var databaseNames = [];
for (var i = 0, l = e.target.result.length; i < l; i++)
databaseNames.push(e.target.result[i]);
};

Creating records

Adding a record to object store requires creating a “readwrite” transaction, as demonstrated below. If your browser supports IndexedDB, you can populate a database with a todo object store and some example entries by clicking the button below the code sample. Make sure you have your dev tools open so that you can see what is going on on the database.


var request = indexedDB.open('todos');

request.onsuccess = function(e)
{
var idb = e.target.result;
var trans = idb.transaction('todo', IDBTransaction.READ_WRITE);
var store = trans.objectStore('todo');

// add
var requestAdd = store.add({text: 'Go to Coop', todo: 'Groceries'});

requestAdd.onsuccess = function(e) {
// do something
};

requestAdd.onfailure = function(e) {
// failed
};
};

Retrieving records

You can retrieve records in a read-only transaction. For iterating over all entries in an object store, simply use the openCursor function. If you have previously chosen to add the six predefined todo entries to the demo store, you can retrieve them using the button below the code.


var request = indexedDB.open('todos');
request.onsuccess = function(e)
{
idb = e.target.result;
var transaction = idb.transaction('todo', IDBTransaction.READ_ONLY);
var objectStore = transaction.objectStore('todo');

objectStore.openCursor().onsuccess = function(event)
{
var cursor = event.target.result;
if (cursor)
{
console.log('Cursor data', cursor.value);
cursor.continue();
}
else
{
console.log('Entries all displayed.');
}
};
};

When opening a cursor to the object store, you can specify various parameters for limiting the scope of the cursor. The following examples demonstrate several of these constraints. Again, if you have chosen to use the build-in demo via the buttons, you can continue to do so for every code example.

The interface we are using to apply constraints to our cursor is IDBKeyRange. When specifying bounds, you can indicate if you want them to be open (excluding the endpoint values, pass true) or closed (including the endpoint values, pass false – this is the default). For in-depth information refer to the documentation on the Mozilla Developer Network.


...
// Retrieve all records after and including 'Groceries' key
var cursor = IDBKeyRange.lowerBound('Groceries', false);
objectStore.openCursor(cursor).onsuccess = function(event)
...


...
// Retrieve all records up to and including 'Groceries' key
var cursor = IDBKeyRange.upperBound('Groceries', false);
objectStore.openCursor(cursor).onsuccess = function(event)
...


...
// Retrieve all records between 'Bills' and 'Flat' keys (inclusive)
var cursor = IDBKeyRange.bound('Bills', 'Flat', false, false);
objectStore.openCursor(cursor).onsuccess = function(event)
...


...
// Retrieve only the record matching the 'FeedTheDog' key
var cursor = IDBKeyRange.only('FeedTheDog');
objectStore.openCursor(cursor).onsuccess = function(event)
...

 

Editing data

To edit or delete records on a database, you will need to create a read-write transaction. The following examples show you how you can edit a record (basically, retrieve it and update it via the object store’s put method) and delete a record (calling delete on the object store and passing the key). If you have populated the demo database using the buttons before, you can now edit and delete these records using the buttons below the code.


// Editing a record
var editRecord = function(key, newValue) {
var request = indexedDB.open('todos');
request.onsuccess = function(e)
{
var idb = e.target.result;
var objectStore = idb.transaction('todo', IDBTransaction.READ_WRITE).objectStore('todo');
var request = objectStore.get(key);

request.onsuccess = function(ev)
{
var data = ev.target.result;
var editDivEl = document.querySelector('#editRecordDiv');

if (data === undefined)
{
editDivEl.innerHTML = 'Key doesnt exist or has been previously' +
'removed';
return;
}

data.text = newValue;
var result = objectStore.put(data);

result.onsuccess = function(ev)
{
var todoName = ev.target.result;
editDivEl.innerHTML = 'Successfully edited key <b>' +
todoName + '</b>';
};

result.onerror = function(ev)
{
console.log('Error occured', ev.srcElement.error.message);
};
};

request.onerror = function(ev)
{
console.log('Error occured', ev.srcElement.error.message);
};
};
};

var reminderDate = new Date();
editRecord('Bills', 'Reminder ' + reminderDate.getHours() + ':' + reminderDate.getMinutes() +
':' + reminderDate.getSeconds());


// Deleting a record
var request = indexedDB.open('todos');
request.onsuccess = function(e)
{
var idb = e.target.result;
var objectStore = idb.transaction('todo', IDBTransaction.READ_WRITE).objectStore('todo');
var request = objectStore.delete('Bills');

request.onsuccess = function(ev)
{
console.log(ev);
};

request.onerror = function(ev)
{
console.log('Error occured', ev.srcElement.error.message);
};
};

Limiting queries

As a final example in this tutorial, I want to show you a way of limiting the number of results returned by a query. IndexedDB doesn’t provide a straightforward method for limiting and grouping database results. Therefore, I have created a simple helper function that helps us limiting the number of records retrieved by a query. Note that in such a context, the IDBCursor.advance method might come in handy, as it lets you skip retrieved records. In the example below, we abort the transaction once the indicated limit has been reached.


/***
* IndexedDB limit output syntax similar to LIMIT 10, 5;
* */
var limitRecords = function(pageSize, skipCount)
{
var request = indexedDB.open('todos');

request.onsuccess = function(e)
{
idb = e.target.result;
var transaction = idb.transaction('todo', IDBTransaction.READ_ONLY);
var objectStore = transaction.objectStore('todo');
var idx = 0;

objectStore.openCursor().onsuccess = function(e)
{
var cursor = e.target.result;
if (cursor)
{
if (skipCount <= idx && idx < pageSize + skipCount)
console.log('Cursor is in the range ', cursor);

idx++;

if (idx >= pageSize + skipCount + 1)
{
// we have all data we requested
// abort the transaction
transaction.abort();
} else {
// continue iteration
cursor.continue();
}
}

};
};
};

Done.

We hope you’ve enjoyed this little tutorial! Of course, there is much more to IndexedDB than what we have covered here – this was just to get you started!









1 reply

Comments are closed.