概述
什么是数据库
数据库是“按照数据结构来组织、存储和管理数据的仓库”。是一个长期存储在计算机内的、有组织的、可共享的、统一管理的大量数据的集合。
关系型数据库,传统的关系型数据库采用表格的储存方式, 数据以行和列的方式进行存储,表与表之间是有很多复杂的关联关系的,要读取和查询都十分方便。 常见的关系型数据库有Mysql,SqlServer等
而非关系型数据不适合这样的表格存储方式,通常以数据集的方式,大量的数据集中存储在一起,类似于键值对、图结构或者文档。如MongoDB、Redis等
“关系型数据库”对一致性要求非常严格,例如要写入100个数据,前99个成功了,结果第100个不合法,此时事务会回滚到最初状态。对于一致性却不是显得那么重要,要求不高,但对性能要求却很高,“非关系型数据库”则更合适;
IndexedDB
IndexedDB 是一个事务型数据库系统,是一个基于 JavaScript 的面向对象数据库,是一种数据持久存储在用户浏览器中的方法,是HTML5中新增的,具有强大的数据存储与查询能力;
现有的浏览器端数据储存方案:
cookie:不超过4KB
webStorage 存储:存储内容大小一般支持5MB左右
IndexedDB:IE的储存上限是250MB,Chrome和Opera是剩余空间的某个百分比,Firefox则没有上
IndexedDB的特点
(1)键值对储存。 IndexedDB内部采用对象仓库(object store)存放数据。所有类型的数据都可以直接存入,包括JavaScript对象。在对象仓库中,数据以“键值对”的形式保存,每一个数据都有对应的键名,键名是独一无二的,不能有重复,否则会抛出一个错误。
(2)异步。 IndexedDB操作时不会锁死浏览器,用户依然可以进行其他操作,这与localStorage形成对比,后者的操作是同步的。异步设计是为了防止大量数据的读写,拖慢网页的表现。
(3)支持事务。 IndexedDB支持事务(transaction),这意味着一系列操作步骤之中,只要有一步失败,整个事务就都取消,数据库回到事务发生之前的状态,不存在只改写一部分数据的情况,对保证数据安全具有重要意义,与传统数据库中的事务目的一致。
(4)同域限制 IndexedDB也受到同域限制,每一个数据库对应创建该数据库的域名。来自不同域名的网页,只能访问自身域名下的数据库,而不能访问其他域名下的数据库。
(5)储存空间大 IndexedDB的储存空间比localStorage大得多,一般来说不少于250MB。IE的储存上限是250MB,Chrome和Opera是剩余空间的某个百分比,Firefox则没有上限。
(6)支持二进制储存。 IndexedDB不仅可以储存字符串,还可以储存二进制数据。
(7) 是文档型数据库,与Mongodb等类似,与常见的关系型数据库不同,不支持SQL语句查询
(8)能够为数据建立索引,以此提高查询数据的各方面性能。(这个非常有用,是大数据快速查找的前提)
IndexedDB重要概念
仓库objectStore
IndexedDB没有表的概念,它只有仓库store的概念,大家可以把仓库理解为表即可,即一个store是一张表。
创建 objectStore 和修改 objectStore 都只能在 db 的 onupgradeneeded 事件中进行,因此,要创建 objectStore,必须在前面的 open 操作那个时候来进行。
索引index
在关系型数据库当中也有索引的概念,我们可以给对应的表字段添加索引,以便加快查找速率。在IndexedDB中同样有索引,我们可以在创建store的时候同时创建索引,在后续对store进行查询的时候即可通过索引来筛选,给某个字段添加索引后,在后续插入数据的过成功,索引字段便不能为空。
创建索引这个动作,实际上是对 objectStore 进行修改,因此,只能在 db 的 onupgradeneeded 事件中处理。
修改索引:虽然 objectStore 本身的信息是不能修改的,比如 name 和 keyPath 都是不能修改的,但是它所拥有的索引可以被修改,修改其实就是删除+添加操作。用到的就是 deleteIndex 这个方法,所以,如果你想修改一个索引,要做的就是先删除掉原来的同名索引,然后添加新的索引
游标cursor
游标是IndexedDB数据库新的概念,大家可以把游标想象为一个指针,比如我们要查询满足某一条件的所有数据时,就需要用到游标,我们让游标一行一行的往下走,游标走到的地方便会返回这一行数据,此时我们便可对此行数据进行判断,是否满足条件。
【注意】:IndexedDB查询不像MySQL等数据库方便,它只能通过主键、索引、游标方式查询数据。
事务
数据库的事务(英文为’transaction’),我们可以理解为对数据库的操作,而且专指一个序列上的操作;。举个例子,银行转账,一个账号钱少了然后另一个账号钱多了,这两个操作要么都执行,要么都不执行。像这种操作就可以看成一个事务。
事务的提出主要是为了保证并发情况下保持数据一致性;
可以粗浅地理解为:开始事务,巴拉巴拉操作,如果错误,回滚(rollback),如果没问题,提交(commit),结束事务。
transaction方法用于创建一个数据库事务。向数据库添加数据之前,必须先创建数据库事务。
transaction方法接受两个参数:第一个参数是一个数组,里面是所涉及的对象仓库,通常是只有一个;第二个参数是一个表示操作类型的字符串。目前,操作类型只有两种:readonly(只读)和readwrite(读写)。添加数据使用readwrite,读取数据使用readonly。
transaction方法返回一个事务对象,该对象的objectStore方法用于获取指定的对象仓库。
var t = db.transaction(["firstOS"],"readwrite");
var store = t.objectStore("firstOS");
IndexedDB实操
检测浏览器是否支持IndexedDB API,并做好前缀处理:
{{replace_code_start}} window.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB; window.IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction || window.msIDBTransaction; window.IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange || window.msIDBKeyRange if (!window.indexedDB) { window.alert("Your browser doesn't support a stable version of IndexedDB.") } {{replace_code_end}}
1. 创建或连接数据库
{{replace_code_start}} /** * 打开数据库 * @param {object} dbName 数据库的名字 * @param {string} storeName 仓库名称 * @param {string} version 数据库的版本 * @return {object} 该函数会返回一个数据库实例 */ function openDB(dbName, version = 1) { return new Promise((resolve, reject) => { // 注意兼容浏览器 var indexedDB = window.indexedDB let db; // 打开数据库,若没有则会创建 const request = indexedDB.open(dbName, version); // 数据库打开成功回调 request.onsuccess = function (event) { db = event.target.result; // 数据库对象 console.log("数据库打开成功"); resolve(db); }; // 数据库打开失败的回调 request.onerror = function (event) { console.log("数据库打开报错"); }; // 数据库有更新时候的回调 request.onupgradeneeded = function (event) { // 数据库创建或升级的时候会触发 console.log("onupgradeneeded"); db = event.target.result; // 数据库对象 var objectStore; // 创建存储库 objectStore = db.createObjectStore("users", { keyPath: "userId", // 这是主键 // autoIncrement: true // 实现自增 }); // 创建索引,在后面查询数据的时候可以根据索引查 objectStore.createIndex("nameIndex", "name", { unique: false }); objectStore.createIndex("ageIndex", "age", { unique: false }); objectStore.createIndex("sexIndex", "sex", { unique: false }); }; }); } {{replace_code_end}}
我们将创建数据库的操作封装成了一个函数,并且该函数返回一个promise对象,使得在调用的时候可以链式调用,函数主要接收两个参数:数据库名称、数据库版本。函数内部主要有三个回调函数,分别是:
onsuccess:数据库打开成功或者创建成功后的回调,这里我们将数据库实例返回了出去。
onerror:数据库打开或创建失败后的回调。
onupgradeneeded:当数据库版本有变化的时候会执行该函数,比如我们想创建新的存储库(表),就可以在该函数里面操作,更新数据库版本即可。
第一次打开数据库时,会先触发upgradeneeded事件,然后触发success事件。
createIndex 方法接收三个参数,第一个为索引名,第二个为数据对象的属性,上例中使用 name 属性来创建索引,第三个参数为可选参数,值为一个js对象。 unique 这个索引是否是唯一, 该对象中的 unique 属性为 true,代表索引值不可以相同,即两条数据的 name 不可以相同,为 false 则可以相同。
2. 新增数据
{{replace_code_start}} /** * 新增数据 * @param {object} db 数据库实例 * @param {string} storeName 仓库名称 * @param {string} data 数据 */ function addData(db, storeName, data) { var request = db .transaction([storeName], "readwrite") // 事务对象 指定表格名称和操作模式("只读"或"读写") .objectStore(storeName) // 仓库对象 .add(data); request.onsuccess = function (event) { console.log("数据写入成功"); }; request.onerror = function (event) { console.log("数据写入失败"); }; } {{replace_code_end}}
IndexedDB插入数据需要通过事务来进行操作,插入的方法也很简单,利用IndexedDB提供的add方法即可,这里我们同样将插入数据的操作封装成了一个函数,接收三个参数,分别如下:
db:在创建或连接数据库时,返回的db实例,需要那个时候保存下来。
storeName:仓库名称(或者表名),在创建或连接数据库时我们就已经创建好了仓库。
data:需要插入的数据,通常是一个对象
**【注意】:**插入的数据是一个对象,而且必须包含我们声明的索引键值对。
3. 通过主键读取数据
{{replace_code_start}} /** * 通过主键读取数据 * @param {object} db 数据库实例 * @param {string} storeName 仓库名称 * @param {string} key 主键值 */ function getDataByKey(db, storeName, key) { return new Promise((resolve, reject) => { var transaction = db.transaction([storeName]); // 事务 var objectStore = transaction.objectStore(storeName); // 仓库对象 var request = objectStore.get(key); // 通过主键获取数据 request.onerror = function (event) { console.log("事务失败"); }; request.onsuccess = function (event) { console.log("主键查询结果: ", request.result); resolve(request.result); }; }); } {{replace_code_end}}
主键即刚刚我们在创建数据库时声明的keyPath,通过主键只能查询出一条数据。
4. 通过游标查询数据
{{replace_code_start}} /** * 通过游标读取数据 * @param {object} db 数据库实例 * @param {string} storeName 仓库名称 */ function cursorGetData(db, storeName) { let list = []; var store = db .transaction(storeName, "readwrite") // 事务 .objectStore(storeName); // 仓库对象 var request = store.openCursor(); // 指针对象 // 游标开启成功,逐行读数据 request.onsuccess = function (e) { var cursor = e.target.result; if (cursor) { // 必须要检查 list.push(cursor.value); cursor.continue(); // 遍历了存储对象中的所有内容 } else { console.log("游标读取的数据:", list); } }; } {{replace_code_end}}
上面函数开启了一个游标,然后逐行读取数据,存入数组,最终得到整个仓库的所有数据。
5. 通过索引查询数据
{{replace_code_start}} /** * 通过索引读取数据 * @param {object} db 数据库实例 * @param {string} storeName 仓库名称 * @param {string} indexName 索引名称 * @param {string} indexValue 索引值 */ function getDataByIndex(db, storeName, indexName, indexValue) { var store = db.transaction(storeName, "readwrite").objectStore(storeName); var request = store.index(indexName).get(indexValue); request.onerror = function () { console.log("事务失败"); }; request.onsuccess = function (e) { var result = e.target.result; console.log("索引查询结果:", result); }; } {{replace_code_end}}
索引名称即我们创建仓库的时候创建的索引名称,也就是键值对中的键,最终会查询出所有满足我们传入函数索引值的数据。
6. 通过索引和游标查询数据
通过4和5节我们发现, 单独通过索引或者游标查询出的数据都是部分或者所有数据,如果我们想要查询出索引中满足某些条件的所有数据,那么单独使用索引或游标是无法实现的。当然,你也可以查询出所有数据之后在循环数组筛选出合适的数据,但是这不是最好的实现方式,最好的方式当然是将索引和游标结合起来。
{{replace_code_start}} /** * 通过索引和游标查询记录 * @param {object} db 数据库实例 * @param {string} storeName 仓库名称 * @param {string} indexName 索引名称 * @param {string} indexValue 索引值 */ function cursorGetDataByIndex(db, storeName, indexName, indexValue) { let list = []; var store = db.transaction(storeName, "readwrite").objectStore(storeName); // 仓库对象 var request = store .index(indexName) // 索引对象 .openCursor(IDBKeyRange.only(indexValue)); // 指针对象,这里设置为只获取indexName等于indexValue的数据 request.onsuccess = function (e) { var cursor = e.target.result; if (cursor) { // 必须要检查 list.push(cursor.value); cursor.continue(); // 遍历了存储对象中的所有内容 } else { console.log("游标索引查询结果:", list); } }; request.onerror = function (e) {}; } {{replace_code_end}}
利用索引和游标结合查询,我们可以查询出索引值满足我们传入函数值的所有数据对象,而不是之查询出一条数据或者所有数据。
7. 通过索引和游标分页查询
IndexedDB分页查询不像MySQL分页查询那么简单,没有提供现成的API,所以需要我们自己实现分页。
{{replace_code_start}} /** * 通过索引和游标分页查询记录 * @param {object} db 数据库实例 * @param {string} storeName 仓库名称 * @param {string} indexName 索引名称 * @param {string} indexValue 索引值 * @param {number} page 页码 * @param {number} pageSize 查询条数 */ function cursorGetDataByIndexAndPage( db, storeName, indexName, indexValue, page, pageSize ) { let list = []; let counter = 0; // 计数器 let advanced = true; // 是否跳过多少条查询 var store = db.transaction(storeName, "readwrite").objectStore(storeName); // 仓库对象 var request = store .index(indexName) // 索引对象 .openCursor(IDBKeyRange.only(indexValue)); // 指针对象 request.onsuccess = function (e) { var cursor = e.target.result; if (page > 1 && advanced) { advanced = false; cursor.advance((page - 1) * pageSize); // 跳过多少条 return; } if (cursor) { // 必须要检查 list.push(cursor.value); counter++; if (counter < pageSize) { cursor.continue(); // 遍历了存储对象中的所有内容 } else { cursor = null; console.log("分页查询结果", list); } } else { console.log("分页查询结果", list); } }; request.onerror = function (e) {}; } {{replace_code_end}}
这里用到了IndexedDB的一个API:advance。该函数可以让我们的游标跳过多少条开始查询。假如我们的额分页是每页10条数据,现在需要查询第2页,那么我们就需要跳过前面10条数据,从11条数据开始查询,直到计数器等于10,那么我们就关闭游标,结束查询。
8. 更新数据
IndexedDB更新数据较为简单,直接使用put方法,值得注意的是如果数据库中没有该条数据,则会默认增加该条数据,否则更新。有些同学喜欢更新和新增都是用put方法,这也是可行的。
{{replace_code_start}} /** * 更新数据 * @param {object} db 数据库实例 * @param {string} storeName 仓库名称 * @param {object} data 数据 */ function updateDB(db, storeName, data) { var request = db .transaction([storeName], "readwrite") // 事务对象 .objectStore(storeName) // 仓库对象 .put(data); request.onsuccess = function () { console.log("数据更新成功"); }; request.onerror = function () { console.log("数据更新失败"); }; } {{replace_code_end}}
put方法接收一个数据对象。
9. 通过主键删除数据
主键即我们创建数据库时申明的keyPath,它是唯一的。
{{replace_code_start}} /** * 通过主键删除数据 * @param {object} db 数据库实例 * @param {string} storeName 仓库名称 * @param {object} id 主键值 */ function deleteDB(db, storeName, id) { var request = db .transaction([storeName], "readwrite") .objectStore(storeName) .delete(id); request.onsuccess = function () { console.log("数据删除成功"); }; request.onerror = function () { console.log("数据删除失败"); }; } {{replace_code_end}}
该种删除只能删除一条数据,必须传入主键。
10. 通过索引和游标删除指定数据
{{replace_code_start}} /** * 通过索引和游标删除指定的数据 * @param {object} db 数据库实例 * @param {string} storeName 仓库名称 * @param {string} indexName 索引名 * @param {object} indexValue 索引值 */ function cursorDelete(db, storeName, indexName, indexValue) { var store = db.transaction(storeName, "readwrite").objectStore(storeName); var request = store .index(indexName) // 索引对象 .openCursor(IDBKeyRange.only(indexValue)); // 指针对象 request.onsuccess = function (e) { var cursor = e.target.result; var deleteRequest; if (cursor) { deleteRequest = cursor.delete(); // 请求删除当前项 deleteRequest.onerror = function () { console.log("游标删除该记录失败"); }; deleteRequest.onsuccess = function () { console.log("游标删除该记录成功"); }; cursor.continue(); } }; request.onerror = function (e) {}; } {{replace_code_end}}
上段代码可以删除索引值为indexValue的所有数据,值得注意的是使用了IDBKeyRange.only()API,该API代表只能当两个值相等时,具体API解释可参考MDN官网。
openCursor 方法接收两个参数,第一个是 IDBKeyRange 对象,该对象创建方法主要有以下几种:
// boundRange 表示主键值从1到10(包含1和10)的集合。
// 如果第三个参数为true,则表示不包含最小键值1,如果第四参数为true,则表示不包含最大键值10,默认都为false
var boundRange = IDBKeyRange.bound(1, 10, false, false);
// onlyRange 表示由一个主键值的集合。only() 参数则为主键值,整数类型。
var onlyRange = IDBKeyRange.only(1);
// lowerRaneg 表示大于等于1的主键值的集合。
// 第二个参数可选,为true则表示不包含最小主键1,false则包含,默认为false
var lowerRange = IDBKeyRange.lowerBound(1, false);
// upperRange 表示小于等于10的主键值的集合。
// 第二个参数可选,为true则表示不包含最大主键10,false则包含,默认为false
var upperRange = IDBKeyRange.upperBound(10, false);
openCursor 方法的第二个参数表示游标的读取方向,主要有以下几种:
next : 游标中的数据按主键值升序排列,主键值相等的数据都被读取
nextunique : 游标中的数据按主键值升序排列,主键值相等只读取第一条数据
prev : 游标中的数据按主键值降序排列,主键值相等的数据都被读取
prevunique : 游标中的数据按主键值降序排列,主键值相等只读取第一条数据
11. 清除数据库
{{replace_code_start}} /** * 通过主键删除数据 * @param {object} db 数据库实例 * @param {string} storeName 仓库名称 */ function deleteDB(db, storeName) { var request = db .transaction([storeName], "readwrite") .objectStore(storeName) .clear(); request.onsuccess = function () { console.log("数据清除成功"); }; request.onerror = function () { console.log("数据清除失败"); }; } {{replace_code_end}}
没有参数,发起一个删除objectStore里面的所有object的Request。
清除数据之前请备份好数据
12. 关闭数据库
当我们数据库操作完毕后,建议关闭它,节约资源。
{{replace_code_start}} /** * 关闭数据库 * @param {object} db 数据库实例 */ function closeDB(db) { db.close(); console.log("数据库已关闭"); } {{replace_code_end}}
13. 删除数据库
最后我们需要删库跑路,删除操作也很简单。
{{replace_code_start}} /** * 删除数据库 * @param {object} dbName 数据库名称 */ function deleteDBAll(dbName) { console.log(dbName); let deleteRequest = window.indexedDB.deleteDatabase(dbName); deleteRequest.onerror = function (event) { console.log("删除失败"); }; deleteRequest.onsuccess = function (event) { console.log("删除成功"); }; } {{replace_code_end}}
总结
IndexedDB数据库没有我们想象的那么复杂,了解了它的几个基本概念,上手还是很快的,无非就是增删改查等等,虽然可能开发中用的少,但是了解一下不至于真正用到的时候两眼抓瞎。