Using IndexedDB on Firefox


As Firefox 4 is now stable, chances are that the async IDB (IndexedDB) API is not going to change anymore. But IDB is not localStorage – it can be a major pain to work with. So, here is a guide on how to use IDB in Firefox as a key-value store.

TL;DR
There is example code available at the end of the post. If you don‘t want to read this lengthy post and/or prefer to jump right in the middle of the action, grab the example implementation and play around with it.

Key-Value Store? But, IDB is a full-blown database!

Yes, it is. But I still argue that in WebApp context a key-value store will cover about 90% of use cases. So I won‘t cover in this post how to create fancy key ranges or how to create multiple indices. But I will cover the five basic methods that you will need to work with any store: get, set, remove, getAll and clear – this will enable you to work with IDB and give you an understanding of how it works. You‘re then of course heartly encouraged to explore it and it‘s features further!

A word on requests, transactions and events

As long as the sync API is not there, all operations are async. That means that you will make requests. All the time. These requests will have an onerror and an onsuccess property, where you can put your callbacks/handlers in. If your request was successful, you will get an event in your callback. That event contains a lot of useful and useless information, but what you will want to look at in most cases is event.target.result – this is where your result lives in. In most cases, you will have to start a transaction before; inside of the transaction, you can access the objectStore and make your request. However, there are different types of transactions, but you will get the details below.

Getting the Object Store ready to use

Yep, it‘s not that you can just start away and store your data, there‘s some work to do ahead…

Open the Database

Easy one:

var openRequest = mozIndexedDB.open(dbName, dbDescription);
openRequest.onsuccess = function(event){
    //event.target.result contains a reference to the db
}

event.target.result then contains a reference to the database. Store it, you will need it.

Creating/Opening the ObjectStore

That‘s a little tougher. Some operations, such as creating and deleting object stores, require the database to be in a „mutation transaction state“. Wow. Well, there‘s only one way to put the db into this state, and that‘s via a setVersion request. If you are opening the db for the first time ever, you will need to do this anyway. You can, of course, change the version of the database anytime, which might be a useful thing if you, e.g., change the structure of how your objectStore saves the data. To check what version the database in the user‘s browser has, you can check the version property in the reference to the database anytime.

var versionRequest = db.setVersion(dbVersion);
versionRequest.onsuccess = function(){
    // Woohoo, the database is in a mutation transaction!!
};

Inside of the callback function, you can now check if your store already exists, or if you need to create it. The reference to your db contains an object called objectStoreNames, which is a list of existing objectStores. As this is a DOMStringList, it features the handy contains() method:

var hasMyStore = db.objectStoreNames.contains(storeName);

If the store is not there, you need to create it via the createObjectStore() method. It takes two arguments: the name of the store, and an object with two properties: keyPath and autoIncrement. keyPath is… well… the path to the key. Think of it as the one column that is the primary key in your data table (you all did some MySQL at some time, I bet). You can do really fancy things with the keyPath in objectStores, but it‘s perfectly fine to just use something like „id“ as the keyPath. If you want to store an object and want to provide the keyPath, all you need to do is let the object you want to store have a property with the name of the keyPath, and a unique value. Uh, it‘s not that complicated, examples will follow. AutoIncrement is, well, autoIncrement. You have the option to let the objectStore take care for the uniqueness of it‘s data‘s keyPath properties, or do it yourself.

var store = db.createObjectStore(storeName, {keyPath: 'id', autoIncrement: false});

You immediately get a reference to the newly created objectStore, so save it.

If the store is already there, you need to open it. Um, well, you don‘t really have to open it, but you might want to obtain a reference to it, just to make sure that it really exists. To do this, you need to start a transaction via the transaction() method. It accepts three arguments: an array of store names (the ones which you want to work with), an integer denoting whether you want to read and/or write during that transaction, and a timeout. There are constants for read/write you can use for better readability in your code, you can find them at IDBTransaction.READ_ONLY and IDBTransaction.READ_WRITE. The timeout parameter is optional. That method returns a transaction object that contains a method called objectStore, which you can use to access your store. This is the „default“ way transactions work; the mutation transaction above is an eception to this, where you need to be in the callback of the transaction to modify the database.

In our case, we can start an „empty“ transaction:

var emptyTransaction = db.transaction([], IDBTransaction.READ_ONLY);
var store = emptyTransaction.objectStore(storeName);

