All IndexedDB functionality is accessed through the window.indexedDB object, which is an IDBFactory object containing an open and a deleteDatabase method (there is also a cmp method available for comparing two keys).
Every operation with IndexedDB is asynchronous: you make a request for something to happen and you're returned an IDBRequest object. You then attach to the IDBRequest object's onerror and onsuccess event handlers to act on the results when they become available.
The IDBFactory's open and deleteDatabase methods return an IDBOpenDBRequest object, which is derived from the IDBRequest object and provides two additional event handlers: onblocked and onupgradeneeded.
Opening a Database
To open a database, or to create one if it doesn't already exist, we simply call the open method of the window.indexedDB object and specify the desired database name.
The open method accepts a version number for the second parameter and will default to 1 if the database doesnt't exist (if a database doesn't exist, when open is called, the database gets created). If specified, the version number must be greater than 0.
If the request to open a database was successful, the event object passed into the onsuccess event handler will contain an IDBDatabase object which we can then use to manipulate the objects in the database.
The following is an example of how you request that a database be opened:
var g_db = null;
var dbRequest = window.indexedDB.open("Example");
dbRequest.onerror = function (evt) {
alert("Database error: " + evt.target.error.name);
}
dbRequest.onsuccess = function (evt) {
g_db = evt.target.result; // IDBDatabase object
}
Creating an Object Store
Each database can have one or more object stores which hold your records as key/value pairs. Each object store has a name and the name must be unique within the database. The records within an object store are sorted in ascending order based on the keys and each key must be unique for that object store. Each object store has an optional key generator and an optional key path. If you specify a key path, the object store is said to use in-line keys (the key is stored as part of the value). If you don't specify a key path, the object store is said to use out-of-line keys (the key is stored separately from the value that is being stored). There are three ways that keys can be specified for an object store:
- A key generator which can generate a monotonically increasing number every time a key is needed
- Keys can be derived from a key path
- Keys can be explicitly specified when data is being added to the object store
Each record in an object store has a value which can be primitives (string, number, boolean, null, and undefined) as well as anything supported by the structured clone algorithm which includes things like objects, arrays, dates, and even files from the HTML5 File API.
When a database is first created, or when a version number is specified that is higher than the current database version, an onupgradeneeded event handler will be triggered as part of the database versionchange transaction.If the upgradeneeded event is triggered, the onsuccess event handler of the open database request, will only be triggered after the versionchange transaction has completed.
The onupgradeneeded event handler is where all database structure changes are performed, including the creation of object stores. To create an object store, you need to call the createObjectStore method on the IDBDatabase object which can be found in the event's target.result property of the onupgradeneeded event handler.
The following example illustrates how to create an object store called "Employees" that will use a key path:
dbRequest.onupgradeneeded = function (evt) {
var db = evt.target.result;
var objectStore = db.createObjectStore("Employees", { keyPath: "EmpId" });
}
Creating an Index
Indexes allow you to look up of data within an object store using properties of the values stored rather than just using the keys.
An index is a special object store for looking up records in another object store (also known as a referenced object store). The records in an index are automatically adjusted when items are added, updated, or deleted in the referenced object store. Several indexes can reference the same object store.
Indexes can also be used to enforce some database rules. For example, an index contains a unique flag which, when set to true, can be used to prevent having two records with the same value in an object store (an error would be thrown if the result of an insert/update results in a duplicate value).
Indexes also contain a multiEntry flag which effects how an index behaves when it comes to arrays. When set to false, a single record whose key is an array is added to the index. When set to true, however, a record is added to the index for each array item allowing you to filter on the individual items of the array.
To create an index, you need to call the createIndex method on the IDBDatabase object passing in a name for the index as the first parameter, a keypath as the second parameter, and an optional third parameter object indicating if the index is to be unique or not or if multiEntry is enabled or not.
Since indexes modify the database structure, indexes must be created and modified within the onupgradeneeded event handler as in the following example:
dbRequest.onupgradeneeded = function (evt) {
// ...object stores created...
/* Create an index so we can search by employee names. Multiple employees might have the same name so we don't want the index to be unique */
objectStore.createIndex("EmpName", "EmpName", { unique : false });
/* Create an index to search employees by company email address. Since, typically, no two employees would share the same work email address, we can put a restriction on the records to enforce that each record have a unique email address */
objectStore.createIndex("EmpEmail", "EmpEmail", { unique : true });
}
Starting a Transaction
All reading or modification of data within a database must happen within a transaction.
The scope of a transaction is determined when the transaction is created and is simply what object stores and indexes the transaction has access to. A database can have multiple active transactions at once and, with readonly transactions, any number of them can execute at the same time even if they overlap scopes.
Unlike transactions of other database technologies, with IndexedDB, the transactions are auto-committed if the call is successful (your code doesn't need to explicitly call a commit method). If an error is thrown, or the call is explicitly aborted by code, then the transaction automatically rolls back. A database can have multiple active transactions at once and, with readonly transactions, any number of them can execute at the same time even if they overlap scopes.
Be especially careful with readwrite transactions because, if the scope overlaps, transactions will not execute at the same time. They will instead be queued up which could slow down the responsiveness of your web application. There are three transaction modes available:
- readonly
- readwrite
- versionchange - This transaction is automatic when a database is first created or when the version number is changed. We saw this transaction in action earlier with the onupgreadeneeded event handler which is triggered by this transaction.
To create a transaction we need to call the transaction method of our IDBDatabase object, which will return us an IDBTransaction object.
The transaction method accepts two parameters. The first parameter is an array of object store names that we want as part of the transaction. The second parameter is optional and indicates the transaction mode we want. If not specified, the second parameter defaults to readonly mode.
The following is an example of how you would create a readwrite transaction on the Employees object store:
var dbTrans = g_db.transaction(["Employees"], "readwrite");
Note:Remember transactions are expected to be short-lived.
Adding Data to an Object Store
To add data to an object store, we could populate the object store during the onupgradeneeded event handler since we're already in a transaction. Because we would already have a reference to the object store, it's just a matter of calling the object store's add method as in the following example:
dbRequest.onupgradeneeded = function (evt) {
// ...object stores and indexes created...
// Add the employee array items to the object store
for (var i in arrEmployees) {
objectStore.add(arrEmployees[i]);
}
}
Adding records to an object store during the upgradeneeded event is fine when the database is being created or upgraded but what about when the database already exists and we don't need to update the version?
To add a record to an object store, we first need to obtain a readwrite transaction on the object store we wish to add data to by calling the transaction method on our IDBDatabase object.
Once we have the IDBTransaction object we can call that object's objectStore method requesting one of the object stores that we specified when we called the transaction method.
The objectStore method returns us an IDBObjectStore object which we can then use to call the add method to request our object be inserted as in the following example:
var objEmployee = { "EmpId": "101", "EmpName": "Sam Smith", "EmpEmail": "SamSmith@SomeCompany.com" };
// Create a readwrite transaction for the Employees object store
var dbTrans = g_db.transaction(["Employees"], "readwrite");
// Get the IDBObjectStore object for the Employees object store
var dbObjectStore = dbTrans.objectStore("Employees");
// Request the addition of our object to the object store
var dbAddRequest = dbObjectStore.add(objEmployee);
dbAddRequest.onsuccess = function (evt) { alert("Success!"); }
dbAddRequest.onerror = function (evt) { /* handle the error */ }
There is also a put method that can be used. The put method will act like an add method if the record doesn't exist yet but, if the record does exist, the put method overwrites the existing record with the new data. The code works exactly the same as in the above example with the exception that you request a put rather than an add.
Both the add and put requests accept an optional second parameter which is how you would specify the key for the record if you're not using inline keys.
Querying Data
Requesting a record from an object store is very similar to adding it to the object store: You create your transaction (a readonly transaction will do in this case), request the object store object, and then request the record as in the following example:
/* Since the 2nd parameter of the transaction method is optional and defaults to "readonly" we've left it out to reduce code. Also, since we don't need the IDBTransaction object after we call the objectStore method we can chain the calls together and not bother storing the transaction object to a variable */
var dbObjectStore = g_db.transaction(["Employees"]).objectStore("Employees");
// A Get is based on the key. In our case, it's the EmpId value
var dbGetRequest = dbObjectStore.get("101");
dbGetRequest.onsuccess = function (evt) {
alert("Success! The employee's name: " + evt.target.result.EmpName); }
dbGetRequest.onerror = function (evt) { /* handle the error */ }
Using a Cursor
A get is useful if you only want one record and you know the key value of the record you're looking for.
If you wish to iterate over multiple records, perhaps to build up a list of items for display, you'll need to use a cursor.
Note:If you wish to loop over records in search of a specific record, cursors are not the recommended approach. See the "Using an Index" section which will discuss using cursors on indexes.
As with a get request, to use a cursor you must first create a transaction and then obtain the object store object which you can then call the openCursor method on.
The first parameter of openCursor method is an optional key range which is used to restrict the number of records returned (if not specified, all records from the object store are returned).
You can create a key range object by calling one of the following methods on the IDBKeyRange object: only, lowerBound, upperBound, and bound. The second parameter of openCursor is also optional and specifies the direction that you will iterate over the results: "next" is for ascending order which is the default and "prev" is for descending order.
If you want to specify the second parameter without having to specify the first one, you can pass in a null for the first parameter.
The following is an example of using a cursor to loop over an entire object store's records (no range filter and using the default direction):
var dbCursorRequest = dbObjectStore.openCursor();
dbCursorRequest.onsuccess = function (evt) {
var curCursor = evt.target.result;
if (curCursor) {
/* Grab the current employee object from the cursor's value property (we could also grab the key) */
var objEmployee = curCursor.value;
// ...do something with objEmployee...
// Cause onsuccess to fire again with the next item
curCursor.continue();
} // End if
}
dbCursorRequest.onerror = function (evt) { /* handle the error */ }
A few notes about cursors:
- To move to the next record in the object store, you call the continue method of the cursor object which will cause the onsuccess event handler to fire again for the next record.
- If there are no more records in the object store, the cursor will be undefined. You will need to test for this condition before trying to access the key or value.
Using an Index
If you ever need to look for records based on a value other than the key, it's much more efficient to use an index.
You can request a get on an index which will return you a single record. Even if there are multiple records for the search value, you will always get the record with the lowest key value. The following is an example of doing a get request on an index:
var dbIndex = dbObjectStore.index("EmpName");
var dbGetRequest = dbIndex.get("Sam Smith");
dbGetRequest.onsuccess = function (evt) {
var objEmployee = evt.target.result;
/* do something with the result */
}
If you want to take advantage of the better performance of indexes but still need to loop over multiple records, you have an option of two different types of cursors.
The standard index cursor behaves the same as a normal cursor, where the value returned is the whole object from the object store. The following is an example of how you obtain a standard index cursor:
var dbCursorRequest = dbIndex.openCursor();
dbCursorRequest.onsuccess = function (evt) {
var curCursor = evt.target.result;
if (curCursor) {
/* Grab the current employee object from the cursor's value property (we could also grab the key). Do something with the object */
var objEmployee = curCursor.value;
// Cause onsuccess to fire again with the next item
curCursor.continue();
} // End if
}
The other type of cursor that's possible on an index is a key cursor where the cursor's key is the index value and the cursor's value is the object store's key. For example:
var dbCursorRequest = dbIndex.openKeyCursor();
dbCursorRequest.onsuccess = function (evt) {
var curCursor = evt.target.result;
if (curCursor) {
/* curCursor.key will hold a value like "Sam Smith"
curCursor.value will hold the record's key (e.g. "101" in our case) */
// Cause onsuccess to fire again with the next item
curCursor.continue();
} // End if
}
With both types of index cursors you can specify a key range filter like you can with normal cursors.
You can also specify the direction like you can with normal cursors but, in this case, you can make use of two additional values on indexes when those indexes are not specified as unique: "nextunique" and "prevunique" (filters the returned records to just unique items).
Deleting Data
To delete records from an object store there are a couple of options available. The first option is to use a delete request on the object store requesting that a specific record be deleted (bear in mind that the transaction shown in the examples below need to be set to readwrite):
var dbDeleteRequest = dbObjectStore.delete("101");
dbDeleteRequest.onsuccess = function(evt){ /* record is gone */ }
dbDeleteRequest.onerror = function(evt){}
The other option is to use the clear request on the object store requesting that all records within the object store be deleted along with any index records that reference the object store:
var dbClearRequest = dbObjectStore.clear();
dbClearRequest.onsuccess = function(evt){ /* records are gone */ }
dbClearRequest.onerror = function(evt){}
Handling Errors
Most documentation you will find online about the IDBRequest onerror event handlers will tell you that you need to access the errorCode value. This is no longer the case. The event's target value now holds an error object, which is a DOMError object. Rather than errorCode you now have access to the name property of the error object as in the following example:
dbRequest.onerror = function (evt) {
alert("Error: " + evt.target.error.name);
}
Aside from the onerror handlers of the IDBRequest object, it's also very important to wrap your database code in a try/catch statement since several actions will throw exceptions even if you have set up error event handlers.
For example, you will receive a ConstraintError exception if you try to create the same index twice which gets by the onerror handler and shows up in the console window (if your user is using IE and has IE set up to show errors, the user will receive a prompt with the error).
The W3C specification lists some common exceptions, if you're interested in knowing what could trigger one: http://www.w3.org/TR/IndexedDB/#exceptions
Closing a Database Connection
A database can be closed in several ways, including by the garbage collector or if the user navigates away from your page.
You can explicitly close a database connection by calling the close method on the IDBDatabase object as in the following example:
/* g_db is a global object holding the evt.target.result value (our IDBDatabase object) from the onsuccess event handler when we called window.indexedDB.open */
g_db.close();
{{ parent.title || parent.header.title}}
{{ parent.tldr }}
{{ parent.linkDescription }}
{{ parent.urlSource.name }}