Skip to content

C++ 数据库、表、事务

qiuwenchen edited this page Mar 7, 2024 · 3 revisions

本文主要介绍 WCDB C++ 的三个基础概念:数据库 - WCDB::Database、表 - WCDB::Table 和 事务。

数据库

WCDB::Database 是 WCDB C++ 中最基础的类,几乎所有操作都由该类发起。

初始化

WCDB::Database 可以通过文件路径来创建,而且 WCDB 会自动创建中间路径上的文件夹:

WCDB::Database database("~/Intermediate/Directories/Will/Be/Created/sample.db");

标签

通过设置标签,可以区分不同的数据库。

int myTag1 = 1;
WCDB::Database database1(path1);
database1.tag = myTag1;

int myTag2 = 2;
WCDB::Database database2(path2);
database2.tag = myTag2;

Tag不会实际保存到文件中,但因为同一个路径的数据库会共享同一份底层内存数据,所以同一个路径的WCDB::Database会共享同一个 tag

printf("%ld", database1.tag); // 输出 1

WCDB::Database anotherDatabase1(path1);
printf("%ld", anotherDatabase1.tag); // 输出 1

打开数据库

延迟初始化是 WCDB 的原则之一,绝大部分数据只会在需要用到时才创建并初始化。数据库的打开就是其中一个例子。

数据库会在第一次进行操作时,自动打开并初始化。开发者不需要手动调用

WCDB::Database database(filePath);
printf("%d", database.isOpened()); // 输出 0
ret = database.createTable<Sample>("sampleTable");
printf("%d", database.isOpened()); // 输出 1

同时,也可以通过 canOpen 接口测试数据库能否正常打开。

WCDB::Database database1(filePath);
printf("%d", database1.isOpened()); // 输出 0
printf("%d", database1.canOpen());  // 输出 1。仅当数据库无法打开时,如路径无法创建等,该接口会返回 NO
printf("%d", database.isOpened());  // 输出 1
WCDB::Database database2(filePath);
printf("%d", database2.isOpened()); // 输出 1。WCDB 同一路径的数据库共享数据和状态等。

关闭数据库

与打开数据库相对应,关闭数据库一般情况下也不需要开发者手动调用。当某个路径的WCDB::Database已经全部析构时,数据库会自动关闭,并回收内存。

{
  WCDB::Database database1(filePath);
  ret = database1.createTable<Sample>("sampleTable");// 数据库此时会被自动打开
  printf("%d", database1.isOpened()); // 输出 1
}// 作用域结束,database1 析构、关闭数据库并回收内存
WCDB::Database database2(filePath);
printf("%d", database2.isOpened()); // 输出 0。
WCDB::Database database1(filePath);
{
  WCDB::Database database2(filePath);
  ret = database2.createTable<Sample>("sampleTable");// 数据库此时会被自动打开
  printf("%d", database2.isOpened()); // 输出 1
}// 作用域结束,database2 析构,但 database1 仍持有该路径的数据库,因此不会被关闭。
printf("%d", database1.isOpened()); // 输出 1。

同时,也可以调用 close 接口,手动关闭数据库。

WCDB::Database database(filePath);
printf("%d", database.canOpen());  // 输出 1
printf("%d", database.isOpened()); // 输出 1
database.close();
printf("%d", database.isOpened()); // 输出 0

WCDB 也提供了 blockadeunblockadeisBlockaded 接口用于分步执行关闭数据库操作,可参考[相关接口文档][Swift-API-Reference-Close-OnClosed]

关闭数据库与线程安全

某些情况下,开发者需要确保数据库完全关闭后才能进行操作,如将数据库整体zip打包发送。

数据库是二进制文件,zip打包的过程中若数据发生了变化,则打包后的文件数据可能会不完整、损坏。因此,WCDB 支持往 close 接口中传一个回调,数据库关闭成功之后会调用。

database.close([&]() {
    //执行独占的数据库操作,如zip压缩打包
});

close的回调范围内,可确保数据库完全关闭,不会有其他线程的数据访问、操作数据库,因此可以安全地操作文件。

内存回收

purge 接口用于回收暂不使用的内存,主要是关闭闲置的数据连接。

// 回收 database 数据库中暂不使用的内存
database.purge();
// 回收所有已创建的数据库中暂不使用的内存
WCDB::Database::purgeAll();

在 iOS 平台上,当内存不足、收到系统警告时,WCDB 会自动调用 WCDB::Database::purgeAll() 接口以减少内存占用。

数据库的文件操作

