Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML

Getting Started with IndexedDB

4.95/5 (32 votes)
2 Aug 2012CPOL7 min read 188.3K   2.2K  
The article describes the HTML5 IndexedDB API and how to use it.

Introduction

One of the new JavaScript APIs that HTML5 has to offer is the IndexedDB API. In the past, I wrote a post about the Web Storage API which is a simple key/value dictionary that is stored in the web browser and persists data. IndexedDB is a full blown index database which adds more offline capabilities to Web applications. In this article, I’ll present the IndexedDB API and explain some basic concepts about it.

Background - What is IndexedDB?

IndexedDB API is a specification for an index database which exists in the browser. The IndexedDB is made of records holding simple values and hierarchical objects. Each of the records consist of a key path and a corresponding value which can be a simple type like string or date and more advanced types like JavaScript objects and arrays. It can include indexes for faster retrieval of records and can store large amount of objects.

IndexedDB has two API modes – synchronous and asynchronous. Most of the time, you will use the asynchronous API. The synchronous API was created to be used only with conjunction with Web Workers (and it is currently not supported by most of the browsers).

The IndexedDB API is exposed through the window.indexedDB object. The API isn’t fully supported by most of the browsers today. The major browsers that support the API expose the indexedDB object with their prefixes. The following line of code should be used before you use the indexedDB currently and you should use libraries like Modernizr to detect if the browser supports the API:

JavaScript
var indexedDB = window.indexedDB || window.webkitIndexedDB || 
                window.mozIndexedDB || window.msIndexedDB;

In the day of writing the post, IndexedDB is supported by Firefox from version 4 (Firefox is currently the most updated browser with regard to the specifications), Chrome from version 11 and IE10.

Using the IndexedDB API, you can take Web applications offline and decrease the number of server round-trips since you can store common data in a local database instead of the server database.

Opening a Database

Before you can start using the IndexedDB, you first need to open the database for use. Since the IndexedDB is working asynchronous calling the open function will return an IDBRequest object which you will use to wire a success and error event handlers. Here is an example of opening a database:

JavaScript
var db;
var request = indexedDB.open("TestDatabase");
request.onerror = function(evt) {
  console.log("Database error code: " + evt.target.errorCode);
};
request.onsuccess = function(evt) {
  db = request.result;
};

In the example, a call to the open function is used to open a database with the name TestDatabase. After the call, two callback functions are wired to the returned IDBRequest, one for the onerror and one for the onsuccess. In the success callback, you can get the database object from the request and store it for further use.

The open function accepts another parameter which isn’t passed in the example which is the version number of the database. The version number is used to change the version of the database. In the case where the database’s version is smaller than the provided version, the upgradeneeded event will be fired and you will be able to change the database’s structure in its handler. Changing the version of the database is the only way to change the structure of the database.

Creating an objectStore

The IndexedDB can hold one or more objectStores. objectStores resemble tables in relational databases but are very different from them. They hold the key/value records and can have key paths, key generators and indexes. You use the IndexedDB’s createObjectStore function to create an objectStore. The function gets a name for the objectStore and an options object to configure things like key paths and key generators.

Key paths and key generators are used to create the main index for the stored value. The key path is a string that defines how to extract a key from the given value. It is used with JavaScript objects which have a property with the exact name of the key path. If a property with the exact name doesn’t exist, you need to supply a key generator such as autoIncrement. The key generator is used to hold any kind of value. It will generate a key automatically for you but you can also pass your own key for a stored value if you want.

objectStores can also have indexes which will be used later for data retrieval. Indexes are created with the objectStore createIndex function which can get three parameters – the name of the index, the name of the property to put the index on and an options object.

Here is an example of creating an objectStore inside the onupdateneeded event handler:

JavaScript
var peopleData = [
    { name: "John Dow", email: "john@company.com" },
    { name: "Don Dow", email: "don@company.com" }
];
 
