Skip to main content Accessibility Feedback

Creating and reading database stores in indexeddb

Yesterday, we looked at what indexedDB is, and how to create databases. Today, we’re going to learn how to create stores (similar to tabs in SQL or collections in MongoDB).

Let’s dig in!

Creating a database store

Now that we have a database, we need to create a store to add our wizards to.

The indexedDB.open() method has an additional event: onupgradeneeded. This event fires whenever a database is created for the first time or when the version number changes.

Inside the onupgradeneeded callback function, we can access the oldVersion number on the event object. If it’s less than the current version, we can run any required code: in this case, creating a store.

To trigger it to run, let’s bump our version number from 1 to 2.

// Open a database
let openDB = indexedDB.open('spellbook', 2);

// If the version has increased or there's no existing DB
openDB.onupgradeneeded = function (event) {

	// Get the database and previous version number
	let db = openDB.result;
	let oldVer = event.oldVersion;

	// If there's no wizards store, create it
	if (oldVer < 2) {
		// Create the store...
	}

};

To create a new store, we can use the IDBDatabase.createObjectStore() method on our database.

The method requires just one argument: the name of the store. Let’s call ours wizards.

openDB.onupgradeneeded = function (event) {

	// Get the database and previous version number
	let db = openDB.result;
	let oldVer = event.oldVersion;

	// If there's no wizards store, create it
	if (oldVer < 2) {
		db.createObjectStore('wizards');
	}

};

If we use the IDBDatabase.createObjectStore() method with just one argument, we will need to specify a unique key for each item with add to the database.

There’s an optional second argument, an object of options, that you can provide to automate the process instead.

If the value of each item in your database is going to be an object, the keyPath property lets you specify a property in that object to use as the key for the item.

For example, imagine if each wizard in the database looked like this.

let merlin = {
	name: 'Merlin',
	spells: ['Summon Owl', 'Dancing Teacups']
};

If we use {keyPath: 'name'} as our options, this entry would have a key of Merlin in the database, since that’s the name property in our object.

openDB.onupgradeneeded = function (event) {

	// Get the database and previous version number
	let db = openDB.result;
	let oldVer = event.oldVersion;

	// If there's no wizards store, create it
	if (oldVer < 2) {
		db.createObjectStore('wizards', {keyPath: 'name'});
	}

};

Alternatively, you can pass in {autoIncrement: true} to have a random incremental key automatically generated by the database.

Transactions

The indexedDB API uses a concept called transactions to group a collection of operations or tasks together.

The operations don’t complete and write changes to the database until all of them are successful. This helps prevent data loss.

For example, imagine if I had a database for a bank. I wanted to take money out of John’s account, and move it into Sally’s. After the task to remove the money from John’s account finishes, but before it’s moved into Sally’s account, the power goes out. What happens to the money?

Transactions in the indexedDB API prevent the money from being lost in a situation like this. The removal of money from John’s account isn’t saved until it’s in Sally’s account, and the money added to Sally’s account isn’t saved until it’s removed from John’s account.

Every task in a transaction succeeds, or none of them do.

Creating a transaction

Any time you want to get, set, or delete data from a database, you need to create a transaction.

To create a transaction, use the IDBDatabase.transaction() method. It accepts two arguments: the storenames the transaction is for (as either a string or an array of strings), and the mode.

If you’ll only be reading data, use readonly for mode. If you’ll be saving data, use readwrite instead.

// Create a transaction as a read/write operation
let tx = db.transaction('wizards', 'readwrite');

Next, we need to get the store we’ll be reading or writing our data to using the IDBTransaction.objectStore() method. Pass in the store name as an argument.

The store, and any tasks you run on it, are now associated with the transaction (tx).

// Create a transaction as a read/write operation
let tx = db.transaction('wizards', 'readwrite');

// Get the store for this transaction
let store = tx.objectStore('wizards');

If you’re only using a single store for this transaction, you can combine these into a single line. For transactions involving multiple stores, the transaction should be saved to its own variable.

// For single-store transactions, these can be combined
let store = db.transaction('wizards', 'readwrite').objectStore('wizards');

Transactions automatically commit

In the indexedDB API, transactions autocommit.

When all of the tasks associated with a transaction are done, the transaction automatically completes and commits those changes.

Because this happens automatically, you can’t run asynchronous code in the middle of a transaction (such as fetching data from an API). If you do, the transaction will commit and end before you get a response from your API.

// For single-store transactions, these can be combined
let store = db.transaction('wizards', 'readwrite').objectStore('wizards');

// Get data to add to the database from an API
fetch('https://jsonplaceholder.typicode.com/todos').then(function (response) {
	return response.json();
}).then(function (data) {

	// If you try to write data to the store here, it will fail
	// The transaction associated with the store will have already committed and closed

});

If you need to get data asynchronously to write to your database, fetch it first, then create a transaction and use it.

// Get data to add to the database from an API
fetch('https://jsonplaceholder.typicode.com/todos').then(function (response) {
	return response.json();
}).then(function (data) {

	// This WILL work, since the data is already available when you create your transaction

	// Create a transaction as a read/write operation
	let tx = db.transaction('wizards', 'readwrite');

	// Get the store for this transaction
	let store = tx.objectStore('wizards');

});

