A guide to IndexDB API

What is IndexedDB?

IndexedDB is a large-scale, NoSQL storage system. It lets you store just about anything in the user’s browser. In addition to the usual search, get, and put actions, IndexedDB also supports transactions.

IndexedDB is a low-level API for client-side storage of significant amounts of structured data, including files/blobs. This API uses indexes to enable high-performance searches of this data.

MDN Definition

Features:

  • IndexedDB databases store key-value pairs.
  • IndexedDB is built on a transactional database model. Everything you do in IndexedDB always happens in the context of a transaction
  • The IndexedDB API is mostly asynchronous. The API doesn’t give you data by returning values; instead, you have to pass a callback function.
  • IndexedDB uses DOM events to notify you when results are available. 
  • IndexedDB is object-oriented. IndexedDB is not a relational database with tables representing collections of rows and columns. 
  • IndexedDB does not use Structured Query Language (SQL). It uses queries on an index that produces a cursor, which you use to iterate across the result set.
  • IndexedDB adheres to a same-origin policy.

IndexedDB term:

  • Database – This is the highest level of IndexedDB. It contains the object stores, which in turn contain the data you would like to persist. You can create multiple databases with whatever names you choose, but generally, there is one database per app.
  • Object store – An object store is an individual bucket to store data. You can think of object stores as being similar to tables in traditional relational databases.
  • Index – An Index is a kind of object store for organizing data in another object store (called the reference object store) by an individual property of the data. The index is used to retrieve records in the object store by this property.
  • Operation – An interaction with the database.
  • Transaction – A transaction is a wrapper around an operation, or group of operations, that ensures database integrity. If one of the actions within a transaction fails, none of them are applied and the database returns to the state it was in before the transaction began.
  • Cursor – A mechanism for iterating over multiple records in the database.

Basic pattern

The basic pattern that IndexedDB encourages is the following:

  1. Open a database.
  2. Create an object store in the database. 
  3. Start a transaction and make a request to do some database operation, like adding or retrieving data.
  4. Wait for the operation to complete by listening to the right kind of DOM event.
  5. Do something with the results (which can be found on the request object).

Start with IndexDB:

#1. Check it is supported or not:

