基于indexDB的Dexie数据库

什么是Dexie?

A Minimalistic Wrapper for IndexedDB
这是官网的一句话翻译过来就是IndexedDB 的一个最小化包装

Dexie使用本机IndexedDB API解决了三个主要问题:

  • 模棱两可的错误处理
  • 不好查询
  • 代码复杂性

有使用过indexDB的同学就知道其查询语句的麻烦特别是多条件查询的时候
这是Dexie的官网https://dexie.org/

性能

Dexie表现出色。 它的批量方法利用了IndexedDB中一个鲜为人知的特性,可以在不收听每个onsuccess事件的情况下存储东西。 这样可以最大限度地提高性能。

安装及使用

1.安装

模块化开发下可以使用npm/cnpm/yarn安装

npm install dexie

而想直接引入使用的也可以使用以下方法

<script src="https://unpkg.com/dexie@latest/dist/dexie.js"></script>

2.使用

这里我采用了一下官网的一个简单的例子

//模块化开发下需要引入该组件
import Dexie from 'dexie'
//创建一个数据库 若数据库已存在则为打开 
//打开数据库时,会判断当前version值是否大于已经存在的version值,若大于则会upgrade即升到最高版本
var db = new Dexie("test_db");

db.version(1).stores({
     student: 'name,age'
 });
 db.open()
//写入一些数据
db.student.put({name: "小明", age: 18}).then (function(){
    //当数据存储完成后 我们可以读取它
    return db.student.get('小明');
}).then(function (data) {
    console.log("我是小明,今年 " + data.age);
}).catch(function(error) {
   //最后别忘了抓住任何可能发生在上面的代码块。
   console.log("error: " + error);
   db.close()
});
db.close()

API

创建数据库

//注意:不要像在SQL中那样声明所有列。只声明要索引的属性,即要在where(…)查询中使用的属性。
var db = new Dexie("MyDatabase");
db.version(1).stores({
    friends: "++id, name, age, *tags",
    gameSessions: "id, score"
});

语法

++自动递增主键
&唯一主键
*多条目索引
+复合索引

第一第二栏目就不说了很好理解,这里主要说明一下第三第四栏

多条目索引

var db = new Dexie('dbname');
db.version(1).stores ({
  books: 'id, author, name, *categories'
});

在本示例中,书籍可以按多个类别进行分类。这是通过让book对象具有一个名为“categories”的数组属性来实现的,该数组属性包含类别字符串。见以下示例:

db.books.put({
  id: 1,
  name: 'Under the Dome', 
  author: 'Stephen King',
  categories: ['sci-fi', 'thriller']
});

在示例中,我们添加了一本包含多个类别“科幻”和“惊悚”的书。注意,不仅字符串可以放入数组,而且任何可索引类型都是有效的。

如何查询多条目索引?

所有where子句运算符都可用于查询多条目索引对象。但是,运算符的行为不像普通索引那样直观。例如,应该使用WhereClause.equals()运算符查询属于特定类别的书籍,而更具语义的名称可能是contains()。这样做的原因是要映射indexedDB在本机上的工作方式,还允许使用任何运算符,而不将多条目索引绑定到某些运算符。

// 查询所有科幻书籍:
function getSciFiBooks() {
  return db.books
    .where('categories').equals('sci-fi')
    .toArray ();
}
distinct()运算符

查询多条目索引时,如果同一项有多个索引匹配,则可能会得到同一对象的多个结果。因此,在对多条目索引的查询中始终使用Collection.distinct()是一个很好的做法。

// 定义数据库
var db = new Dexie('dbname');
db.version(1).stores ({
  books: 'id, author, name, *categories'
});

// 插入一本多类别的书
db.books.put({
  id: 1,
  name: 'Under the Dome', 
  author: 'Stephen King',
  categories: ['sci-fi', 'thriller']
});

// 查询所有科幻书籍:
function getSciFiBooks() {
  return db.books
    .where('categories').equals('sci-fi')
    .toArray ();
}

// 查询所有科幻或浪漫书籍:
function getSciFiOrRomanceBooks() {
  return db.books
    .where('categories').anyOf('sci-fi', 'romance')
    .distinct() // 筛选掉重复的数据
    .toArray()
}

// 复杂查询
function complexQuery() {
  return db.books
    .where('categories').startsWithAnyOfIgnoreCase('sci', 'ro')
    .or('author').equalsIgnoreCase('stephen king')
    .distinct()
    .toArray();
}

局限性
  • 复合索引不能标记为多条目。其局限性在于indexedDB本身。
  • 不能将主键标记为MultiEntry。