// 获取所有与该数据库相关的文件路径
std::list<WCDB::StringView> paths = database.getPaths();
// 获取所有与该数据库相关的文件占用的大小
database.close([&](){
    // 数据库未关闭状态下也可获取文件大小,但不够准确,开发者可自行选择是否关闭
    printf("%lu", database.getFilesSize().value());
});
// 删除所有与该数据库相关的文件
database.removeFiles();
// 将所有与该数据库相关的文件移动到另一个目录
database.moveFiles(otherDirectory);

WCDB::Table 指代数据库中的一个表。可以通过 getTable 接口获取。

//注意,即便表不实际存在也会返回,推荐在执行建表语句之后获取 Table
WCDB::Table<Sample> table = database.getTable<Sample>("sampleTable");

WCDB::Table相当于指定了表名和模型绑定类的 WCDB::Database,其实只是 WCDB::Database的简化版,增删查改中提到的所有接口WCDB::Table都具备,而且这些接口调用时都不需要再传表名和类型参数,下面是使用示例:

// 返回值需指定为 [Sample] 类型以匹配范型
WCDB::OptionalValueArray<Sample> allObjectsFromDatabase = database.getAllObjects<Sample>("sampleTable");

// table 已经指定了表名和模型绑定的类,因此可以直接获取
WCDB::OptionalValueArray<Sample> allObjectsFromTable = table.getAllObjects();

因为执行数据读写时WCDB::Table使用起来比WCDB::Database更加简洁,而且也有利于以表为单位来管理数据读写逻辑,所以我们推荐尽量使用WCDB::Table来进行数据读写

事务

事务一般用于 提升性能保证数据原子性。可以使用WCDB::Database 发起事务。

ret = database.runTransaction([&](WCDB::Handle& handle) {
    bool ret = table.insertObjects(object);
    ret &= table.deleteObjects(WCDB_FIELD(Sample::identifier) == 1);
    return ret;
});

事务回调中的WCDB::Handle对象暂时忽略,后面在高级接口中会有介绍。

性能

事务提升性能的实质是批量处理。

std::list<Sample> objects;
for(int i = 0; i < 100000; i++) {
    Sample obj;
    obj.identifier = i;
    objects.push_back(obj);
}

// 单独插入,效率很差
for(const Sample& obj : objects) {
    table.insertObjects(obj);
}

// 事务插入,性能较好
ret = database.runTransaction([&](WCDB::Handle& handle) {
    for(const Sample& obj : objects) {
        ret &= table.insertObjects(obj);
    }
    return ret;
});

// insertObjects: 接口内置了事务,并对批量数据做了针对性的优化,性能更好
table.insertObjects(objects);

原子性

试考虑以下代码:

std::async(std::launch::async, [&](){
    table.deleteObjects();
});
table.insertObjects(object);
WCDB::OptionalValue count = table.selectValue(Sample::allFields().count());
printf("%lld", count.value().intValue());// 可能输出 0 或 1

Sample::allFields().count()表示SQL语法中的count(*),这个在语言集成查询中会有进一步介绍。

在多线程下,删除操作发生的时机是不确定的。倘若它发生在 插入完成之后取出数据之前 的瞬间,则 selectValue 无法取出刚才插入的数据,且这种多线程低概率的 bug 是很难查的。

而事务可以保证一段操作的原子性:

std::async(std::launch::async, [&](){
    table.deleteObjects();
});
ret = database.runTransaction([&](WCDB::Handle& handle) {
    ret &= table.insertObjects(object);
    WCDB::OptionalValue count = table.selectValue(Sample::allFields().count());
    printf("%lld", count.value().intValue());// 输出 1
    return ret;
});

执行事务

WCDB 提供了三种事务,普通事务、嵌入事务和可中断事务。

// 普通事务
ret = database.runTransaction([&](WCDB::Handle& handle) {
    return table.insertObjects(object);
});
// 普通事务支持嵌套调用
ret = database.runTransaction([&](WCDB::Handle& handle) {
    return database.runTransaction([&](WCDB::Handle& handle) {
    	return table.insertObjects(object);
		});
});

普通事务可以通过返回值控制提交或者回滚事务,返回true提交事务,返回false则回滚事务。

注意回滚事务时,事务调用本身的返回值也会为false。

普通事务可以互相嵌套执行,嵌套时支持只回滚嵌套事务中的局部更改内容。

insertObjectsinsertOrReplaceObjectsinsertOrIgnoreObjectscreateTable 等 WCDB 自带的接口都使用了嵌入事务

关于可中断事务,因为使用较为复杂,后面在高级接口中会有介绍。

Clone this wiki locally