There you have your reference to the store.

Data Manipulation

Now that the store is ready to use, it‘s time to store some data in it. To do so, you need to start a transaction, access the object store, and call the store‘s put method:

var putTransaction = db.transaction([storeName], IDBTransaction.READ_WRITE);
var putRequest = putTransaction.objectStore(storeName).put(dataObj);
putRequest.onsuccess = function(event){
    //event.target.result contains the value of the keyPath of the data written (the „key“)
};

That‘s all. This type of transaction is what you also use for other data manipulation tasks, for example to retrieve data:

var getTransaction = db.transaction([storeName], IDBTransaction.READ_ONLY);
var getRequest = getTransaction.objectStore(storeName).get(key);
getRequest.onsuccess = function(event){
    //event.target.result contains the data object
};

You will need to provide the key of the stored entry to get it; as you need to do when you want to remove it:

var removeTransaction = db.transaction([storeName], IDBTransaction.READ_WRITE);
var deleteRequest = removeTransaction.objectStore(storeName).delete(key);
deleteRequest.onsuccess = function(event){
    //event.target.result contains the key of the deleted data object
};

If you want to get all data objects from the store:

var getAllTransaction = db.transaction([storeName], IDBTransaction.READ_ONLY);
var getAllRequest = getAllTransaction.objectStore(storeName).getAll();
getAllRequest.onsuccess = function(event){
    //event.target.result contains an array of all data objects
};

And, finally, if you want to clear (i.e., delete all data objects) the store:

var clearTransaction = db.transaction([storeName], IDBTransaction.READ_WRITE);
var clearRequest = clearTransaction.objectStore(storeName).clear();
clearRequest.onsuccess = function(event){
    //event.target.result contains undefined
};

But how‘s this better than localStorage?

It isn‘t. Yeah, well, it is, as you can store objects and don‘t have to stringify them before. But basically, to get the real advantages of IDB, you will need to dive deeper into it. Features like keyRanges remove the need to do map/reduce action when you search for items in your store. But now you have an idea of how the whole thing works. If you want to go further, browse in the spec or the MDC docs and try things out!

Example Code

There is an example page over here: http://static.uxebu.com/~jens/indexeddb/ff-idb.html.

It uses an ObjectStore wrapper I created using dojo that covers the basic methods mentioned above, the JavaScript is here: http://static.uxebu.com/~jens/indexeddb/idb-ff.js.

When you open the page, open the console and you will see some messages there. Check out the source to see what‘s going on; it‘s basically a round trip through all the basic methods.

Further Reading

  • https://ronomon.com Joran Greef

    It’s not quite a full-blown database either. You can’t do set operations on indices. You can’t manage indices at time of putting/deleting objects. You have to predefine indexes, so in a sense IndexedDB is not schema-less and this introduces headaches when you need to migrate your data structures. This imposes performance problems on mobile devices, since the underlying key-path mechanism requires objects to be deserialized and then serialized for every index you create (whereas if IndexedDB supported application-managed indices you’d be free to do this in any novel way and there are plenty). If you start storing more than 5,000 objects in IndexedDB then it will bring something like an iPad to its knees (or you may be unable to reason about the indexing taking place in the background). Considering that most indexed key/value stores get this point right, it’s frustrating to see that this design flaw in the specification, which is complicated as a result, without providing any benefits, despite the limitations.

    IndexedDB is an order of magnitude slower than WebSQL. In fact, some browsers have simply based their IndexedDB implementation on WebSQL. Mozilla have deprecated WebSQL. Developers must get out there on the W3C public web apps forum and ask for it to be added to Firefox. Then we can have two competing database APIs in Chrome, Safari, Firefox and Opera (and hopefully IE9). Don’t let these things pass you by. Mozilla is the only one blocking this, and for conjectural reasons at that.

    SQLite (the reference implementation for WebSQL) is the world’s most widely deployed database. It has millions of tests in its test suites, is superbly efficient, and is often more performant than other SQL databases. It’s hard to imagine IndexedDB coming anywhere close (and it’s disappointing and crippled to start with, these issues are simply being relegated to a “version 2″ which may take a few more years to materialize). Better to have something that works and works well, and is practically a standard, then something which is designed by a committee of which only one or two members have probably ever designed databases before.