Adding data to a database store

Now that we have a database and a store, we’re ready to add some data to it.

In a real app, you can add data to your database in all sorts of ways: from user inputs, API calls, and more. For this lesson, let’s add an array of wizards when the database successfully opens.

Inside the onsuccess callback function, we’ll pass db, the database, into an addWizards() function.

// If the database was successfully opened
openDB.onsuccess = function () {

	// Get the database
	let db = openDB.result;

	// Add wizards to the database
	addWizards(db);

};

Inside the addWizards() function, we have an array of wizards. Each one has a name and an array of spells that they know.

We want to add an entry in the wizards store for each wizard in the array.

// Add wizards to the database
function addWizards (db) {

	// Wizard data
	let wizards = [
		{
			name: 'Merlin',
			spells: ['Summon', 'Dancing Teacups']
		},
		{
			name: 'Gandalf',
			spells: ['Vanish', 'Flood', 'Light', 'Fire']
		},
		{
			name: 'Radagast',
			spells: ['Summon', 'Talk to Animals']
		}
	];

}

Next, we’ll create a transaction and get the wizards store.

// Create a transaction and get the store
let store = db.transaction('wizards', 'readwrite').objectStore('wizards');

Now, we can loop through each wizard in the wizards array and add it to the database with the store.add() method.

Pass in the item to add as an argument. If your store has a keyPath or uses autoIncrement, it will create a key automatically. If not, you can pass one in as a second argument.

// Add each wizard to the database
for (let wizard of wizards) {

	// Add the wizard
	let request = store.add(wizard);

}

This is an asynchronous action.

Once again, we can use onsuccess and onerror events to run callback functions when our request succeeds or fails.

// Add each wizard to the database
for (let wizard of wizards) {

	// Add the wizard
	let request = store.add(wizard);

	// Log the success
	request.onsuccess = function () {
		console.log('Wizard added:', request.result);
	};

	// Log the error
	request.onerror = function () {
		console.warn(request.error);
	};

}

The IDBObjectStore.add() method adds an item to the database if it doesn’t already exist. If an item is already in the database, it will fail and throw an error instead.

If you tried to run the code above more than once, for example, this warning would log to the console.

// DOMException: Key already exists in the object store.

Updating data in a store

The IDBObjectStore.put() method adds an item to the database if it doesn’t already exist, and updates an existing item if it does.

The openDB.onsuccess callback function, let’s run a function called updateWizards(), and pass the db in as an argument.

// If the database was successfully opened
openDB.onsuccess = function () {

	// Get the database
	let db = openDB.result;

	// Update wizards in the database
	updateWizards(db);

};

Inside the updateWizards() function, we’ll create an updated object for wizards in the database.

Next, we’ll create a new transaction and get the wizards store. Then, we’ll loop through each one and use the store.put() method to update the entry in the database.

Because Merlin already exists, it will be updated in the database. Jafar is new, and will be added.

// Update wizards in the database
function updateWizards (db) {

	// Wizard data
	let wizards = [
		{
			name: 'Merlin',
			spells: ['Summon', 'Dancing Teacups', 'Heal']
		},
		{
			name: 'Jafar',
			spells: ['Hypnosis']
		}
	];

	// Create a transaction and get the store
	let store = db.transaction('wizards', 'readwrite').objectStore('wizards');

	// Update each wizard to the database
	for (let wizard of wizards) {

		// Update the wizard
		let request = store.put(wizard);

	}

}

Once again, we can use onsuccess and onerror events to run callback functions when our request succeeds or fails.

// Update each wizard to the database
for (let wizard of wizards) {

	// Update the wizard
	let request = store.put(wizard);

	// Log the success
	request.onsuccess = function () {
		console.log('Wizard updated:', request.result);
	};

	// Log the error
	request.onerror = function () {
		console.warn(request.error);
	};

}

Deleting data from a store

The IDBObjectStore.delete() method deletes an item from a store. Pass in the key of the item to delete as an argument.

Inside the openDB.onsuccess event, let’s run a deleteWizard() function and pass in the db as an argument.

// If the database was successfully opened
openDB.onsuccess = function () {

	// Get the database
	let db = openDB.result;

	// Delete a wizard from the database
	deleteWizard(db);

};

In the deleteWizard() function, we’ll first create a new transaction, and get the wizards store.

Next, we’ll use the store.delete() method to delete Gandalf from the database. As always, we can attach onsuccess and onerror events to the request to run callback functions if the request succeeds or fails.

// Delete a wizard from the database
function deleteWizard (db) {

	// Create a transaction and get the store
	let store = db.transaction('wizards', 'readwrite').objectStore('wizards');

	// Update the wizard
	let request = store.delete('Gandalf');

	// Log the success
	request.onsuccess = function () {
		console.log('Wizard deleted:', request.result);
	};

	// Log the error
	request.onerror = function () {
		console.warn(request.error);
	};

}

Digging deeper into indexedDB

If you want to dig into more advanced topics than we covered in this series, I have a guide and ebook on this topic that you might enjoy.