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:
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:
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:
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:
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:
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) {
};
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:
transaction.oncomplete = function(evt) {
};
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:
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. Cursor
s are opened against an objectStore
which is part of a transaction
. Here is an example of using a cursor
:
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. Cursor
s 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:
<!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) {
};
}, 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) {
};
}, 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.