WCDB 学习笔记

2,142 阅读9分钟

WCDB C++ 学习笔记

WCDB 项目主页:github.com/Tencent/wcd…

WCDB 官方教程:github.com/Tencent/wcd…

WCDB 非常吸引我的两大特色点:

1、全文索引(中文分词)

2、可中断事务

PC 端开发,涉及全文索引问题时,常见的解决方案有:SolrSQLite3 FTS5CLucence 等。但使用他们都有一个问题,就是中文分词,需要消耗一定的人力去搞。而我并不想花太多精力去做本该搜索引擎做的事。这也是为什么选择 WCDB 的原因。

另一个原因就是 WCDB 提供了可中断事务,这个功能好啊,解决了界面线程访问数据库可能卡死的问题。

于是萌生了学习下 WCDB 的念头,在看完官方教程后,遂写下此笔记。

一、数据类型

WCDB 中定义了一些自己的数据类型,本节逻列了这些数据类型及其含义。

基础数据类型

类型描述
WCDB::UnsafeStringView同 C++ 17 std::string_view ,可以简单理解为更高性能的 std::string
WCDB::StringView继承至 UnsafeStringView
WCDB::StringViewSet继承至 public std::set<StringView>
WCDB::StringViewMap<T>继承至 std::map<StringView, T>
WCDB::UnsafeData二进制数据,可以理解为存储了 (unsigned char* buffer, size_t len)
WCDB::Data继承至 UnsafeData

SQLite 数据

类型描述
WCDB::Value<T>对应 SQLite 值,类型有:Null、Integer、Float、Text、BLOB
WCDB::ValueArray<T>继承至:std::vector <T>
WCDB::OneRowValuetypedef ValueArray<Value> OneRowValue
WCDB::OneColumnValuetypedef ValueArray<Value> OneColumnValue
WCDB::MultiRowsValuetypedef ValueArray <ValueArray<Value>> MultiRowsValue

SQLite Optional 数据

类型描述
WCDB::Optional<T>同 C++ 17 std::optional ,相当于返回值附带了个bool来标识该返回值是否有效
WCDB::OptionalValueArray<T>using OptionalValueArray = Optional<ValueArray<ValueType>>
WCDB::OptionalValuetypedef Optional<Value> OptionalValue
WCDB::OptionalOneRowtypedef OptionalValueArray<Value> OptionalOneRow
WCDB::OptionalOneColumntypedef OptionalValueArray<Value> OptionalOneColumn
WCDB::OptionalMultiRowsOptionalValueArray<ValueArray<Value>> OptionalMultiRows

SQLite 多表数据

类型描述
WCDB::MultiObject存储多表查询结果中的一整行数据,该行数据由多张表的多个字段组成
WCDB::OptionalMultiObjecttypedef Optional<MultiObject> OptionalMultiObject
WCDB::OptionalMultiObjectArraytypedef OptionalValueArray<MultiObject> OptionalMultiObjectArray

二、类继承关系图

WCDB类图.jpg

三、SQL 语句拼接

