什么是 IndexedDB
IndexedDB 是一个基于 JavaScript 的面向对象数据库,是一个事务型数据库系统。是一种底层 API,用于在客户端存储大量的结构化数据(也包括文件/二进制大型对象(blobs)),允许存储和检索用键索引的对象,使用索引实现对数据的高性能搜索。
与传统的 web storage 相比,IndexedDB 可以存储更大量的结构化数据,且支持复杂的查询。
本文章将介绍 IndexedDB 的基础知识,包括创建数据库、创建对象存储仓库、执行增删改查操作等。
基本概念定义
IndexedDB 是一个复杂的 API,涉及到很多概念,把不同的实体抽象成一个一个对象,下面就从操作数据库的流程中来介绍这些常用的对象。
- 『IDBOpenDBRequest 打开数据库请求对象』 表示打开或删除数据库的请求。
- 『IDBDatabase 数据库对象』 表示一个数据库连接。这是在数据库中获取事务的唯一方式。同一个时刻,数据库只能存在一个版本,如果版本不相同在对数据库进行增删改、索引等操作必须通过升级数据库来完成。
- 『IDBObjectStore 仓库对象』 表示一个对象库,其中的记录根据其键值进行排序,可以实现快速插入,查找和有序检索。为了方便理解,可以把“对象存储空间”想象成关系数据库的“表”结构。
- 『IDBIndex 索引对象』 表示数据存储对象的索引,用于在仓库对象中查找记录,索引是对数据库表中一列或多列的值进行排序的一种结构,使用索引可快速访问数据库表中的特定信息。index 是一个持久的
键——值
存储,插入、更新或删除数据记录,索引就会自动更新。 - 『IDBTransaction事务对象』 用来异步操作数据库事务,所有的读写操作都要通过这个对象进行。
indexedDB
支持事务意味着在一系列的操作步骤中一旦有一步异常了,整个事务会取消恢复至事务发生之前的状态。 - 『IDBCursor游标对象』 用于遍历数据库中的多条记录。游标有一个源,表示需要遍历的索引或者对象存储区。游标在所属区间范围内有一个位置,根据记录健(存储字段)的顺序递增或递减方向移动。
- 『IDBKeyRange主键组对象』 代表数据仓库(object store)里面的一组主键。
操作数据库流程
打开数据库
// 打开我们的数据库
const request = window.indexedDB.open("MyTestDatabase", 3);
open 方法接受两个参数。
- 第一个是数据库名称,如果数据库名称不存在则会创建一个;
- 第二个是数据库版本号,可省略,省略时如果打开已有数据库则默认为当前数据库版本,新建数据库时默认为
1
。
open 请求不会立即打开数据库或者开始一个事务,而是立即返回一个 IDBOpenDBRequest
对象 request
,并异步执行打开操作。其中 IDBOpenDBRequest
对象有三个处理事件:onerror
、onsuccess
、upgradeneeded
,包含处理结果或者错误。
request.onerror = (event) => {
// 数据库打开报错
console.error(`数据库错误:${event.target.errorCode}`);
};
request.onsuccess = (event) => {
// 数据库打开成功
const db = event.target.result;
};
request.onupgradeneeded = (event) => {
// 创建一个新的数据库或者增加已存在的数据库的版本号
const db = event.target.result;
};
每一个请求都有一个 readyState 属性,初始时为 pending,当请求完成或失败的时候,readyState 会变为 done。当状态值变为 done 时,每一个请求都会返回 result 和 error 属性,并且会触发一个事件,如果成功打开数据库一切顺利的话,会触发 onsuccess
事件;有任何错误的话,则会触发 onerror
事件;当你创建一个新的数据库或者增加已存在的数据库的版本号,都会触发 onupgradeneeded
事件。
event.target.result
是一个 IDBDatabase
的实例。IndexedDB 使用对象存储而不是表,并且一个数据库可以包含任意数量的对象存储
新建数据库
新建数据库与打开数据库是同一个操作,如果 open
指定的数据库不存在,就会新建。当新建一个数据库,或者指定一个版本号较高的版本时,会触发 onupgradeneeded
事件。
IDBVersionChangeEvent
对象会作为参数传递给绑定在request.result
(例如示例中的 db
)上的 onversionchange
事件处理器上。
request.onupgradeneeded = (event) => {
// 创建一个新的数据库或者增加已存在的数据库的版本号
const db = event.target.result;
db.onversionchange = () => {
console.log('数据库版本变更');
};
};
删除数据库
deleteDatabase()
方法用于删除一个数据库,参数为数据库的名字。它会立刻返回一个IDBOpenDBRequest
对象,对数据库进行异步删除。调用deleteDatabase()
方法以后,当前数据库的其他已经打开的连接都会接收到versionchange
事件。删除不存在的数据库并不会报错。
const request = window.indexedDB.deleteDatabase('MyTestDatabase');
request.onsuccess = function (event) {
console.log('数据库删除成功');
};
request.onerror = function (event) {
console.log('数据库删除失败');
};
新建对象仓库
在数据库新建完成后,通常会创建一个对象仓库,
request.onupgradeneeded = (event) => {
const db = event.target.result;
// 创建一个对象存储来存储人员信息,我们将“id”作为键路径。
const objectStore = db.createObjectStore("person", {
keyPath: "id",
autoIncrement: false,
});
}
IndexedDB
使用对象存储而不是表,并且一个数据库可以包含任意数量的对象存储。每当一个值被存入一个对象存储时,它会与一个键相关联。其中键取决于对象存储是使用键路径还是键生成器 分成以下几种不同的创建方式:
autoIncrement
属性表示,是否使用自动递增的整数作为主键(第一个数据记录为1,第二个数据记录为2,以此类推),默认为false
。
🔔 通常情况下可以增加条件判断,当前要创建的表格是否存在,不存在再创建。
if (!db.objectStoreNames.contains('person')) {
objectStore = db.createObjectStore('person', { keyPath: 'id' });
}
启动事务
事务是一组数据库操作的逻辑单元,它要么全部成功执行,要么全部回滚。具有以下特性:
- 原子性(Atomicity):事务中的所有操作要么全部成功执行,要么全部回滚,不会出现部分执行的情况。
- 一致性(Consistency):事务执行前后,数据库的状态保持一致,不会破坏数据的完整性和约束条件。
- 隔离性(Isolation):并发执行的多个事务之间应该相互隔离,每个事务都应该感觉不到其他事务的存在。
- 持久性(Durability):事务一旦提交成功,对数据库的修改应该永久保存,即使系统发生故障也不会丢失。
IDBDatabase.transaction(objectName, [mode])
方法方法接受两个参数(一个是可选的)并返回的就是一个 IDBTransaction 对象。通过IDBTransaction.objectStore(name)
方法,可以获取到 IDBObjectStore 对象。
- 第一个参数
objectName
是一个数组,表示事务希望跨越的对象存储空间的列表。 - 第二个参数
mode
可选,默认readonly
表示只读事务;如果想写入数据则需要传入readwrite
事务包含三个事件,error
、abort
和 complete
error
接收由它产生的所有请求所产生的错误,并且错误会中断它所处的事务abort
如果事务中没有处理一个已发生的错误事件或者调用abort()
方法,那么该事务会被回滚,并触发abort
事件。complete
在所有请求完成后,事务的complete
事件会被触发。
request.onupgradeneeded = (event) => {
const db = event.target.result;
// 在所有数据添加完毕后的处理
transaction.oncomplete = (event) => {
console.log("全部请求完成了!");
};
transaction.onerror = (event) => {
// 错误处理!
};
// 开启一个可写的事务 transaction,并从中获取一个对象存储 objectStore
const transaction = db.transaction(["person"], "readwrite");
const objectStore = transaction.objectStore("person");
}
增删查改操作
增加数据
add(value, key)
用于向对象仓库添加数据,返回一个 IDBRequest 对象。该方法只用于添加数据,如果主键相同会报错,因此更新数据必须使用put()
方法。该方法返回一个 IDBRequest 对象。
value
是待添加的数据key
是主键,该参数可选,如果省略默认为null
function addData = () {
const request = db.transaction(['person'], 'readwrite')
.objectStore('person')
.add({ id: 1, name: '张美丽', age: 30, email: 'zhangmeili@qq.com' });
request.onsuccess = function (event) {
// event.target.result 被添加的数据的键
console.log('数据写入成功');
};
request.onerror = function (event) {
console.log('数据写入失败');
}
}
删除数据
delete(key)
方法用于删除指定主键的记录。该方法返回一个 IDBRequest 对象。
function deleteData () {
const request = db.transaction(['person'], 'readwrite')
.objectStore('person')
.delete(1);
request.onsuccess = function (event) {
console.log('数据删除成功');
};
request.onerror = function (event) {
console.log('数据删除失败');
}
}
clear()
删除当前对象仓库的所有记录,不需要参数,该方法返回一个 IDBRequest 对象。
function deleteData () {
const request = db.transaction(['person'], 'readwrite')
.objectStore('person')
.clear();
request.onsuccess = function (event) {
console.log('数据清空成功');
};
request.onerror = function (event) {
console.log('数据清空失败');
}
}
修改数据
put(value, key)
可以对数据进行修改,方法用于更新某个主键对应的数据记录,如果对应的键值不存在,则插入一条新的记录。该方法返回一个 IDBRequest 对象。
value
为待更新的数据key
为主键,该参数可选,且只在自动递增时才有必要提供,因为那时主键不包含在数据值里面
function putData () {
const request = db.transaction(['person'], 'readwrite')
.objectStore('person')
.put({ id: 1, name: '张美丽', age: 31, email: 'zhangmeili@qq.com' });
request.onsuccess = function (event) {
console.log('数据更新成功');
};
request.onerror = function (event) {
console.log('数据更新失败');
}
}
读取数据
get(key)
用于获取主键对应的数据记录,该方法返回一个 IDBRequest 对象。
function getData () {
const request = db.transaction(['person'])
.objectStore('person')
.get(1);
request.onsuccess = function (event) {
console.log('数据获取成功', event.target.result);
// { id: 1, name: '张美丽', age: 31, email: 'zhangmeili@qq.com' }
};
request.onerror = function (event) {
console.log('数据获取失败');
}
}
遍历数据表
get(key)
要求你知道想要检索的是哪一个键,如果你想要遍历对象存储空间中的所有值,那么你可以使用游标。
function getAllData () {
const objectStore = db.transaction("person")
.objectStore("person");
objectStore.openCursor().onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
console.log(`id: ${cursor.key} , name: ${cursor.value.name}`);
cursor.continue();
} else {
console.log("没有更多记录了!");
}
};
}
openCursor()
用来获取一个游标的指针对象。游标对象 event.target.result
的 key
和 value
表示实际的键和值。continue()
方法将游标指针移到下一个数据对象,如果当前数据对象已经是最后一个数据了,则游标指针指向null
。
openCursor()
函数接受两个参数:
- 第一个
query
限制被检索的项目的范围,默认为所有的记录,可以指定为主键值,或者一个 IDBKeyRange 对象。 - 第二个
direction
确定迭代的方向,默认值为next
,其他可能的值为prev
、nextunique
和prevunique
IDBCursor
对象有以下方法:
advance(n)
:指针向前移动 n 个位置。continue()
:指针向前移动一个位置。它可以接受一个主键作为参数,这时会跳转到这个主键。continuePrimaryKey()
:该方法需要两个参数,第一个是key
,第二个是primaryKey
,将指针移到符合这两个参数的位置。delete()
:用来删除当前位置的记录,返回一个 IDBRequest 对象。该方法不会改变指针的位置。update()
:用来更新当前位置的记录,返回一个 IDBRequest 对象。它的参数是要写入数据库的新的值。
创建并使用索引
indexedDB
索引的意义在于允许通过任意属性获取数据记录,对于某些数据,一个对象存储空间可能需要指定多个键,可以将其中一个设置为主键,其他属性设置为索引。
下面是对 name
字段创建了索引,后面就可以通过 name
来获取数据记录了。
request.onupgradeneeded = (event) => {
const db = event.target.result;
// 创建一个对象存储来存储人员信息,我们将“id”作为键路径。
const objectStore = db.createObjectStore('person', { keyPath: 'id' });
// 对 name 字段创建索引,name 不唯一
objectStore.createIndex("nameIndex", "name", { unique: false });
const request = db.transaction(['person'])
.objectStore('person')
.index('nameIndex')
.get('张美丽');
request.onsuccess = function (event) {
console.log('数据获取成功', event.target.result);
// { id: 1, name: '张美丽', age: 31, email: 'zhangmeili@qq.com' }
};
request.onerror = function (event) {
console.log('数据获取失败');
}
}
createIndex()
接收三个参数:
indexName
:索引名称keyPath
:索引所在的属性options
:配置对象unique
表示是否唯一;multiEntry
当索引属性是一个数组时,设置为true,则createIndex将在每个数组元素的索引中添加一个条目。否则,它会添加一个包含该数组的条目。
IDBIndex 对象有以下异步的方法,立即返回的都是一个 IDBRequest 对象。
count()
:用来获取记录的数量。它可以接受主键或 IDBKeyRange 对象作为参数,这时只返回符合主键的记录数量,否则返回所有记录的数量。get(key)
:用来获取符合指定主键的数据记录。getKey(key)
:用来获取指定的主键。getAll()
:用来获取所有的数据记录。它可以接受两个参数,都是可选的,第一个参数用来指定主键,第二个参数用来指定返回记录的数量。如果省略这两个参数,则返回所有记录。由于获取成功时,浏览器必须生成所有对象,所以对性能有影响。如果数据集比较大,建议使用 IDBCursor 对象。getAllKeys()
:该方法与IDBIndex.getAll()
方法相似,区别是获取所有主键。openCursor()
:用来获取一个 IDBCursor 对象,用来遍历索引里面的所有条目。openKeyCursor()
:该方法与IDBIndex.openCursor()
方法相似,区别是遍历所有条目的主键。
下面是通过 IDBCursor 对象来遍历根据索引获取到的所有数据。
request.onupgradeneeded = (event) => {
const db = event.target.result;
// 创建一个对象存储来存储人员信息,我们将“id”作为键路径。
const objectStore = db.createObjectStore('person', { keyPath: 'id' });
// 对 name 字段创建索引,name 不唯一
objectStore.createIndex("nameIndex", "name", { unique: false });
const indexRequest = objectStore.transaction(['person'])
.objectStore('person')
.index('nameIndex');
indexRequest.openCursor().onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
console.log(`id: ${cursor.key} , name: ${cursor.value.name}`);
cursor.continue();
} else {
console.log("没有更多记录了!");
}
};
}
IDBKeyRange 对象
IDBKeyRange
对象表示一组主键,根据这一组主键可以获取到仓库对象或者索引中的一组记录。它有四个静态方法,用来指定主键的范围:
IDBKeyRange.lowerBound()
:指定下限。IDBKeyRange.upperBound()
:指定上限。IDBKeyRange.bound()
:同时指定上下限。- 支持四个参数
IDBKeyRange.bound(lower, upper, lowerOpen, upperOpen)
lowerOpen
upperOpen
分别表示下限/下限是否为开区间。
- 支持四个参数
IDBKeyRange.only()
:指定只包含一个值。
IDBKeyRange 实例对象生成以后,将它作为参数传给 IDBIndex 对象的openCursor()
方法,就可以在所设定的范围内读取数据。
function getAllData () {
const indexRequest = objectStore.transaction(['person'])
.objectStore('person')
.index('nameIndex');
const keyRangeValue = IDBKeyRange.bound('A', 'K', false, false);
indexRequest.openCursor(keyRangeValue).onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
console.log(`id: ${cursor.key} , name: ${cursor.value.name}`);
cursor.continue();
} else {
console.log("没有更多记录了!");
}
};
}
IDBKeyRange 有一个实例方法includes(key)
返回一个布尔值,表示某个主键是否包含在当前这个主键组之内。
var keyRangeValue = IDBKeyRange.bound('A', 'K', false, false);
keyRangeValue.includes('F') // true
keyRangeValue.includes('K') // false
keyRangeValue.includes('M') // false