function initDb() {
    var request = indexedDB.open("PeopleDB", 1);  
    request.onsuccess = function (evt) {
        db = request.result;                                                            
    };
 
    request.onerror = function (evt) {
        console.log("IndexedDB error: " + evt.target.errorCode);
    };
 
    request.onupgradeneeded = function (evt) {                   
        var objectStore = evt.currentTarget.result.createObjectStore("people", 
                                     { keyPath: "id", autoIncrement: true });
 
        objectStore.createIndex("name", "name", { unique: false });
        objectStore.createIndex("email", "email", { unique: true });
 
        for (i in peopleData) {
            objectStore.add(peopleData[i]);
        }
    };
}

The example show some important things:

  • onupdateneeded is called before the onsuccess callback and therefore you can use the evt.currentTarget.result to get the database which is getting opened.
  • The key path is created with an id string which doesn’t exist in the supplied object. The key path is used with conjunction with the autoIncrement option to create an incrementing key generator.
  • You can use the unique constraint on indexes in order to enforce simple constraints. When the unique option is true, the index will enforce the constraint for inserted emails.
  • You can use the objectStore’s add function to add records to the objectStore.

Creating a Transaction

When you have an objectStore, you will probably want to use it with CRUD (create/read/update/delete) operations. The only way to use CRUD in IndexedDB is through an IDBTransaction object. The IDBTransaction is also supported with browser prefixes currently (like the IndexedDB object), so the following line of code should be used:

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

The IDBTransaction object can be created in three modes: read-only, read/write and snapshot. You should use the read/write mode only when you want to update the objectStores and read-only in other cases. The reason for that is that read-only transaction can run concurrently. By default, transactions run in read-only mode.

The transactions are asynchronous as all the other IndexedDB API calls. That means that you can wire handlers for their error, abort, and complete events. Here is an example of opening an add transaction:

JavaScript
var transaction = db.transaction("people", IDBTransaction.READ_WRITE);
var objectStore = transaction.objectStore("people");                    
var request = objectStore.add({ name: name, email: email });
request.onsuccess = function (evt) {
    // do something when the add succeeded                          
};

The example shows that you first create a transaction object for the people objectStore. Then, you retrieve the objectStore from the transaction object and perform an operation on it. That operation is called asynchronous and therefore you wire an onsuccees event handler to deal with the request’s success. In the example, I didn’t wire any of the transaction event handlers but you can use them like in the following example:

JavaScript
transaction.oncomplete = function(evt) {  
  // do something after the transaction completed  
};

Retrieving Data

In order to retrieve data from the objectStore, you will use a transaction object and also the objectStore’s get function. The get function expects a value which will be used against the key path of the objectStore. Here is an example of using the get function:

JavaScript
var transaction = db.transaction("people");  
var objectStore = transaction.objectStore("people");  
var request = objectStore.get(1);  
request.onsuccess = function(evt) {  
  alert("Name for id 1 " + request.result.name);  
};

Another way to retrieve data is using a cursor. You will use cursors when the key path isn’t known to you. Cursors are opened against an objectStore which is part of a transaction. Here is an example of using a cursor:

JavaScript
var transaction = db.transaction("people", IDBTransaction.READ_WRITE);
var objectStore = transaction.objectStore("people");
 
var request = objectStore.openCursor();
request.onsuccess = function(evt) {  
    var cursor = evt.target.result;  
    if (cursor) {  
        output.textContent += "id: " + cursor.key + " is " + cursor.value.name + " ";        
        cursor.continue();  
    }  
    else {  
        console.log("No more entries!");  
    }  
};

In the example, the openCursor function is called against the objectStore. Then, an onsuccess function is wired to the cursor request and is used to write to a div called output the data which was retrieved by the cursor. The previous example is a very simple cursor example. Cursors can be used with more sophisticated queries which won’t be shown in this post.

The Full Example

Here is a full example of some IndexedDB concepts:

