likes
comments
collection

Flutter 的键值存储数据库

作者站长头像
站长
· 阅读数 214

Flutter 键值存储数据库

键值存储是开发中十分常见的需求,在Flutter开发中,一般使用 shared_preferences 插件来实现。shared_preferences 本质上就是将键值对保存到一个XML文件中进行持久化。而shared_preferences 实际上存在一定缺陷,譬如其性能较差,不适合处理大量数据,不能创建新的XML文件,所有数据存在同一个文件中。除此外,还有其他一些持久化方案,如SQLite、Hive等。SQLite是关系型数据库,使用起来相对繁琐;而Hive是用Dart实现的一个轻量级键值对数据库,它使用简单,但同样性能较差,而且在储存大量数据时,更加耗费内存,因为它是一次性将所有数据读取到内存中,这在移动端不是很可取。当然,如果你只是用来存储几个简单的配置项数据,那也够用了。

unqlite

以上概述了一下Flutter的键值持久化方案,那么接下来就隆介绍一下本文推荐的Flutter键值存储方案——一个轻量级嵌入式nosql数据库unqlite!

unqlite是一个嵌入式的数据库,它实现了一个独立的、无服务器、零配置、事务性的nosql数据库引擎。它是一个文档存储数据库,类似于MongoDB, Redis, CouchDB等,同时也是一个标准的key/value存储数据库,类似于BerkeleyDB, LevelDB, 等。

  1. 首先unqlite是一个无服务器的数据库,这意味着它的通讯是直接读取数据库文件的(从磁盘读取),没有中间服务器进程。
  2. 然后unqlite也不像其他服务器一样,需要安装、配置以及权限管理等。它是直接嵌入到我们的程序中的
  3. unqlite是一个单一数据库文件,它的数据库是一个普通的磁盘文件。这就意味着你可以把它随意的复制或备份到其他地方。
  4. unqlite文件格式是跨平台的。在一台机器上编写的数据库文件可以复制到具有不同体系结构的不同机器上并在其上使用。
  5. unqlite是一个标准的键/值存储数据库,类似于BerkeleyDB,Tokyo Cabinet,LevelDB等,但具有丰富的功能集,包括对事务的支持(ACID)。在KV存储下,键和值都被视为简单的字节数组,因此内容可以是ASCII字符串,二进制blob甚至磁盘文件。

简单概括,unqlite是一个标准C语言实现的轻量级、高性能nosql数据库,经过编译后,它只有几百KB大小,可以嵌入到我们的App中,它与SQLite数据库类似,区别在于一个是基于SQL的关系型数据库,一个是NoSql数据库。

unqlite_flutter

既然unqlite是一个C语言编写的数据库,那么Flutter开发如何使用它呢?

随着Dart版本的不断迭代,Dart语言的FFI接口逐渐成熟,FFI类似于Java的JNI,赋予了Dart语言直接调用C语言的能力。有了这种能力,那么基于C/C++开发的优秀的高性能的库,都可以用于Flutter开发中,可以说是C/C++生态为我所用!想学习Flutter的FFI开发,可以参考博主的B站视频 Dart FFI开发入门 以及 程序员的C

当然,这里我已经完成了Dart FFI调用的封装,可以直接依赖我开发好的插件库——unqlite_flutter

目前已完成的功能:

  • key-value 储存
  • JSON 文档储存

快速上手

简单键值对存储

添加依赖:

  unqlite: ^0.0.3
  unqlite_flutter: ^0.0.3

示例代码:

// 创建或打开一个数据库
UnQLite db = UnQLite.open("${appDocDir.path}/test.db");

// 保存键值对
db.store("name", "Alex");
db.store("age", 18);
db.store(19, "haha");

// 通过指定泛型获取值
debugPrint(db.fetch<String>("name"));
debugPrint('${db.fetch<int>("age")}');
debugPrint(db.fetch<String>(19));

// 另一种获取值的方法
db.fetchCallback<int>("age", (val) {
    debugPrint('age=$val');
});

当然,还有另一种获取数据的方式,可能比fetch更快:

var cursor = db.cursor();
cursor.seek('name');
debugPrint('=> ${cursor.key} => ${cursor.value}');

你还可以使用事务。在一个事务中,如果发生异常,你可以回滚所有操作:

    var trans = db.transaction().begin();
    try {
      for (var i = 0; i < 100000; i++) {
        if (i == 10) {
          // 这里我们抛出一个异常
          throw Exception('test');
        }
        db.store("transaction_$i", "here is a transaction_$i");
      }
      trans.commit();
    } catch (e) {
      // 处理异常,手动回滚事务
      trans.rollback();
    }

以上示例中,我们保存十万个数据,一旦中间发生了什么异常,我们执行回滚操作,则前面所有的保存的数据将被取消。