if (!('indexedDB' in window)) {
  console.log('This browser doesn't support IndexedDB');
  return;
}

#2. Open Database:

// Let us open our database syntax
var request = window.indexedDB.open(name, version);

name: The name of the database.

version (Optional): The version to open the database with.
If the version is not provided and the database exists, then a connection to the database will be opened without changing its version.
If the version is not provided and the database does not exist, then it will be created with version 1.

The open request doesn’t open the database or start the transaction right away. The call to the open() function returns an IDBOpenDBRequest object with a result (success) or error value that you handle as an event.

onerror: If there was any problem, an error event as its argument.
onsucess: If everything succeeds, a success event as its argument.
onupgradeneeded: If the database doesn’t already exist, or f the database does exist but you are specifying an upgraded version number, an onupgradeneeded event is triggered.

request.onerror = function(event) {
  // Do something with request.errorCode!
};
request.onsuccess = function(event) {
  // Do something with request.result!
};

#3. Creating or updating the version of the database:

When you create a new database or increase the version number of an existing database the onupgradeneeded the event will be triggered, you should create the object stores needed for this version of the database.

request.onupgradeneeded = function(event) { 
  // Save the IDBDatabase interface 
  var db = event.target.result;

  // Create an objectStore for this database
  var objectStore = db.createObjectStore("name", { keyPath: "myKey" });
};

You only need to create any new object stores, or delete object stores from the previous version that are no longer needed.

When you define object stores, you can define how data is uniquely identified in the store using the primary key. You can define a primary key by either defining a key path or by using a key generator.

key path is a property that always exists and contains a unique value. For example, in the case of a “myKey”
You could also use a key generator, such as autoIncrement. The key generator creates a unique value for every object added to the object store. By default, if we don’t specify a key, IndexedDB creates a key and stores it separately from the data.

Trying to create an object store with a name that already exists (or trying to delete an object store with a name that does not already exist) will throw an error.

To avoid any error of deleting or creating existing object store, you can first check that object sore exist or not.

request.onupgradeneeded = function(event) { 
  // Save the IDBDatabase interface 
  var db = event.target.result;
  // Create an objectStore for this database
   if (!db.objectStoreNames.contains('name')) { // if there's no "books" store
    db.createObjectStore('name', {keyPath: 'myKey'}); // create it
  }
}; 

To delete a object store

db.deleteObjectStore('name')

If the onupgradeneeded event exits successfully, the onsuccess handler of the open database request will then be triggered. 

Let see example all above event handler:

const dbName = "the_name";
var request = indexedDB.open(dbName, 2);
request.onerror = function (event) {
  // Handle errors.
};
request.onupgradeneeded = function (event) {
  var db = event.target.result;

  // Create an objectStore to hold information about our customers. We're
  // going to use "ssn" as our key path because it's guaranteed to be unique 
  var objectStore = db.createObjectStore("customers", { keyPath: "ssn" });

  // Create an index to search customers by name. We may have duplicates
  // so we can't use a unique index.
  objectStore.createIndex("name", "name", { unique: false });

  // Create an index to search customers by email. We want to ensure that
  // no two customers have the same email, so use a unique index.
  objectStore.createIndex("email", "email", { unique: true });

  // Use transaction oncomplete to make sure the objectStore creation is
  // finished before adding data into it.
  objectStore.transaction.oncomplete = function (event) {
    // This is what our customer data looks like.
    const customerData = [
      { ssn: "444-44-4444", name: "Bill", age: 35, email: "bill@company.com" },
      { ssn: "555-55-5555", name: "Donna", age: 32, email: "donna@home.org" },
    ];
    // Store values in the newly created objectStore.
    var customerObjectStore = db
      .transaction("customers", "readwrite")
      .objectStore("customers");
    customerData.forEach(function (customer) {
      customerObjectStore.add(customer);
    });
  };
};

Adding, retrieving, and removing data:

Before you can do anything with our new database, we need to start a transaction.

Transaction
db.transaction(storeNames, mode);

storeNames: The names of object stores that are in the scope of the new transaction, declared as an array of strings. Specify only the object stores that you need to access.
If you need to access only one object store, you can specify its name as a string.

db.db.transaction('customer');
db.transaction(['customer']);

mode (Optional): The types of access that can be performed in the transaction. Transactions are opened in one of the modes: readonlyreadwrite.

There is one more mode versionchange which we cannot specified manually. This called by indexDB automatically when open the database

Transactions have three available modes: readonlyreadwrite, and versionchange.

Transaction mode

#1. Adding to database:

let transaction = db.transaction(["customers"], "readwrite"); 
OR
let transaction = db.transaction("customers", "readwrite"); 


// Do something when all the data is added to the database.
transaction.oncomplete = function(event) {
  console.log("All done!");
};
transaction.onerror = function(event) {
  // Don't forget to handle errors!
};

// get an object store to operate on it
let customers= transaction.objectStore("customers"); 

let customer=   { ssn: "444-44-4444", name: "Bill", age: 35, email: "bill@company.com" },

// add the customer add handle error, sucess event of adding customer
let request = customers.add(customer); 
request.onsuccess = function() { 
  console.log("Book added to the store", request.result);
};

request.onerror = function() {
  console.log("Error", request.error);
};

The transaction() function takes two arguments (though one is optional) and returns a transaction object. The first argument is a list of object stores that the transaction will span. 

Transactions can receive DOM events of three different types:  errorabort, and complete.

error:  Transaction receives error events from any requests that are generated from it and the default behavior of an error is to abort the transaction in which it occurred. Unless you handle the error by first calling stopPropagation() on the error event then doing something else, the entire transaction is rolled back. 

abort: If you don’t handle an error event or if you call abort() on the transaction, then the transaction is rolled back and an abort event is fired on the transaction.

complete: After all pending requests have completed, you’ll get a complete event.

Now that you have a transaction (line 1), you’ll need to get the object store from it (line 15). Transactions only let you have an object store that you specified when creating the transaction. Then you can add all the data you need (line 20).

Object stores support two methods to store a value:

  • put(value, [key]) Add the value to the store. The key is supplied only if the object store did not have keyPath or autoIncrement option. If there’s already a value with the same key, it will be replaced.
  • add(value, [key]) Same as put, but if there’s already a value with the same key, then the request fails, and an error with the name "ConstraintError" is generated.

#2. Removing data from the database:

var transaction= db.transaction(["customers"], "readwrite")
let customer = transaction.objectStore("customers")
let request = customer.delete("444-44-4444"); 
request.onsuccess = function(event) {
  // It's gone!
};

#3. Getting data from the database:

Now that the database has some info in it, you can retrieve it in several ways. First, the simple get(). You need to provide the key to retrieve the value.

var transaction = db.transaction(["customers"]);
var objectStore = transaction.objectStore("customers");
var request = objectStore.get("444-44-4444");
request.onerror = function(event) {
  // Handle errors!
};
request.onsuccess = function(event) {
  // Do something with the request.result!
  console.log("Name for SSN 444-44-4444 is " + request.result.name);
};

#4. Updating an entry in the database:

// Here we had to specify a readwrite transaction because we want to write to the database, not just read from it.
var transacton = db.transaction(["customers"], "readwrite")
let objectStore  = transacton.objectStore("customers");
var request = objectStore.get("444-44-4444");
request.onerror = function(event) {
  // Handle errors!
};
request.onsuccess = function(event) {
  // Get the old value that we want to update
  var data = event.target.result;
  
  // update the value(s) in the object that you want to change
  data.age = 42;

  // Put this updated object back into the database.
  var requestUpdate = objectStore.put(data);
   requestUpdate.onerror = function(event) {
     // Do something with the error
   };
   requestUpdate.onsuccess = function(event) {
     // Success - the data is updated!
   };
};

So here we’re creating an objectStore and requesting a customer record out of it, identified by its ssn value (444-44-4444). We then put the result of that request in a variable (data), update the age property of this object, then create a second request (requestUpdate) to put the customer record back into the objectStore, overwriting the previous value.

#5. Using a cursor:

Using get() requires that you know which key you want to retrieve. The cursor does not require us to select the data based on a key; we can just grab all of it.

// Syntax
var request = ObjectStore.openCursor();
var request = ObjectStore.openCursor(query);
var request = ObjectStore.openCursor(query, direction);

query (Optional): A key or key range to be queried. If a single valid key is passed, this will default to a range containing only that key. If nothing is passed, this will default to a key range that selects all the records in this object store.

direction (Optional): It tells the cursor what direction to travel. Valid values are "next""nextunique""prev", and "prevunique". The default is "next".

var objectStore = db.transaction("customers").objectStore("customers");
objectStore.openCursor().onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    console.log("Name for SSN " + cursor.key + " is " + cursor.value.name);
    cursor.continue();
  }
  else {
    console.log("No more entries!");
  }
};