以下浏览器不支持多条目索引:

  • ie10 、11
  • 基于非chromium的Microsoft Edge浏览器
  • Safari 8, 9.

复合索引

复合索引是基于多个键路径的索引。它可以有效地为一个索引中的多个属性建立索引,以方便地找到两个键的组合及其值的存在性。

定义架构时必须指定复合索引:

var db = new Dexie('dbname');
db.version(1).stores({
    people: 'id, [firstName+lastName]'
});

在上面的示例中,firstName和lastName属性中包含有效键的记录将被索引。如果存储了具有属性{firstName:‘foo’,lastName:‘bar’}的对象,则可以使用以下方法有效地查找该对象:

db.people.where('[firstName+lastName]').equals(['foo', 'bar'])
//或者下面这样
db.people.where({firstName: 'foo', lastName: 'bar'})`

第二种写法是一个特例,它只在Dexie>=2.0中工作,并且可以通过多个属性进行匹配,无论您的浏览器是否支持复合查询。

附赠官网的语法文档

升级(upgrade)

db.version(1).stores({
    friends: "++id,name,age,*tags",
    gameSessions: "id,score"
});

db.version(2).stores({
    friends: "++id, [firstName+lastName], yearOfBirth, *tags", // 更改索引
    gameSessions: null // Delete 对象仓库

}).upgrade(tx => {
    // 仅当安装了低于2的version时才会执行
    return tx.table("friends").modify(friend => {
        friend.firstName = friend.name.split(' ')[0];
        friend.lastName = friend.name.split(' ')[1];
        friend.birthDate = new Date(new Date().getFullYear() - friend.age, 0);
        delete friend.name;
        delete friend.age;
    });
});

附上有关数据库版本控制的详细信息

类绑定

class Friend {
    // Prototype method
    save() {
        return db.friends.put(this); // 只保存自己的 props.
    }

    // Prototype property
    get age() {
        return moment(Date.now()).diff(this.birthDate, 'years');
    }
}

db.friends.mapToClass(Friend);

新增数据

await db.friends.add({name: "Josephine", age: 21});
//or
await db.friends.bulkAdd([
  {name: "Foo", age: 31},
  {name: "Bar", age: 32}
]);

add()

item要添加的对象
key主键option

注意
将给定对象添加到存储。如果已经存在具有相同主键的对象,则操作将失败,并将使用错误对象调用返回的promise catch()回调。如果操作成功,则返回的promise then()回调将接收对象存储区上的add请求的结果,即插入对象的id。

只有当表使用非入站键时,才必须使用可选的第二个键参数。如果在具有入站密钥的表上提供密钥参数,则操作将失败,返回的承诺将被拒绝。

table.bulkAdd(items, keys?, options?);

item要添加的对象数组
keys (非必填)对应于给定项数组的主键数组
option(非必填){allKeys?: boolean} 如果指定{allKeys:true},则返回值将是结果主键的数组,而不是上一次添加的主键。如果表使用入站键,则可以将选项作为第二个参数提供。API将知道第二个参数是通过类型检查表示选项还是键数组。

何时使用keys参数

  • 如果主键是入站的,则不能提供keys参数。
  • 如果主键是非入站但自动递增的,则keys参数是可选的。
  • 如果主键是非入站和非自动递增的,那么keys参数是必需的。
var db = new Dexie("test");
db.version(1).stores({
    tableWithInboundKeys: "id,x,y,z", // 不能提供 "keys"
    tableWithAutoIncNonInbound: "++,x,y,x", // 可选 "keys"
    tableWithoutInboundKeys: ",x,y,z" // 必需提供 "keys"
});

如果有大量对象要添加到对象存储中,那么bulkAdd()比在循环中执行add()要快一点。

更新数据

await db.friends.put({id: 4, name: "Foo", age: 33});
//or
await db.friends.bulkPut([
    {id: 4, name: "Foo2", age: 34},
    {id: 5, name: "Bar2", age: 44}
]);
await db.friends.update(4, {name: "Bar"});

await db.customers
    .where("age")
    .inAnyRange([ [0, 18], [65, Infinity] ])
    .modify({discount: 0.5});

table.put(item, [key])

item要添加的对象
key主键option

注意
如果已存在具有相同主键的对象,则该对象将替换为给定对象。如果它不存在,则将添加它。

只有当表使用非入站键时,才必须使用可选的第二个键参数。如果在具有入站密钥的表上提供密钥参数,则操作将失败,返回的rejection promise。

table.bulkPut(items, keys?, options?)

item要put的对象数组
keys (非必填)对应于给定项数组的主键数组
option(非必填){allKeys?: boolean} 如果指定{allKeys:true},则返回值将是结果主键的数组,而不是上一次添加的主键。如果表使用入站键,则可以将选项作为第二个参数提供。API将知道第二个参数是通过类型检查表示选项还是键数组。

table.update(key, changes)

key主键
changes对象,该对象包含要更改的每个属性的键路径。

返回值:0/1
Promise 更新记录的数量(如果对象已更新,则为1,否则为0)。结果为0的原因可能是找不到提供的密钥,或者提供的数据与现有数据相同,因此没有更新任何内容。

collection

表示数据库对象的集合。注意,它本身不包含任何对象。相反,它为如何执行数据库查询做了准备。调用返回Promise的方法时将执行查询,例如toArray()、keys()、count()或each()。

Methods(方法)

and()

将基于JS的条件添加到集合

clone()

在进一步操作查询之前克隆该查询(不克隆数据库项)。

count()

获取集合中的项目数

delete()

删除集合中的所有对象

desc()

按降序排序

distinct()

删除具有相同主键的项的重复项

each()

执行查询并为每个项调用函数

eachKey()

对正在使用的索引或主键执行查询,并为每个键调用一个函数

eachPrimaryKey()

对索引执行查询,并为对应于索引的每个主键调用函数。

eachUniqueKey()

对正在使用的索引或主键执行查询,并为每个唯一键调用函数

filter()

筛选对象

first()

获取集合中的第一个项目

keys()

检索包含集合的所有键的数组(索引键或主键取决于where()子句)

last()

获取集合中的最后一项

limit()

将结果限制为给定的项目数

modify()

使用给定的属性或函数修改集合中的所有对象。

offset()

忽略给定偏移量前的N项并返回其余项

or()

逻辑或运算

primaryKeys()

检索包含集合的所有主键的数组

raw()

不要通过阅读挂钩过滤结果

reverse()

颠倒项目顺序。

sortBy()

执行查询并获取结果按给定属性排序的数组

toArray()

执行查询并获取一个数组,其结果按where()子句中使用的索引排序

uniqueKeys()

检索包含集合的所有唯一键(索引键或主键取决于where()子句)的数组

until()

忽略给定筛选器返回true后发生的项。

collection.modify(changes)

changes对象,其中包含要应用于集合中所有对象的更改。调用方可以提供函数而不是对象
  • 如果提供了对象,则每个键都是要更改的属性的路径,每个值都是要设置的新值
  • 如果提供了函数,则将为集合中的每个匹配对象调用给定函数。函数检索对象作为第一个参数。然后,函数可以修改、添加或删除对象的属性。当函数返回时,框架将用对给定对象所做的更改将其更新到数据库。

注意
如果给定的更改是一个对象,则对象中的每个键表示一个keyPath,每个值表示要设置的新值。键路径可以是属性的名称,或者如果包含句点(.),则充当嵌套对象中属性的路径。

如果值是函数,则将为每个对象调用该函数,以便该函数可以修改或删除对象上的任何属性。函数也可以使用this.value=otherObject将对象替换为另一个对象。最后,函数还可以通过delete this.value删除对象;请参见下面的示例。

警告
Safari 10和11有一个问题,如果使用dexie@2,那么这个方法基本上会停止处理使用索引的查询(与dexie where子句一样)。Dexie@3解决了这个问题。德克西594期正在讨论这个问题

删除数据

await db.friends.delete(4);
await db.friends.bulkDelete([1,2,4]);

Table.delete()

主键要删除的对象的主键

Table.bulkDelete()

keys要删除的对象的主键数组
const oneWeekAgo = new Date(Date.now() - 60*60*1000*24*7);

await db.logEntries
    .where('timestamp').below(oneWeekAgo)
    .delete();

Collection.delete()

删除查询出的数据

成功则返回删除的数量Promise()

如果未能删除任何对象或回调函数中发生异常,则整个操作将失败,事务(transaction ())将中止。

如果捕捉到返回的承诺,事务将不会中止,并且您将收到一个包含以下属性的Dexie.mulmodifyerror对象:

failures发生的所有错误的错误对象数组
failedKeys具有失败删除的键的数组。这个数组的顺序与failures相同,因此failures[i]总是表示failedKeys[i]的失败
successCount成功删除的次数。

如果要记录错误,但仍要中止事务,则必须将操作封装在transaction()块中,然后捕获事务。也可以捕获该操作并在catch()子句中调用transaction.abort()。

查询数据

const someFriends = await db.friends
    .where("age").between(20, 25)
    .offset(150).limit(25)
    .toArray();
    
await db.friends
    .where("name").equalsIgnoreCase("josephine")
    .each(friend => {
        console.log("Found Josephine", friend);
    });
    
const abcFriends = await db.friends
    .where("name")
    .startsWithAnyOfIgnoreCase(["a", "b", "c"])
    .toArray();
    
await db.friends
    .where('age')
    .inAnyRange([[0,18], [65, Infinity]])
    .modify({discount: 0.5});

const forbundsKansler = await db.friends
    .where('[firstName+lastName]')
    .equals(["Angela", "Merkel"])
    .first();

在Dexie2.0中,您可以简单地执行上面的查询:

const forbundsKansler = await db.friends.where({
    firstName: "Angela",
    lastName: "Merkel"
}).first();
//or
const forbundsKansler = await db.friends.get({
    firstName: "Angela",
    lastName: "Merkel"
});

此查询等于:从firstName='Angela’按姓氏排序的朋友中选择所有


const angelasSortedByLastName = await db.friends
    .where('[firstName+lastName]')
    .between([["Angela", ""], ["Angela", "\uffff"])
    .toArray()

选择成绩前五的数据

const best5GameSession = await db.gameSessions
    .orderBy("score").reverse()
    .limit(5)
    .toArray();

Table.where()

通过创建WhereClause实例开始筛选对象存储。

// Dexie 1.x and 2.x:
table.where(indexOrPrimaryKey)

// Dexie 2.x only:
table.where(keyPathArray);
table.where({keyPath1: value1, keyPath2: value2, ...});

Parameters

indexOrPrimaryKey: String在Version.stores()中注册的索引或主键的名称。特殊字符串“:id”表示主键。
keyPathArray(Dexie 2.x only)识别要筛选的键槽的字符串。必须与复合索引或主键匹配。
{keyPath1: value1, keyPath2: value2, …}(Dexie 2.x only)过滤准则

WhereClause

表示索引或主键上的筛选器。

Methods(方法)

- above()

返回索引位于给定键之上的对象集合

- aboveOrEqual()

返回索引高于或等于给定键的对象集合

- anyOf()

返回一个对象集合,其中索引等于给定数组中的任何键

- anyOfIgnoreCase()

返回一个对象集合,其中索引与任何给定字符串匹配,忽略大小写差异。

- below()

返回索引低于给定键的对象集合

- belowOrEqual()

返回索引低于或等于给定键的对象集合

- between()

返回索引位于给定边界之间的对象集合

- equals()

返回索引等于给定键的对象集合

- equalsIgnoreCase()

返回一个对象集合,其中索引等于给定的字符串键,忽略大小写差异

- inAnyRange()

返回索引在任何给定范围内的集合。

- noneOf()

返回一个集合,其中index等于给定数组中任何键以外的任何键

- notEqual()

返回一个集合,其中index不等于给定值

- startsWith()

返回一个对象集合,其中索引以给定的字符串键开头

- startsWithAnyOf()

返回一个对象集合,其中索引以任何给定字符串开头

- startsWithIgnoreCase()

返回一个对象集合,其中索引以给定的字符串键开头,忽略大小写差异

- startsWithAnyOfIgnoreCase()

返回一个对象集合,其中索引以任何给定字符串开头,忽略大小写差异

存储二进制数据

var db = new Dexie("MyImgDb");
db.version(1).stores({
    friends: "name"
});

// 下载并存储图片
async function downloadAndStoreImage() {
    const res = await fetch("some-url-to-an-image.png");
    const blob = await res.blob();
    await db.friends.put({
        name: "David",
        image: blob
    });
}

索引二进制数据(indexDB 2.0)

IndexedDB 2.0包含对索引二进制数据的支持。Chrome和Safari以及部分Firefox支持这个规范(Firefox在使用二进制主键时有一个bug,但是在使用二进制索引时效果很好)。

var db = new Dexie("MyImgDb");
db.version(1).stores({
    friends: "id, name" // 使用二进制UUID作为id
});

// IndexedDB 2.0 允许索引ArrayBuffer和XXXArray
// (类型化数组,但不是blob)
async function playWithBinaryPrimKey() {
    // 存储二进制数据:
    await db.friends.put({
        id: new Uint8Array([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]),
        name: "David"
    });

    // 通过二进制搜索检索
    const friend = await db.friends.get(
        new Uint8Array([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]));

    if (friend) {
        console.log(`Found friend: ${friend.name}`);
    } else {
        console.log(`Friend not found`);
    }
}

事务(Transaction)

一系列操作步骤之中,只要有一步失败,整个事务就都取消,数据库回滚到事务发生之前的状态,不存在只改写一部分数据的情况。

await db.transaction('rw', [db.friends], async () => {
  const friend = await db.friends.get(1);
  ++friend.age;
  await db.friends.put(friend);
});

  • 7
    点赞
  • 1
    评论
  • 19
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值