HTML
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <title>IndexedDB</title>
    <script type="text/javascript">
        var indexedDB = window.indexedDB || window.webkitIndexedDB || 
                        window.mozIndexedDB || window.msIndexedDB;
        var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction;
        var db;
        (function () {     
            var peopleData = [
                { name: "John Dow", email: "john@company.com" },
                { name: "Don Dow", email: "don@company.com" }
            ];
 
            function initDb() {
                var request = indexedDB.open("PeopleDB", 1);  
                request.onsuccess = function (evt) {
                    db = request.result;                                                            
                };
 
                request.onerror = function (evt) {
                    console.log("IndexedDB error: " + evt.target.errorCode);
                };
 
                request.onupgradeneeded = function (evt) {                   
                    var objectStore = evt.currentTarget.result.createObjectStore(
                             "people", { keyPath: "id", autoIncrement: true });
 
                    objectStore.createIndex("name", "name", { unique: false });
                    objectStore.createIndex("email", "email", { unique: true });
 
                    for (i in peopleData) {
                        objectStore.add(peopleData[i]);
                    }
                };
            }
 
            function contentLoaded() {
                initDb();                
                var btnAdd = document.getElementById("btnAdd");
                var btnDelete = document.getElementById("btnDelete");
                var btnPrint = document.getElementById("btnPrint");                
 
                btnAdd.addEventListener("click", function () {
                    var name = document.getElementById("txtName").value;
                    var email = document.getElementById("txtEmail").value;
 
                    var transaction = db.transaction("people", IDBTransaction.READ_WRITE);
                    var objectStore = transaction.objectStore("people");                    
                    var request = objectStore.add({ name: name, email: email });
                    request.onsuccess = function (evt) {
                        // do something after the add succeeded
                    };
                }, false);
 
                btnDelete.addEventListener("click", function () {
                    var id = document.getElementById("txtID").value;
 
                    var transaction = db.transaction("people", IDBTransaction.READ_WRITE);
                    var objectStore = transaction.objectStore("people");
                    var request = objectStore.delete(id);
                    request.onsuccess = function(evt) {  
                        // It's gone!  
                    };
                }, false);
 
                btnPrint.addEventListener("click", function () {
                    var output = document.getElementById("printOutput");
                    output.textContent = "";
 
                    var transaction = db.transaction("people", IDBTransaction.READ_WRITE);
                    var objectStore = transaction.objectStore("people");
 
                    var request = objectStore.openCursor();
                    request.onsuccess = function(evt) {  
                        var cursor = evt.target.result;  
                        if (cursor) {  
                            output.textContent += "id: " + cursor.key + 
                                        " is " + cursor.value.name + " ";                            
                            cursor.continue();  
                        }  
                        else {  
                            console.log("No more entries!");  
                        }  
                    };  
                }, false);              
            }
 
            window.addEventListener("DOMContentLoaded", contentLoaded, false); 
    })();       
    </script>
</head>
<body>
    <div id="container">
        <label for="txtName">
            Name:
        </label>
        <input type="text" id="txtName" name="txtName" />
        <br />
        <label for="txtEmail">
            Email:
        </label>
        <input type="email" id="txtEmail" name="txtEmail" />
        <br />
        <input type="button" id="btnAdd" value="Add Record" />
        <br />
        <label for="txtID">
            ID:
        </label>
        <input type="text" id="txtID" name="txtID" />
        <br />
        <input type="button" id="btnDelete" value="Delete Record" />
        <br />
        <input type="button" id="btnPrint" value="Print objectStore" />
        <br />
        <output id="printOutput">
        </output>
    </div>
</body>
</html>
Pay attention – This example will only work on Firefox 10 since Firefox 10 is currently the only browser that updated the IndexedDB API implementation to use the latest specification version.

IndexedDB and Web Storage APIs

As written in the introduction, there are two kinds of data stores in the browsers – the IndexedDB and the Web Storage. One of the questions that I hear a lot is why to have two different storage types? In simple scenarios where key/value pairs are needed with very small amount of data, the Web Storage is much more suitable than IndexedDB and can simplify your work. On the other hand, in scenarios where you need efficient search for values or you have large number of objects that you want to store on the client-side, IndexedDB is preferable. Both of the options complement each other and can be used together in the same application.

Summary

IndexedDB includes a massive API for using a built-in index database in the browser. It can be used to store data on the client-side and with Web Storage to offer to opportunity to take applications offline and to reduce server round-trips for data retrieval. For further information about IndexedDB, you can go to its specifications in this link.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)