#6. Using Index:

We can retrieve records in an object store through the primary key or by using an index. An index lets you look up records in an object store using properties of the values in the object stores records other than the primary key.

//Syntax
var myIDBIndex = objectStore.createIndex(indexName, keyPath);
var myIDBIndex = objectStore.createIndex(indexName, keyPath, objectParameters);

indexName: The name of the index to create. Note that it is possible to create an index with an empty name.

keyPath: The key path for the index to use. Note that it is possible to create an index with an empty keyPath, and also to pass in a sequence (array) as a keyPath.

objectParameters (optional): An object, which can include the following properties:
unique: If true, the index will not allow duplicate values for a single key.
multiEntry: If true, the index will add an entry in the index for each array element when the keyPath resolves to an Array. If false, it will add one single entry containing the Array.

Indexes must be made when you create your object stores. Means it must be done in upgradeneeded

var request = indexedDB.open('CustomersDB', 2);
request.onerror = function (event) {
  // Handle errors.
};
request.onupgradeneeded = function (event) {
  var db = event.target.result;

  // Create an objectStore to hold information about our customers. We're
  // going to use "ssn" as our key path because it's guaranteed to be unique 
  var objectStore = db.createObjectStore("customers", { keyPath: "ssn" });

  // Create an index to search customers by name. We may have duplicates
  // so we can't use a unique index.
  objectStore.createIndex("name", "name", { unique: false });

  // Create an index to search customers by email. We want to ensure that
  // no two customers have the same email, so use a unique index.
  objectStore.createIndex("email", "email", { unique: true });
}

Storing customer data using the SSN as a key is logical since the SSN uniquely identifies an individual. If you need to look up a customer by name, however, you’ll need to iterate over every SSN in the database until you find the right one.
Searching in this fashion would be very slow, so instead you can use an index.

// First, make sure you created index in request.onupgradeneeded:
// objectStore.createIndex("name", "name");
// Otherwize you will get DOMException.

var index = objectStore.index("name");

index.get("Donna").onsuccess = function(event) {
  console.log("Donna's SSN is " + event.target.result.ssn);
};

If you need to access all the entries with a given name you can use a cursor. You can open two different types of cursors on indexes. openCursor() and openKeyCursor()

// Using a normal cursor to grab whole customer record objects
index.openCursor().onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    // cursor.key is a name, like "Bill", and cursor.value is the whole object.
    console.log("Name: " + cursor.key + ", SSN: " + cursor.value.ssn + ", email: " + cursor.value.email);
    cursor.continue();
  }
};

