# Indexed DB API
The IndexedDB API
gives us the ability to create local document databases that are accessible with JavaScript through either a script that is attached to an HTML file or through a Service Worker.
# IndexedDB Libraries
There are a number of libraries that have been created to allow us to manage and use IndexedDB databases. Too many to cover here in fact.
However, if you only want to use your IndexedDB database like you have used LocalStorage or SessionStorage then a good choice is Jake Archibald's idb-key library (opens new window).
It gives us a small number of methods and works with Promises so we can chain methods with then()
methods. It treats all the stores like key-value pairs like LocalStorage does.
Watch this video for a complete overview.
Here are some of the main methods for idb-keyval
:
set('mykey', 'my value').then(() => {
console.log('Success');
});
get('mykey').then((val) => {
console.log('The value for mykey is', val);
});
setMany([
['key1', 'key2'],
['hello', 'world'],
]).then(() => {
console.log('success');
});
getMany(['key1', 'key2']).then((values) => {
//values is an array of values for those keys
});
delete 'key1'.then(() => {
console.log('success deleting');
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Vanilla JS and the IndexedDB API
Once we move beyond simple key value pairs then it's better to switch libraries or write our own scripts with the built-in IndexedDB API
.
Here is the first of the 8 videos in the playlist about IndexedDB.
Full IndexedDB Playlist (opens new window)
# Core Concepts
When writing your own code to work with IndexedDB, it is not any harder than using a library, it just means that you have to add your own functions for handling the success, complete, and error events.
MDN IndexedDB Reference section (opens new window)
# Structure
An IndexedDB is a document database. That means that the data tends to be in the form of Object
s or Arrays
. You can still store simple primitives like String
s, Number
s, and Boolean
s. With IndexedDB you can also store Binary data like images or other files.
The database
s are bound to an origin (domain, protocol, and port).
You are allowed to have multiple database
s per origin.
Inside a database you can have one or more stores
.
Each store
is a collection of Object
s.
# Opening and Updating Databases & Stores
The methods used for communicating with the databases and stores have events that can be accessed through callback functions.
To use a database you need to call the indexedDB.open(name, ver)
method. It accepts a name String
and a version Number
as arguments. The method returns an IDBOpenDBRequest
object. This object has three events - success
, error
and upgradeneeded
.
The success
event fires when the database has successfully finished opening. This is when you would be able to save a global reference to the database in a global variable.
The error
event fires when there is ANY problem opening the database.
The upgradeneeded
event fires when you call the indexedDB.open()
method with a different version number than what exists. This includes the first time you open the database. This is the ONLY place where you are allowed to create stores
, delete stores
, or create indexes
. Basically, any time you want to change the structure of your database, this event handler function is the place to do it.
# Transactions and Requests
Any time you want to work with the data in your stores
- Create, Read, Update, or Delete - you do this through a Request
. Every Request
or group of Request
s must to be wrapped inside a Transaction
.
A transaction
is create by calling the db.transaction(store(s), mode)
method on your database object. It takes, as its first argument, either a store name String
or an Array of store name String
s. The second argument is the purpose of the transaction. It must be either readonly
or readwrite
.
The transaction
object returned from the db.transaction()
method has two events - error
and complete
. The error
event is pretty obvious. The complete
event means that ALL the request
s inside the transaction
have completed successfully. This is usually a good place to put your code that moves on to the next step in your business logic.
The IDBRequest
objects inside every transaction have two events - error
and success
. Again, the error
is pretty self explanatory. The success
event handler function is where you would get access to the data that was retrieved through a read operation. For create, update, and delete operations you can use the success
event to track your progress through the transaction and to inform the user.
# CRUD Methods
CRUD is a common acronym used with databases. It refers to the four main actions that you can do with any database - Create, Read, Update, and Delete. In Relational Databases, you would use the SQL
Structured Query Language, and call the commands insert
, select
, update
and delete
.
With IndexedDB we have the following commands.
//CREATE
store.add(key, obj); //insert an object. Fail if the key exists
//READ
store.get(key); //get the first object where the key matches
store.getAll(key | keyrange); //get all the objects that match
//UPDATE
store.put(key, obj); //update. Overwrite if key exists. Insert if not.
//DELETE
store.delete(key | keyrange); //delete an object(s)
2
3
4
5
6
7
8
9
Notice how each of these commands has a variable store
in front?
Each time you want to run one of these commands you need to create a transaction, then get a reference to a specific store
and then you can call the specific CRUD method.
Every one of those methods will return a IDBRequest
object that represents the request to carry out the CRUD operation.
Here is a more complete example:
let tx = db.transaction('myStore', 'readwrite');
let store = tx.objectStore('myStore');
let req = store.get('someKey');
req.addEventListener('success', (ev) => {
//ev.target is the same as req
let result = ev.target.result;
//result is the object we requested from the store.
});
req.onerror = (err) => {
//an error happened to the request.
};
2
3
4
5
6
7
8
9
10
11
The example above shows both ways you can write the event listener functions. onerror
and onsuccess
or addEventListener('success', func)
and addEventListener('err', func)
. You can use either.
If the error
event fires, it will trigger the transaction
error
event.
If the success
event fires, it will trigger the transaction
complete
event.
If you call the get
method, then the ev.target.result
value will be the single object that had the matching key.
If you call the getAll
method, then the ev.target.result
object will be an Array
, regardless of the number of records returned.
If there are no matching keys for get
or getAll
the success
event still fires. This is not an error. This is an empty result set.
# KeyRanges
When you want to bring back one or more records that match a range of keys then you can define an IDBKeyRange
object to explain the possible range of values to match against.
Keys in a store
can be a Number
, an Array
, or a String
. When you pass the value of a key to the get
method it MUST be an exact match for the value and datatype. Think JavaScript ===
.
To create a key we can call one of the following methods.
IDBKeyRange.lowerBound(keyval, false);
IDBKeyRange.upperBound(keyval, false);
IDBKeyRange.bound(lowerKeyval, upperKeyval, false, false);
IDBKeyRange.only(keyval); //same as using a single key
2
3
4
The upperBound
and lowerBound
methods let you provide either a max or a min value for the keys to match.
The bound
method lets you provide a min and max value for matching keys.
The true and false values represent whether or not you want to omit the value(s) provided in the keyval arguments. false
means that you want to include the value.
KeyRange reference (opens new window)
# Indexes
An index
can be created whenever you update a database's structure.
Think of it as an extra column that is created with a sorted version of one of the properties. When you want to apply a IDBKeyRange
for a getAll
or update
or delete
you need to have things sorted based on the property you are comparing to the key range.
You can create an index for every property but that is wasteful. Only create indexes for the properties that you will use for a getAll
or update
or delete
.
To create an index we use this method inside our upgradeneeded
event handler function:
objectStore.createIndex('priceIDX', 'price', { unique: false });
The first argument is the label we are giving our new index column.
The second argument is the property that will/does exist in our data.
The third argument is our options object. We need to specify if the values of our property are required to be unique or not. In this case we are saying that the values in the price property are allowed to be duplicates.
Once you have an index, then you can use it in place of a store object for creating the IDBRequest
s in your transaction. Here is an example that creates a transaction
and then makes a request for all the objects from our store
that match a price between $50 and $100, inclusive. The example uses a IDBKeyRange
as well as the index created above.
let tx = db.transaction('myStore', 'readonly');
let store = tx.objectStore('myStore');
let idx = store.index('priceIDX');
let range = IDBKeyRange.bound(50, 100, false, false);
let req = idx.getAll(range);
req.onsuccess = (ev) => {
let result = ev.target.result;
result.forEach((obj) => {
console.log(obj); //console each of the objects that matched.
});
};
2
3
4
5
6
7
8
9
10
11
# Cursors
A cursor provides another way that we would move through the resulting recordset from a data request. It lets us provide a direction to move through the results - next
, nextunique
, prev
, or prevunique
.
It uses an openCursor(key|range, direction)
method to create the cursor. The cursor has a success
and an error
event. Each time the success
event fires it means that the next record is available. You will always get one record at a time. The success
event will fire one last time to say that the records have all be traversed.
The openCursor(key|range, direction)
method can be called on either a store
or an index
object.
idx.openCursor(range, 'next').onsuccess = (ev) => {
let cursor = ev.target.result;
if (cursor) {
console.log(cursor.value);
cursor.continue(); //call success again
} else {
//end of record set
}
};
2
3
4
5
6
7
8
9