Introduction
Quoting MDN:
IndexedDB is a transactional database system, similar to a SQL-based RDBMS. However, unlike SQL-based RDBMSs which use fixed-list tables, IndexedDB is a JavaScript-based object-oriented database. IndexedDB allows you to store and retrieve objects indexed with a key; any object supported by the structured clone algorithm can be stored. You only need to specify the database schema, open a connection to the database, and then retrieve and update data within a series of transactions.
IndexedDB is a low-level API for client-side storage of large amounts of structured data, including files/blobs. This API uses indexes to enable high-performance searches over this data. While Web Storage is useful for storing smaller amounts of data, it is less suitable for storing larger amounts of structured data. IndexedDB provides a solution.
Differences
Here’s a comparison I’ve compiled between Web Storage and IndexedDB. Please point out any inaccuracies.
Usage
1. Opening a Database
const DB_NAME = "Netease";
const DB_VERSION = 1;
const OB_NAMES = {
UseKeyPath: "UseKeyPath",
UseKeyGenerator: "UseKeyGenerator",
};
/**
* NOTE:
* 1. 第一次打开可能会提示用户获取 indexedDB 的权限
* 2. 浏览器隐身模式不会存在本地,只会存储在内存中
*/
const request = window.indexedDB.open(DB_NAME, DB_VERSION);
indexedDB.open
takes two parameters: the database name and version, and returns an IDBOpenDBRequest
object. You can listen for its success
and error
events, similar to DOM events, to get its result. Almost all asynchronous operations with IndexedDB follow this event-driven pattern, returning an IDBRequest
object that contains either a result or an error. In this case, the open
method’s result is an instance of IDBDatabase
.
The second parameter is the database version. The version determines the database’s schema: the object stores within it and their structures. When the database is opened for the first time via the open
method, an onupgradeneeded
event is triggered. We can, and only can, set the database schema here. If the database already exists and we open it with a higher version, the onupgradeneeded
event will also be triggered to update the database schema.
Adding Event Handlers
We can perform corresponding operations by listening to its success
, error
, and upgradeneeded
events.
request.onerror = function (event) {
// Do something with request.errorCode!
console.error("open request failed", event.target.error);
};
request.onsuccess = function (event) {
// Do something with request.result!
// console.log('open request success', event)
var db = event.target.result;
db.onerror = function (e) {
console.error("Database error: ", e.target.error);
};
db.onclose = (e) => {
console.error("Database close:", e.target.error);
};
};
The db
object, which is the main subject for subsequent operations, can be obtained within the success
event.
Error Handling
Since it’s based on a DOM event model, all errors will bubble up. This means that an error triggered on a specific request will bubble up sequentially to the transaction, and then to the db
object.
To simplify error handling, you can directly add error handling to the db
object:
db.onerror = function (e) {
// 可以处理这个数据库上所有的错误
console.error("Database error: ", e.target.error);
};
2. Creating or Updating Database Versions
As mentioned earlier, when creating or incrementing the database version, the onupgradeneeded
event is triggered. Inside this event, you can access the db
object to create or update object stores, as shown below.
request.onupgradeneeded = function (event) {
/**
* NOTE:
* 1. 创建新的 objectStore
* 2. 删除旧的不需要的 objectStore
* 3. 如果需要更新已有 objectStore 的结构,需要先删除原有的 objectStore ,然后重新创建
*/
// The IDBDatabase interface
console.log("onupgradeneeded", event);
var db = event.target.result; // Create an objectStore for this database
const objectStore = db.createObjectStore(OB_NAMES.UseKeyPath, {
keyPath: "time",
});
};
3. Building the Database
IndexedDB stores data in object stores rather than table structures; a database can contain any number of object stores. Whenever a value is stored in an object store, it must be associated with a key. There are several ways to provide keys, depending on whether the object store uses a key path or a key generator.
Let’s look at the differences between them using a table from MDN:
Key Path (keyPath) | Key Generator (autoIncrement) | Description |
---|---|---|
No | No | This object store can hold any kind of value, even primitive values like numbers and strings. You must supply a separate key argument whenever you want to add a new value. |
Yes | No | This object store can only hold JavaScript objects. The objects must have a property with the same name as the key path. |
No | Yes | This object store can hold any kind of value. The key is generated for you automatically, or you can supply a separate key argument if you want to use a specific key. |
Yes | Yes | This object store can only hold JavaScript objects. Usually a key is generated and the value of the generated key is stored in the object in a property with the same name as the key path. However, if such a property already exists, the value of that property is used as key rather than generating a new key. |
From https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Using_IndexedDB
When storing JavaScript objects rather than primitive types, we can also create indexes for the object store. This allows us to find specific objects by their indexed properties. Furthermore, indexes can, to some extent, constrain the objects to be stored. By setting a unique identifier when creating an index, you can ensure that no two objects with the same index value are stored. Let’s look at an example. Suppose we have the following log data to store:
const TestData = [
{
event: "NE-TEST1",
level: "warning",
errorCode: 200,
url: "http://www.example.com",
time: "2017/11/8 下午4:53:039",
isUploaded: false,
},
{
event: "NE-TEST2",
msg: "测试2",
level: "error",
errorCode: 1000,
url: "http://www.example.com",
time: "2017/11/8 下午4:53:042",
isUploaded: false,
},
{
event: "NE-TEST3",
msg: "测试3",
level: "info",
errorCode: 3000,
url: "http://www.example.com",
time: "2017/11/8 下午4:53:043",
isUploaded: false,
},
{
event: "NE-TEST4",
mgs: "测试4",
level: "info",
url: "http://www.example.com",
time: "2017/11/8 下午4:53:0423",
isUploaded: false,
},
];
There are two ways to store this, using a key path and a key generator, respectively.
function obUseKeypath(db) {
const objectStore = db.createObjectStore(OB_NAMES.UseKeyPath, {
keyPath: "time",
});
objectStore.createIndex("errorCode", "errorCode", { unique: false });
objectStore.createIndex("level", "level", { unique: false });
}
function obUseKeyGenerator(db) {
const objectStore = db.createObjectStore(OB_NAMES.UseKeyGenerator, {
autoIncrement: true,
});
objectStore.createIndex("errorCode", "errorCode", { unique: false });
objectStore.createIndex("time", "time", { unique: true });
objectStore.createIndex("level", "level", { unique: false });
}
4. CRUD Operations
To facilitate the introduction of this section, let’s wrap the code from the previous section for easier explanation in subsequent examples:
function openindexedDB() {
// The call to the open() function returns an IDBOpenDBRequest object with a result (success) or error value that you handle as an event.
return new Promise((resolve, reject) => {
/**
* NOTE:
* 1. 第一次打开可能会提示用户获取 indexedDB 的权限
* 2. 浏览器隐身模式不会存在本地,只会存储在内存中
*/
const request = window.indexedDB.open(DB_NAME, DB_VERSION);
request.onerror = function (event) {
// Do something with request.errorCode!
console.log("open request failed", event);
console.error(event.target.error);
};
request.onsuccess = function (event) {
// Do something with request.result!
// console.log('open request success', event)
var db = event.target.result;
db.onerror = function (e) {
console.error("Database error: ", e.target.error);
reject(e.target.error);
};
db.onclose = (e) => {
console.error("Database close:", e.target.error);
reject(e.target.error);
};
resolve(db);
};
request.onupgradeneeded = function (event) {
/**
* NOTE:
* 1. 创建新的 objectStore
* 2. 删除旧的不需要的 objectStore
* 3. 如果需要更新已有 objectStore 的结构,需要先删除原有的 objectStore ,然后重新创建
*/
// The IDBDatabase interface
console.log("onupgradeneeded", event);
var db = event.target.result; // Create an objectStore for this database
obUseKeypath(db);
obUseKeyGenerator(db);
/**
* NOTE:
* transaction
* 三个事件:
* 1. error
* 2. abort
* 3. complete
* 两个方法:
* 1. abort
* Rolls back all the changes to objects in the database associated with this transaction. If this transaction has been aborted or completed, then this method throws an error event.
* 2. objectStore
* Returns an IDBObjectStore object representing an object store that is part of the scope of this transaction.
*/
db.transaction.oncomplete = function (e) {
console.log("obj create success", e);
};
};
});
}
function obUseKeypath(db) {
const objectStore = db.createObjectStore(OB_NAMES.UseKeyPath, {
keyPath: "time",
});
objectStore.createIndex("errorCode", "errorCode", { unique: false });
objectStore.createIndex("level", "level", { unique: false });
}
function obUseKeyGenerator(db) {
const objectStore = db.createObjectStore(OB_NAMES.UseKeyGenerator, {
autoIncrement: true,
});
objectStore.createIndex("errorCode", "errorCode", { unique: false });
objectStore.createIndex("time", "time", { unique: true });
objectStore.createIndex("level", "level", { unique: false });
}
This way, each time we need to perform a database operation, we just need to call the openindexedDB
method.
Transactions
All database operations are built upon transactions, which have three modes: readonly
, readwrite
, and versionchange
.
To modify the database schema, you must be in versionchange
mode. Reading and modifying data correspond to the other two modes.
A transaction
is opened via IDBDatabase.transaction
, which accepts two parameters: storeNames
and mode
.
All database operations follow this flow:
- Get database object
- Open transaction on database
- Open object store on transaction
- Perform operation on object store
To accelerate transaction operations:
- When defining the scope, only define the necessary object stores. This allows multiple transactions to execute in parallel on non-overlapping scopes.
- Only open a
readwrite
transaction when needed. While multiplereadonly
transactions can execute concurrently on overlapping scopes, only onereadwrite
transaction can.
Add
The method is as follows:
- First, get the
db
object. - Then, open a
readwrite
transaction. - Obtain the object store object via the transaction.
- Execute the data addition operation.
/**
* 添加数据
* @param {array} docs 要添加数据
* @param {string} objName 仓库名称
*/
function addData(docs, objName) {
if (!(docs && docs.length)) {
throw new Error("docs must be a array!");
}
return openindexedDB().then((db) => {
const tx = db.transaction([objName], "readwrite");
tx.oncomplete = (e) => {
console.log("tx:addData onsuccess", e);
return Promise.resolve(docs);
};
tx.onerror = (e) => {
e.stopPropagation();
console.error("tx:addData onerror", e.target.error);
return Promise.reject(e.target.error);
};
tx.onabort = (e) => {
console.warn("tx:addData abort", e.target);
return Promise.reject(e.target.error);
};
const obj = tx.objectStore(objName);
docs.forEach((doc) => {
const req = obj.add(doc);
/**
* NOTE:
* request
* 两个事件:
* 1. success
* 2. error
*/ // req.onsuccess = e => console.log('obj:addData onsuccess', e.target)
req.onerror = (e) => {
console.error("obj:addData onerror", e.target.error);
};
});
});
}
If you want to save the TestData
using both key generator and key path methods simultaneously, then:
addData(TestData, OB_NAMES.UseKeyGenerator).then(() =>
addData(TestData, OB_NAMES.UseKeyPath),
);
Delete
The process is the same as adding:
/**
* 删除指定 key 的数据
* @param {string} objName 仓库名称
* @param {*} key 要删除数据的 primary key 值
*/
function deleteData(objName, key) {
return openindexedDB().then((db) => {
const tx = db.transaction([objName], "readwrite");
const obj = tx.objectStore(objName);
const req = obj.delete(key);
req.onsuccess = (e) => {
console.log(`readData success. key:${key},result:`, e.target.result);
return Promise.resolve(e.target.result);
};
req.onerror = (e) => {
console.error(`readData error. key:${key},error: ${e.target.errorCode}`);
return Promise.reject(e.target.error);
};
});
}
If you want to delete the value with key 1
from UserKeyGenerator
, then:
deleteData(OB_NAMES.UseKeyGenerator, 1).then((doc) => console.log(doc));
Retrieve
Method:
/**
* 读取给定 key 的数据
* @param {string} objName 仓库名称
* @param {*} key 要读取数据的 primary key 值
*/
function readData(objName, key) {
return openindexedDB().then((db) => {
const tx = db.transaction([objName]);
const obj = tx.objectStore(objName);
const req = obj.get(key);
req.onsuccess = (e) => {
console.log(`readData success. key:${key},result:`, e.target.result);
return Promise.resolve(e.target.result);
};
req.onerror = (e) => {
console.error(`readData error. key:${key},error: ${e.target.errorCode}`);
return Promise.reject(e.target.error);
};
});
}
Example:
readData(OB_NAMES.UseKeyGenerator, 1);
Update
Method:
/**
* 更新指定 key 的数据
* @param {string} objName 仓库名称
* @param {*} key 指定的 key
* @param {object} changes 要修改的属性值
*/
function updateData(objName, key, changes) {
return openindexedDB().then((db) => {
return new Promise((resolve, reject) => {
const tx = db.transaction([objName], "readwrite");
const obj = tx.objectStore(objName);
const req = obj.get(key);
req.onsuccess = (e) => {
let doc = e.target.result;
let newDoc = Object.assign(doc, changes);
const req = obj.put(newDoc);
req.onsuccess = (e) => {
console.log(`updateData success, newDoc:`, newDoc, e);
resolve(e.target.result);
};
req.onerror = (e) => {
resolve(e.target.result);
};
};
req.onerror = (e) => {
reject(e.target.error);
};
});
});
}
Example:
updateData(OB_NAMES.UseKeyGenerator, 1, { time: "123" })
.then((doc) => console.log(doc))
.catch(console.error);
Using Cursors
Using the get
method requires knowing a key beforehand. If you need to step through all values in an object store, you can use a cursor.
/**
* 通过 cursor 获取制定仓库下的所有数据
* @param {string} objName 仓库名称
* @param {function} [cb] 回调函数,对每次得到的 cursor 进行操作
* @returns {Promise.<array.<object>} 包含所有数据的数组
*/
function getAllByCursor(objName, cb) {
return openindexedDB().then((db) => {
const arr = [];
const tx = db.transaction([objName]);
const obj = tx.objectStore(objName);
return new Promise((resolve) => {
obj.openCursor().onsuccess = (e) => {
const cursor = e.target.result;
if (cursor) {
arr.push(cursor.value);
cb && cb(cursor);
cursor.continue();
} else {
return resolve(arr);
}
};
});
});
}
The openCursor()
method accepts multiple parameters.
- The first parameter can be a key range object to limit the range of values to fetch.
- The second parameter can set the iteration direction.
The result
value of the success callback function is the cursor itself. The current iterated value can be obtained via the cursor object’s key
and value
properties. To continue iterating, call the cursor’s continue()
method. When iteration ends, the cursor (i.e., event.target.result
) will be undefined
.
To get all values from the UseKeyGenerator
store, you can do this:
getAllByCursor(OB_NAMES.UseKeyGenerator).then(console.log);
In addition to openCursor()
, you can also use openKeyCursor()
to retrieve the primary keys of all stored objects. Its usage is the same as openCursor()
.
Using Indexes
When we create an index for an object store, we can then use that index to find values for a specific property:
function getByIndex(objName, indexName, key) {
return openindexedDB().then((db) => {
const index = db.transaction(objName).objectStore(objName).index(indexName);
return new Promise((resolve) => {
index.get(key).onsuccess = (e) => {
console.log("getByIndex", e.target.result);
return resolve(e.target.result);
};
});
});
}
For example, if we want to find logs where level
is info
, we can do this:
getByIndex(OB_NAMES.UseKeyGenerator, "level", "info").then((doc) =>
console.log(doc),
);
Setting Range and Cursor Direction
If you need to set the range for cursor iteration, you can use an IDBKeyRange
object and pass it as the first parameter to openCursor()
or openKeyCursor()
.
A key range value can be considered an interval, and the interval type can be either open or closed. Here are some examples:
/**
* NOTE:
* 只包括给定的值
*/
getWithRangeByCursor(
OB_NAMES.UseKeyGenerator,
"errorCode",
IDBKeyRange.only(1000),
)
.then(console.log)
.catch(console.error);
/**
* NOTE:
* 默认是闭区间
*/
getWithRangeByCursor(
OB_NAMES.UseKeyGenerator,
"errorCode",
IDBKeyRange.lowerBound(1000),
)
.then(console.log)
.catch(console.error);
/**
* NOTE:
* 设置第二个可选参数为 true,则为开区间
*/
getWithRangeByCursor(
OB_NAMES.UseKeyGenerator,
"errorCode",
IDBKeyRange.lowerBound(1000, true),
)
.then(console.log)
.catch(console.error);
/**
* NOTE:
* 闭区间,如果索引是数字,那么按照数字大小决定升序
*/
getWithRangeByCursor(
OB_NAMES.UseKeyGenerator,
"errorCode",
IDBKeyRange.bound(1000, 2000),
)
.then(console.log)
.catch(console.error);
/**
* NOTE:
* 左闭右开区间:如果索引是字符串,那么安装 array.sort() 的方式决定升序
*/
getWithRangeByCursor(
OB_NAMES.UseKeyGenerator,
"time",
IDBKeyRange.bound(
"2017/11/8 下午4:53:042",
"2017/11/8 下午4:53:043",
false,
true,
),
)
.then(console.log)
.catch(console.error);
Descending Iteration
By default, the iteration direction is ascending. If you need to iterate in descending order, simply pass prev
as the second parameter to openCursor()
:
getWithDescendingByCursor(OB_NAMES.UseKeyGenerator, "time")
.then(console.log)
.catch(console.error);
This article was published on December 6, 2017 and last updated on December 6, 2017, 2860 days ago. The content may be outdated.