// Using a key cursor to grab customer record object keys
index.openKeyCursor().onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    // cursor.key is a name, like "Bill", and cursor.value is the SSN.
    // No way to directly get the rest of the stored object.
    console.log("Name: " + cursor.key + ", SSN: " + cursor.primaryKey);
    cursor.continue();
  }
};

#7. Working with ranges

Using keys or a range of keys, we can limit the range using lower and upper bounds. For example, you can iterate over all values of a key in the value range A–Z. A key range can be a single value or a range with upper and lower bounds or endpoints. If the key range has both upper and lower bounds, then it is bounded; if it has no bounds, it is unbounded

RangeCode
All keys ≥ xIDBKeyRange.lowerBound(x)
All keys > xIDBKeyRange.lowerBound(x, true)
All keys ≤ yIDBKeyRange.upperBound(y)
All keys < yIDBKeyRange.upperBound(y, true)
All keys ≥ x && ≤ yIDBKeyRange.bound(xy)
All keys > x &&< yIDBKeyRange.bound(xy, true, true)
All keys > x && ≤ yIDBKeyRange.bound(xy, true, false)
All keys ≥ x &&< yIDBKeyRange.bound(xy, false, true)
The key = zIDBKeyRange.only(z)

#8. Specifying the range and direction of cursors

If you would like to limit the range of values you see in a cursor, you can use an IDBKeyRange object and pass it as the first argument to openCursor() or openKeyCursor()

// Only match "Donna"
var singleKeyRange = IDBKeyRange.only("Donna");

// Match anything past "Bill", including "Bill"
var lowerBoundKeyRange = IDBKeyRange.lowerBound("Bill");

// Match anything past "Bill", but don't include "Bill"
var lowerBoundOpenKeyRange = IDBKeyRange.lowerBound("Bill", true);

// Match anything up to, but not including, "Donna"
var upperBoundOpenKeyRange = IDBKeyRange.upperBound("Donna", true);

// Match anything between "Bill" and "Donna", but not including "Donna"
var boundKeyRange = IDBKeyRange.bound("Bill", "Donna", false, true);

// To use one of the key ranges, pass it in as the first argument of openCursor()/openKeyCursor()
index.openCursor(boundKeyRange).onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    // Do something with the matches.
    cursor.continue();
  }
};

Sometimes you may want to iterate in descending order rather than in ascending order (the default direction for all cursors). Switching direction is accomplished by passing prev to the openCursor() function as the second argument:

objectStore.openCursor(boundKeyRange, "prev").onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    // Do something with the entries.
    cursor.continue();
  }
};

If you just want to specify a change of direction but not constrain the results shown, you can just pass in null as the first argument:

objectStore.openCursor(null, "prev").onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    // Do something with the entries.
    cursor.continue();
  }
};

If you wish to filter out duplicates during cursor iteration over indexes, you can pass nextunique (or prevunique if you’re going backwards) as the direction parameter. When nextunique or prevunique is used, the entry with the lowest key is always the one returned.

index.openKeyCursor(null, "nextunique").onsuccess = function(event) {
  var cursor = event.target.result;
  if (cursor) {
    // Do something with the entries.
    cursor.continue();
  }
};

Version changes while a web app is open in another tab:

When your web app changes in such a way that a version change is required for your database, you need to consider what happens if the user has the old version of your app open in one tab and then loads the new version of your app in another tab.

When you call open() with a greater version than the actual version of the database, all other open databases must explicitly acknowledge the request before you can start making changes to the database (an onblocked event is fired until they are closed or reloaded). Here’s how it works:

var openReq = mozIndexedDB.open("MyTestDatabase", 2);

openReq.onblocked = function(event) {
  // this event shouldn't trigger if we handle onversionchange correctly
  // it means that there's another open connection to same database
  // and it wasn't closed after db.onversionchange triggered for them
  // If some other tab is loaded with the database, then it needs to be closed
  // before we can proceed.
  console.log("Please close all other tabs with this site open!");
};
  
openReq.onupgradeneeded = function(event) {
  // All other databases have been closed. Set everything up.
  db.createObjectStore(/* ... */);
  useDatabase(db);
};
  
openReq.onsuccess = function(event) {
  var db = event.target.result;
  useDatabase(db);
  return;
};

function useDatabase(db) {
  // Make sure to add a handler to be notified if another page requests a version
  // change. We must close the database. This allows the other page to upgrade the database.
  // If you don't do this then the upgrade won't happen until the user closes the tab.
  db.onversionchange = function(event) {
    db.close();
    console.log("A new version of this page is ready. Please reload or close this tab!");
  };

  // Do stuff with the database.
}

Reference:

Leave a Reply