WCDB 中定义了一套语言集成查询(WCDB Integrated Language Query,简称 WINQ

可以简单理解为:用 C++ 语法拼接 SQL 语句

先看一个 WCDB 的示例:

WCDB::StatementSelect select = WCDB::StatementSelect()
    .select(WCDB::Column("column_name"))
    .from("table_name");

printf("%s", select.getDescription().data());// 输出 "SELECT column_name FROM table_name"

想用 C++ 拼接出 SQL 语句,需要支持全部的 SQL 语法。WCDB 将这些语法封装成了两大块:StatementIdentifier

1、Statement: 完整的 SQL 语句。

2、Identifier: 主要封装小的语法块,一条完整的 SQL 由多个不同的 identifier 组成。如:Column、GROUP BY、ORDER BY 等。

示例代码中的 WCDB::StatementSelectWCDB::Column("column_name") 分别对应了 Statement 和 Identifier。

其类继承关系图如下所示:

wcdb-statement-syntax-classes.png

3.1. Statement 系列封装

SQLite 一共 27 种 Statement ,外加一个 EXPLAIN 语句。下表是 WCDB 对它们的封装:

编号WCDB C++ 类SQLite Statement
1WCDB::StatementAlterTablealter-table-stmt
2WCDB::StatementAnalyzeanalyze-stmt
3WCDB::StatementAttachattach-stmt
4WCDB::StatementBeginbegin-stmt
5WCDB::StatementCommitcommit-stmt
6WCDB::StatementCreateIndexcreate-index-stmt
7WCDB::StatementCreateTablecreate-table-stmt
8WCDB::StatementCreateTriggercreate-trigger-stmt
9WCDB::StatementCreateViewcreate-view-stmt
10WCDB::StatementCreateVirtualTablecreate-virtual-table-stmt
11WCDB::StatementDeletedelete-stmt
12delete-stmt-limited
13WCDB::StatementDetachdetach-stmt
14WCDB::StatementDropIndexdrop-index-stmt
15WCDB::StatementDropTabledrop-table-stmt
16WCDB::StatementDropTriggerdrop-trigger-stmt
17WCDB::StatementDropViewdrop-view-stmt
18WCDB::StatementInsertinsert-stmt
19WCDB::StatementPragmapragma-stmt
20WCDB::StatementReindexreindex-stmt
21WCDB::StatementReleaserelease-stmt
22WCDB::StatementRollbackrollback-stmt
23WCDB::StatementSavepointsavepoint-stmt
24WCDB::StatementSelectselect-stmt
25WCDB::StatementUpdateupdate-stmt
26update-stmt-limited
27WCDB::StatementVacuumvacuum-stmt
--
28WCDB::StatementExplainEXPLAIN QUERY PLAN [SQLite Query]
29WCDB::Statement上述所有 StatementXXX 的基类

3.2. Identifier 系列封装

除 Statement 外,WCDB 将其余的 SQL 语法,封装成了 Identifier 系列。共23个类,如下所示:

编号WCDB C++ 类说明SQL 示例
1WCDB::BindParameter绑定参数VALUES( ?, ?, ? )
2WCDB::Column更新列、插入列INSERT INTO table(column_name) VALUES('hello')
3WCDB::ColumnConstraintCONSTRAINT 列约束
4WCDB::ColumnDef创建列、修改列CREATE TABLE table(column_name TEXT);
5WCDB::CommonTableExpression
6WCDB::Expression表达式
7WCDB::Filter
8WCDB::ForeignKey外键
9WCDB::FrameSpec
10WCDB::IndexedColumn索引列ORDER BY name COLLATE NOCASE DESC
11WCDB::Join
12WCDB::JoinConstraint
13WCDB::LiteralValue
14WCDB::OrderingTerm排序
15WCDB::Pragma
16WCDB::QualifiedTable
17WCDB::RaiseFunction
18WCDB::ResultColumn选择列SELECT column_name FROM table
19WCDB::Schema
20WCDB::TableConstraintCONSTRAINT 表约束
21WCDB::TableOrSubquery表名或子查询
22WCDB::Upsert
23WCDB::WindowDef

多个 Identifier 使用 SyntaxList 容器存储,其命名方式为单词复数形式,其伪代码如下:


template<typename T>
class SyntaxList : public std::list<T> {
	// ...
};

// 定义一系列类型:
typedef SyntaxList<IndexedColumn> IndexedColumns;
typedef SyntaxList<Expression> Expressions;
typedef SyntaxList<Column> Columns;
typedef SyntaxList<ColumnConstraint> ColumnConstraints;
typedef SyntaxList<TableOrSubquery> TablesOrSubqueries;
typedef SyntaxList<OrderingTerm> OrderingTerms;
typedef SyntaxList<TableConstraint> TableConstraints;
typedef SyntaxList<ModuleArgument> ModuleArguments;
typedef SyntaxList<CommonTableExpression> CommonTableExpressions;
typedef SyntaxList<ResultColumn> ResultColumns;
typedef SyntaxList<ColumnDef> ColumnDefs;
typedef SyntaxList<BindParameter> BindParameters;
typedef SyntaxList<LiteralValue> LiteralValues;

3.2.1. WCDB_FIELD 宏

针对 WCDB::Column 和 WCDB::ResultColumn 分别有一个子类:WCDB::FieldWCDB::ResultField 。这两个类主要实现结构体成员到 SQL 语法的映射。在使用上,可以直接使用宏 WCDB_FIELD

// WCDB_FIELD 宏定义
#define WCDB_FIELD(memberPointer) WCDB::Field(&memberPointer)

// 示例-1
const WCDB::Field& field = WCDB_FIELD(Sample::identifier);

// 示例-2
WCDB::StatementSelect select =
    WCDB::StatementSelect()
    .select(WCDB_FIELD(Sample::identifier))
    .from("sampleTable");
printf("%s", select.getDescription().data()); // 输出 "SELECT identifier FROM sampleTable"

四、SQL 语句执行

SQLite 官方提供的 C 语言接口,有以下两种执行方式:

// 方式1:sqlite3_exec
sqlite3_exec(db, "SELECT * FROM table", nullptr, nullptr, nullptr);

// 方式2:sqlite3_prepare
sqlite3_stmt* stmt = nullptr;
sqlite3_prepare(db, "SELECT * FROM table", -1, &stmt, nullptr);
sqlite3_step(s);
sqlite3_finalize(s);

WCDB 也提供了两种执行方式:

1、通过WCDB::Database提供的接口执行,简单方便。

2、通过WCDB::Table提供的接口执行,简单方便,更直接。

3、通过 WCDB::Handle 提供的接口执行,性能更佳。(执有 sqlite3_stmt 句柄)

WCDB::Database db("test.db");
WCDB::Handle handle = db.getHandle();
WCDB::Table<T> table1 = db.getTable<T>("table_name");
WCDB::Table<T> table2 = handle.getTable<T>("table_name");

db.insertObjects<T>(t, "table_name"); // Handle与Database相同
table1.insertObjects(t); // 不需要指定模板参数和表名
handle.insertObjects<T>(t, "table_name"); // Handle与Database相同
函数名说明WCDB::DatabaseWCDB::HandleWCDB::Table
createTable创建表createTable("table")同 WCDB::Database:x: 无
insertObjectsinsertObjects(objs,"table");同 WCDB::DatabaseinsertObjects(objs);
deleteObjectsdeleteObjects(...)同 WCDB::DatabasedeleteObjects(...)
updateObjectupdateObject(...)同 WCDB::DatabaseupdateObject(...)
getAllObjectsgetAllObjects(...)同 WCDB::DatabasegetAllObjects(...)
prepareInsertprepareInsert()同 WCDB::DatabaseprepareInsert()
prepareDeleteprepareDelete()同 WCDB::DatabaseprepareDelete()
prepareUpdateprepareUpdate()同 WCDB::DatabaseprepareUpdate()
prepareSelectprepareSelect()同 WCDB::DatabaseprepareSelect()
prepareMultiSelect联表查prepareMultiSelect()同 WCDB::Database:x: 无
preparesqlite3_stmt:x: 无handle持有的主stmt:x: 无
getOrCreatePreparedStatementsqlite3_stmt:x: 无handle持有的辅stmt:x: 无

WCDB::Handle 内部维护了一张 sqlite3_stmt 表,记录了两类 stmt :内置和独立

WCDB::Handle handle = db.getHandle();

// 内置stmt用法
handle.prepare(...);
handle.bindText(...);
handle.step();
handle.finalize();

// 独立stmt用法
WCDB::OptionalPreparedStatement stmt = handle.getOrCreatePreparedStatement(...);
stmt->bindText(...);
stmt->step();
handle.finalizeAllStatement(); // 由Handle管理其生命周期

可以看出:

1、内置 stmt:由 WCDB::Handle 提供接口,prepare 一条语句,会释放掉这前的语句

2、独立 stmt:由 WCDB::OptionalPreparedStatement 提供接口,如果语句相同,会复用同一句柄,由 WCDB::Handle 管理生命周期

4.1. 在 WCDB::Database 下执行

WCDB::Database 封装了很多简便的接口,如:

// 创建表格
database.createTable<Sample>("sampleTable");

// 插入操作
Sample object(1, "name");
database.insertObjects<Sample>(object, "sampleTable");

// 删除操作
database.deleteObjects("sampleTable", WCDB_FIELD(Sample::identifier) > 1);

// 更新操作
Sample object;
object.content = "update";
database.updateObject(object, WCDB_FIELD(Sample::content), "sampleTable",
    WCDB_FIELD(Sample::identifier) > 1 && WCDB_FIELD(Sample::content).notNull()
);

// 查询操作
WCDB::OptionalValueArray<Sample> objects = database.getAllObjects<Sample>("sampleTable",
    WCDB_FIELD(Sample::identifier) < 5 || WCDB_FIELD(Sample::identifier) > 10);

同时 WCDB::Database 也内置了 5 个简单的 prepare 函数如下:

WCDB::Insert<Sample> insert = database.prepareInsert<Sample>(); // 增
WCDB::Delete del = database.prepareDelete(); // 删
WCDB::Update<Sample> update = database.prepareUpdate<Sample>(); // 改
WCDB::Select<Sample> select1 = database.prepareSelect<Sample>(); // 查
WCDB::MultiSelect select2 = database.prepareMultiSelect(); // 多表查

auto select = database.prepareSelect<Sample>().fromTable("sampleTable");
WCDB::OptionalValueArray<Sample> allObjects = select.where(WCDB_FIELD(Sample::identifier) > 1).limit(10).allObjects();

auto del = database.prepareDelete().where(WCDB_FIELD(Sample::identifier) != 0);
del.execute();

// 相当于执行 SQL: 
// SELECT
//   sampleTable.identifier,
//   sampleTable.content,
//   sampleTableMulti.identifier,
//   sampleTableMulti.content
// FROM sampleTable, sampleTableMulti
// WHERE sampleTable.identifier = sampleTableMulti.identifier
WCDB::MultiSelect multiSelect = database.prepareMultiSelect().onResultFields({
    WCDB_FIELD(Sample::identifier).table("sampleTable"),
    WCDB_FIELD(Sample::content).table("sampleTable"),
    WCDB_FIELD(SampleMulti::identifier).table("sampleTableMulti"),
    WCDB_FIELD(SampleMulti::content).table("sampleTableMulti")
	}).fromTables({"sampleTable", "sampleTableMulti"})
    .where(WCDB_FIELD(Sample::identifier).table("sampleTable") ==
    WCDB_FIELD(SampleMulti::identifier).table("sampleTableMulti")
);

//读取两个表格的结果
WCDB::OptionalMultiObject multiObject = multiSelect.firstMultiObject();
Sample sample = multiObject.value().objectAtTable<Sample>("sampleTable").value();
SampleMulti multiSample = multiObject.value().objectAtTable<SampleMulti>("sampleTableMulti").value();

WCDB::Database 还内置了 4 个 get 接口,用于直接从 Statement 中读取结果

OptionalOneRow getOneRowFromStatement(const Statement &statement);
OptionalOneRow getOneColumnFromStatement(const Statement &statement, int index = 0);
OptionalMultiRows getAllRowsFromStatement(const Statement &statement);
OptionalValue getValueFromStatement(const Statement &statement, int index = 0);

// 获取所有内容
WCDB::OptionalMultiRows allRows = database.getAllRowsFromStatement(
    WCDB::StatementSelect().select(Sample::allFields()).from("sampleTable")
);

// 获取第二行
WCDB::OptionalOneRow secondRow = database.getOneRowFromStatement(
    WCDB::StatementSelect().select(Sample::allFields()).from("sampleTable").offset(1)
);

// 获取第二行 content 列的值
WCDB::OptionalOneColumn contentColumn = database.getOneColumnFromStatement(
    WCDB::StatementSelect().select(WCDB_FIELD(Sample::content)).from("sampleTable")
);

// 获取 identifier 的最大值
WCDB::OptionalValue maxId = database.getValueFromStatement(
    WCDB::StatementSelect().select(WCDB_FIELD(Sample::identifier).max()).from("sampleTable")
);

// 获取不同的 content 数
WCDB::OptionalValue distinctContentCount = database.getValueFromStatement(
    WCDB::StatementSelect().select(WCDB_FIELD(Sample::content).count().distinct()).from("sampleTable")
);

4.2. 在 WCDB::PreparedStatement 下执行

当我们需要操作大量的数据量,为了更好的性能,我们需要用到 sqlite3_stmt 句柄。而 WCDB::PreparedStatement 就是对其的封装。以下是关联的几个类:

编号WCDB C++ 类描述
1WCDB::StatementOperation接口类,定义了操作 sqlite3_stmt 的通用接口
2WCDB::PreparedStatement继承 StatementOperation 接口,持有 sqlite3_stmt 句柄
3WCDB::OptionalPreparedStatementtypedef Optional<PreparedStatement> OptionalPreparedStatement

示例代码:

// 获取handle
WCDB::Handle handle = database.getHandle();

ret &= handle.createTable<Sample>("sampleTable");
ret &= handle.createTable<Sample>("newSampleTable");
// prepare statement
WCDB::OptionalPreparedStatement selectSTMT =
    handle.getOrCreatePreparedStatement(WCDB::StatementSelect()
                                        .select(Sample::allFields())
                                        .from("sampleTable"));
WCDB::OptionalPreparedStatement insertSTMT =
    handle.getOrCreatePreparedStatement(WCDB::StatementInsert()
                                        .insertIntoTable("newSampleTable")
                                        .columns(Sample::allFields())
                                        .values(WCDB::BindParameter::bindParameters(Sample::allFields().size())));

while (selectSTMT->step() && !selectSTMT->done()) {
    //1. 可以直接读取对象,会逐个属性来调用sqlite3_column系列接口来读取数据,并赋值给对象
    Sample obj = selectSTMT->extractOneObject<Sample>(Sample::allFields());

    //2. 也可以逐个字段读取,更高效一点
    obj = Sample();
    obj.identifier = selectSTMT->getInteger(0);
    obj.content = selectSTMT->getText(1);

    // 先 reset,其实是sqlite3_reset函数的封装。
    insertSTMT->reset();
    obj.identifier += 10000;

    //1. 可以直接使用对象来bind,会逐个属性调用sqlite3_bind系列接口
    insertSTMT->bindObject(obj, Sample::allFields());

    //2. 也可以逐个字段bind,更高效一点
    insertSTMT->bindInteger(obj.identifier, 1);
    insertSTMT->bindText(obj.content, 2);

    // step 写入数据
    ret &= insertSTMT->step();
}

//statement用完之后可以一次性finalize,底下会调用sqlite3_finalize函数逐个释放preparedStatement的资源
//不调用的话,handle 析构之后也会自动finalize它所创建的所有preparedStatement
handle.finalizeAllStatement();

五、事务

WCDB 提供的事务接口中,可中断事务对于界面开发来说,真的非常实用!它可以让界面线程得到优先处理,而不至于被饿死。后台执行事务时,如果发现界面线程在等待事务,会立即中断自己,保障界面线程的响应。

示例代码:

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

// 可中断事务(必须在UI线程中,设置线程ID,不然无效)
WCDB::Database::setUIThreadId(std::this_thread::get_id()); // 这句必须在UI线程中先执行

std::async(std::launch::async, [&](){
    int index = 0;
    database.runPausableTransactionWithOneLoop([&](WCDB::Handle &handle, bool &stop, bool isNewTransaction){
        bool ret = false;
        // isNewTransaction表示第一次执行,或者事务在上次循环结束之后被中断提交了
        if(isNewTransaction) {
            //新事务先建一下表,避免事务被中断之后,表已经被其他逻辑删除
            ret = handle.createTable<Sample>("sampleTable");
        }
        //写入一个对象,这里还可以用WCDB::PreparedStatement来减少SQL解析的耗时
        ret &= handle.insertObjects<Sample>(objects[index], "sampleTable");
        
        index++;
        //给stop赋值成true表示事务结束,根据返回值ret提交或者回滚事务。
        stop = index >= objects.size();
        return ret;
    });
});

1、使可中断事务生效的关键代码:WCDB::Database::setUIThreadId(std::this_thread::get_id());