WCDB C++ 学习笔记
WCDB 项目主页:github.com/Tencent/wcd…
WCDB 官方教程:github.com/Tencent/wcd…
WCDB 非常吸引我的两大特色点:
1、全文索引(中文分词)
2、可中断事务
PC 端开发,涉及全文索引问题时,常见的解决方案有:Solr、SQLite3 FTS5、CLucence 等。但使用他们都有一个问题,就是中文分词,需要消耗一定的人力去搞。而我并不想花太多精力去做本该搜索引擎做的事。这也是为什么选择 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::OneRowValue | typedef ValueArray<Value> OneRowValue |
| WCDB::OneColumnValue | typedef ValueArray<Value> OneColumnValue |
| WCDB::MultiRowsValue | typedef ValueArray <ValueArray<Value>> MultiRowsValue |
SQLite Optional 数据
| 类型 | 描述 |
|---|---|
| WCDB::Optional<T> | 同 C++ 17 std::optional ,相当于返回值附带了个bool来标识该返回值是否有效 |
| WCDB::OptionalValueArray<T> | using OptionalValueArray = Optional<ValueArray<ValueType>> |
| WCDB::OptionalValue | typedef Optional<Value> OptionalValue |
| WCDB::OptionalOneRow | typedef OptionalValueArray<Value> OptionalOneRow |
| WCDB::OptionalOneColumn | typedef OptionalValueArray<Value> OptionalOneColumn |
| WCDB::OptionalMultiRows | OptionalValueArray<ValueArray<Value>> OptionalMultiRows |
SQLite 多表数据
| 类型 | 描述 |
|---|---|
| WCDB::MultiObject | 存储多表查询结果中的一整行数据,该行数据由多张表的多个字段组成 |
| WCDB::OptionalMultiObject | typedef Optional<MultiObject> OptionalMultiObject |
| WCDB::OptionalMultiObjectArray | typedef OptionalValueArray<MultiObject> OptionalMultiObjectArray |
二、类继承关系图
三、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 将这些语法封装成了两大块:Statement 和 Identifier 。
1、Statement: 完整的 SQL 语句。
2、Identifier: 主要封装小的语法块,一条完整的 SQL 由多个不同的 identifier 组成。如:Column、GROUP BY、ORDER BY 等。
示例代码中的 WCDB::StatementSelect 和 WCDB::Column("column_name") 分别对应了 Statement 和 Identifier。
其类继承关系图如下所示:
3.1. Statement 系列封装
SQLite 一共 27 种 Statement ,外加一个 EXPLAIN 语句。下表是 WCDB 对它们的封装:
| 编号 | WCDB C++ 类 | SQLite Statement |
|---|---|---|
| 1 | WCDB::StatementAlterTable | alter-table-stmt |
| 2 | WCDB::StatementAnalyze | analyze-stmt |
| 3 | WCDB::StatementAttach | attach-stmt |
| 4 | WCDB::StatementBegin | begin-stmt |
| 5 | WCDB::StatementCommit | commit-stmt |
| 6 | WCDB::StatementCreateIndex | create-index-stmt |
| 7 | WCDB::StatementCreateTable | create-table-stmt |
| 8 | WCDB::StatementCreateTrigger | create-trigger-stmt |
| 9 | WCDB::StatementCreateView | create-view-stmt |
| 10 | WCDB::StatementCreateVirtualTable | create-virtual-table-stmt |
| 11 | WCDB::StatementDelete | delete-stmt |
| 12 | delete-stmt-limited | |
| 13 | WCDB::StatementDetach | detach-stmt |
| 14 | WCDB::StatementDropIndex | drop-index-stmt |
| 15 | WCDB::StatementDropTable | drop-table-stmt |
| 16 | WCDB::StatementDropTrigger | drop-trigger-stmt |
| 17 | WCDB::StatementDropView | drop-view-stmt |
| 18 | WCDB::StatementInsert | insert-stmt |
| 19 | WCDB::StatementPragma | pragma-stmt |
| 20 | WCDB::StatementReindex | reindex-stmt |
| 21 | WCDB::StatementRelease | release-stmt |
| 22 | WCDB::StatementRollback | rollback-stmt |
| 23 | WCDB::StatementSavepoint | savepoint-stmt |
| 24 | WCDB::StatementSelect | select-stmt |
| 25 | WCDB::StatementUpdate | update-stmt |
| 26 | update-stmt-limited | |
| 27 | WCDB::StatementVacuum | vacuum-stmt |
| -- | ||
| 28 | WCDB::StatementExplain | EXPLAIN QUERY PLAN [SQLite Query] |
| 29 | WCDB::Statement | 上述所有 StatementXXX 的基类 |
3.2. Identifier 系列封装
除 Statement 外,WCDB 将其余的 SQL 语法,封装成了 Identifier 系列。共23个类,如下所示:
| 编号 | WCDB C++ 类 | 说明 | SQL 示例 |
|---|---|---|---|
| 1 | WCDB::BindParameter | 绑定参数 | VALUES( ?, ?, ? ) |
| 2 | WCDB::Column | 更新列、插入列 | INSERT INTO table(column_name) VALUES('hello') |
| 3 | WCDB::ColumnConstraint | CONSTRAINT 列约束 | |
| 4 | WCDB::ColumnDef | 创建列、修改列 | CREATE TABLE table(column_name TEXT); |
| 5 | WCDB::CommonTableExpression | ||
| 6 | WCDB::Expression | 表达式 | |
| 7 | WCDB::Filter | ||
| 8 | WCDB::ForeignKey | 外键 | |
| 9 | WCDB::FrameSpec | ||
| 10 | WCDB::IndexedColumn | 索引列 | ORDER BY name COLLATE NOCASE DESC |
| 11 | WCDB::Join | ||
| 12 | WCDB::JoinConstraint | ||
| 13 | WCDB::LiteralValue | ||
| 14 | WCDB::OrderingTerm | 排序 | |
| 15 | WCDB::Pragma | ||
| 16 | WCDB::QualifiedTable | ||
| 17 | WCDB::RaiseFunction | ||
| 18 | WCDB::ResultColumn | 选择列 | SELECT column_name FROM table |
| 19 | WCDB::Schema | ||
| 20 | WCDB::TableConstraint | CONSTRAINT 表约束 | |
| 21 | WCDB::TableOrSubquery | 表名或子查询 | |
| 22 | WCDB::Upsert | ||
| 23 | WCDB::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::Field 和 WCDB::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::Database | WCDB::Handle | WCDB::Table |
|---|---|---|---|---|
| createTable | 创建表 | createTable("table") | 同 WCDB::Database | :x: 无 |
| insertObjects | 增 | insertObjects(objs,"table"); | 同 WCDB::Database | insertObjects(objs); |
| deleteObjects | 删 | deleteObjects(...) | 同 WCDB::Database | deleteObjects(...) |
| updateObject | 改 | updateObject(...) | 同 WCDB::Database | updateObject(...) |
| getAllObjects | 查 | getAllObjects(...) | 同 WCDB::Database | getAllObjects(...) |
| prepareInsert | 增 | prepareInsert() | 同 WCDB::Database | prepareInsert() |
| prepareDelete | 删 | prepareDelete() | 同 WCDB::Database | prepareDelete() |
| prepareUpdate | 改 | prepareUpdate() | 同 WCDB::Database | prepareUpdate() |
| prepareSelect | 查 | prepareSelect() | 同 WCDB::Database | prepareSelect() |
| prepareMultiSelect | 联表查 | prepareMultiSelect() | 同 WCDB::Database | :x: 无 |
| prepare | sqlite3_stmt | :x: 无 | handle持有的主stmt | :x: 无 |
| getOrCreatePreparedStatement | sqlite3_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++ 类 | 描述 |
|---|---|---|
| 1 | WCDB::StatementOperation | 接口类,定义了操作 sqlite3_stmt 的通用接口 |
| 2 | WCDB::PreparedStatement | 继承 StatementOperation 接口,持有 sqlite3_stmt 句柄 |
| 3 | WCDB::OptionalPreparedStatement | typedef 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());