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 consists of a key path and a corresponding value
which can be a simple type like string or date and more advance 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 isn’t 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 then the provided version the upgradeneeded event will be fired and you will be able to
change the database’s structure in it’s 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 the a property with the
exact name of the key path. If a property with the exact name doesn’t exists 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: "[email protected]" },
{ name: "Don Dow", email: "[email protected]" }
];
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 exists 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. Cursors are opened against an obejctStore 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. 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:
<!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: "[email protected]" },
{ name: "Don Dow", email: "[email protected]" }
];
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 a key/value pairs are needed with very small amount of data, the Web Storage is
much more suitable then 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.