你还可以使用迭代器来进行遍历:

for (var entry in db.cursor()) {
  var content = '${entry.key} => ${entry.value}';
  debugPrint(content);
}

JSON

处理JSON文档

    UnQLite db = UnQLite.open("${appDocDir.path}/test2.db");
    var users = db.collection("users");
    // Create a collection
    users.create();

    // 保存JSON
    users.store(jsonDecode('''
{
        "title": "test json string",
        "author": [
                "arcticfox1919"
        ],
        "year": 2022,
        "like": "flutter"
}
    '''));

    users.store({'name': 'Mickey', 'age': 17});
    users.store([
      {'name': 'Alice', 'age': 18},
      {'name': 'Bruce', 'age': 19},
      {'name': 'Charlie', 'age': 20},
    ]);

    // 获取全部数据
    print(users.all());
    // print(users.fetch(0));
    // print(users.fetch(1));
    // print(users.fetch(2));
    // print(users.errorLog());

    print(users.creationDate());
    print(users.len());
    print(users.fetchCurrent());

    // 删除全部
    users.drop();
    db.close();

为什么你应该使用unqlite_flutter?

  • 比 Hive 更快,占用更少的内存
  • 可以支持 JSON 文档

它有什么缺点? 因为使用了dart ffi,所以不能支持Flutter web。

下面是一些性能测试数据,这里没有列出内存占用的百分比,但可以肯定 unqlite 比Hive 使用了更少的内存:

UnQLite:

UnQLite init:1 ms
write 100,000 entries :611 ms
fetch 100,000 entries :370 ms
seek  100,000 entries :215 ms
iterate 100,000 entries :225 ms
transaction rollback :39 ms

Hive:

Hive init:48 ms
put 100,000 entries :807 ms
get 100,000 entries :290 ms

这是用于测试的代码,两者都在同一部真机上以profile模式运行:

    testUnQLite() async {
    var appDocDir = await getApplicationDocumentsDirectory();
    final start = DateTime.now().millisecondsSinceEpoch;
    UnQLite db = UnQLite.open("${appDocDir.path}/test.db");
    final t1 = DateTime.now().millisecondsSinceEpoch;

    for (var i = 0; i < 100000; i++) {
      db.store("my_key_$i", "Here is a value for testing—$i");
    }

    final t2 = DateTime.now().millisecondsSinceEpoch;
    for (var i = 0; i < 100000; i++) {
      var r = db.fetch<String>("my_key_$i");
      // debugPrint("fetch :$r");
    }
    final t3 = DateTime.now().millisecondsSinceEpoch;
    
    var cursor = db.cursor();
    for (var i = 0; i < 100000; i++) {
      cursor.seek('my_key_$i');
      // debugPrint('=> ${cursor.key} => ${cursor.value}');
    }
    final t4 = DateTime.now().millisecondsSinceEpoch;

    var count = 0;
    for (var entry in db.cursor()) {
      count++;
      var content = '${entry.key} => ${entry.value}';
      // debugPrint(content);
    }
    print('count => $count');
    final t5 = DateTime.now().millisecondsSinceEpoch;

    var trans = db.transaction().begin();
    try {
      for (var i = 0; i < 100000; i++) {
        if (i == 10) {
          throw Exception('test');
        }
        db.store("transaction_$i", "here is a transaction_$i");
      }
      trans.commit();
    } catch (e) {
      trans.rollback();
    }
    final t6 = DateTime.now().millisecondsSinceEpoch;

    debugPrint("UnQLite init:${t1-start} ms");
    debugPrint("write 100,000 entries :${t2-t1} ms");
    debugPrint("fetch 100,000 entries :${t3-t2} ms");
    debugPrint("seek  100,000 entries :${t4-t3} ms");
    debugPrint("iterate 100,000 entries :${t5-t4} ms");
    debugPrint("transaction rollback :${t6-t5} ms");
    db.close();
  }

  testHive() async {
    var appDocDir = await getApplicationDocumentsDirectory();
    var path = appDocDir.path;
    final start = DateTime.now().millisecondsSinceEpoch;
    Hive.init(path);
    var box = await Hive.openBox('testBox');

    final t1 = DateTime.now().millisecondsSinceEpoch;

    for (var i = 0; i < 100000; i++) {
      box.put("my_key_$i", "here is a transaction_$i");
    }

    final t2 = DateTime.now().millisecondsSinceEpoch;

    for (var i = 0; i < 100000; i++) {
      var name = box.get('my_key_$i');
    }

    final t3 = DateTime.now().millisecondsSinceEpoch;

    box.close();
    debugPrint("Hive init:${t1-start} ms");
    debugPrint("put 100,000 entries :${t2-t1} ms");
    debugPrint("get 100,000 entries :${t3-t2} ms");
  }