-
Notifications
You must be signed in to change notification settings - Fork 1.5k
C++ 语言集成查询
语言集成查询(WCDB Integrated Language Query,简称 WINQ),是 WCDB 的一项基础特性。它使得开发者能够通过 C++ 的语法特性去完成 SQL 语句。
WCDB::OptionalValueArray<Sample> objects = database.getAllObjects<Sample>("sampleTable", WCDB_FIELD(Sample::identifier) > 1);其中第二个参数 WCDB_FIELD(Sample::identifier) > 1 就是语言集成查询的其中一个写法。其虽然是 identifier 和数字 1 的比较,但其结果并不为 Bool 值,而是 WCDB::Expression。该 WCDB::Expression 作为 SQL 的 where 参数,用于数据库查询。
语言集成查询基于 SQLite 的 SQL 语法实现。只要是 SQL 支持的语句,都能使用语言集成查询完成。也因此,语言集成查询具有和 SQL 语法一样的复杂性,本文不可能将所有涉及的接口和类都介绍一遍。而是结合 WCDB,介绍常用的类型,并在最后说明如何将已有 SQL 转换为语言集成查询的写法。
Column 代表数据库内的一个字段,如 Insert 语句中的 column-name,指定了需要插入的数据所属的字段。它可通过字段名创建。
WCDB::Column identifierColumm = WCDB::Column("identifier");
WCDB::StatementInsert insert = WCDB::StatementInsert()
.insertIntoTable("sampleTable")
.column(identifierColumm).value(1);
printf("%s", insert.getDescription().data()); // 输出 "INSERT INTO sampleTable(identifier) VALUES(1)"ResultColumn 通常代表数据库查询中的结果,如 Select 语句中的 result-column,指定了期望查询的结果。
WCDB::Column identifierColumm = WCDB::Column("identifier");
WCDB::ResultColumn identifierResultColumm = WCDB::ResultColumn(identifierColumm);
WCDB::StatementSelect select =
WCDB::StatementSelect().select(identifierResultColumm).from("sampleTable");
printf("%s", select.getDescription().data()); // 输出 "SELECT identifier FROM sampleTable"细心观察语法中的描述可以发现,许多节点的参数可以不止一种。以刚才提到的 ResultColumn 为例。

可以看到,Expression 也可以转换为 ResultColumn。
我们再回到 StatementSelect 语句的 select 函数,倘若它只接受 ResultColumn 类作为参数,那么每次调用时,都需要将 Expression 转换为 ResultColumn。
// 以下为示例代码,并非 WCDB 真正的实现
class StatementSelect {
StatementSelect& select(const ResultColumns& resultColumns);
// ...
}
WCDB::Column identifierColumm = WCDB::Column("identifier");
WCDB::Expression identifierExpression = WCDB::Expression(identifierColumm);
WCDB::ResultColumn identifierResultColumm = WCDB::ResultColumn(identifierExpression);
WCDB::StatementSelect select =
WCDB::StatementSelect().select(identifierResultColumm).from("sampleTable");
printf("%s", select.getDescription().data()); // 输出 "SELECT identifier FROM sampleTable"可以看到,需要 3 重转换,才能将 Column 转换为我们需要的 ResultColumn。
为了解决这个问题,WCDB 根据 C++ 的 SFINEA 机制,定义了一系列 Convertible 类,用于语法中可互相转换的类型。下面给出ResultColumnConvertible的代码示例
template<typename T, typename Enable = void>
class ResultColumnConvertible final : public std::false_type {
public:
static ResultColumn asResultColumn(const T&);
};
template<>
class ResultColumnConvertible<Expression> final : public std::true_type {
public:
static ResultColumn asResultColumn(const Expression& expression);
};
class ResultColumn {
template<typename T,
typename Enable = typename std::enable_if<ResultColumnConvertible<T>::value>::type>
ResultColumn(const T& t)
: ResultColumn(ResultColumnConvertible<T>::asResultColumn(t))
{
}
}基于 Convertible 类,select 接口的参数虽然限定为 ResultColumns,但所有实现了 ResultColumnConvertible 的类型,都能作为 select 函数的参数。
在 SQL 语法中,Expression 是能转换为 ResultColumn 的类型;而 Column 是能转换为 Expression 的类型,因此其也同时是能转换为 ResultColumn 的类型。
// WCDB 内部的代码示例
template<>
class ExpressionConvertible<Column> final : public std::true_type {
public:
static Expression asExpression(const Column& column);
};
template<typename T>
class ResultColumnConvertible<T, typename std::enable_if<ExpressionConvertible<T>::value>::type> final
: public std::true_type {
public:
static ResultColumn asResultColumn(const T& t)
{
return ExpressionConvertible<T>::asExpression(t);
}
};因此,原来的 select 语句可以直接简写为:
WCDB::Column identifierColumm = WCDB::Column("identifier");
WCDB::StatementSelect select =
WCDB::StatementSelect().select(identifierColumm).from("sampleTable");
printf("%s", select.getDescription().data()); // 输出 "SELECT identifier FROM sampleTable"WCDB 内的 Convertible 类型转换类较多,这里不一一赘述。开发者也无需逐一了解,在使用时再查阅接口即可。
WCDB::Field 严格来说不属于语言集成查询的一部分,它们是语言集成查询和模型绑定结合的产物。
当需要通过对象来操作数据库时,如"get object"或者"update with object"等,WCDB 不仅需要知道查找数据的哪个字段(即 Column 所完成的事情,WCDB::Field实际继承了Column),还需要知道这个字段对应模型绑定中的哪个变量。
const WCDB::Field& field = WCDB_FIELD(Sample::identifier);
WCDB::OptionalValueArray<Sample> objects = database.getAllObjectsWithFields<Sample>("sampleTable", field);而 Field 就是存储了数据库字段和模型绑定字段的映射关系,而且还可以分离这个映射,具体用法可参考高级用法一章的查询重定向。
基于模型绑定,开发者可以完全摆脱通过字符串创建 Column,更便捷地操作数据库。Field 是继承了Column来实现的,WINQ中所有使用Column的地方都可以使用Field 。
WCDB::StatementSelect select =
WCDB::StatementSelect().select(WCDB_FIELD(Sample::identifier)).from("sampleTable")
printf("%s", select.getDescription().data());// 输出 "SELECT identifier FROM sampleTable"Expression 可以算是 SQL 里最复杂的一个了。它在许多语句中都出现,比如查询语句的 where、groupBy、having、limit、offset,更新语句的 value、where、limit、offset等等。同时,它的完整定义也很长,甚至还包含了递归。
从单个 Expression 的语法角度来看,它支持从数字、字符串、字段创建。而 WCDB 将其扩展为支持所有字段映射类型,包括内建的类型和自定义的类型。
WCDB::Expression expressionInt = WCDB::Expression(1);
WCDB::Expression expressionDouble = WCDB::Expression(2.0);
WCDB::Expression expressionString = WCDB::Expression("3");
WCDB::Expression expressionColumn = WCDB::Expression(WCDB::Column( "identifier"));除此之外,还有内建的绑定参数
WCDB::BindParameter也可以转成Expression类型,甚至WCDB::StatementSelect也能转成Expression类型,因为有些场景子查询的结果也能作为判断条件。
多个 Expression 之间可以通过函数或运算符的方式进行操作,如:
WCDB::Expression expression = expressionColumn.between(expressionInt, expressionDouble);
printf("%s", expression.getDescription().data()); // 输出 "identifier BETWEEN 1 AND 2.0"Expression 语法所支持的运算符有十多个,WCDB 基本都支持,但在语义上改为更符合 C++ 的习惯。例如
-
||运算符在 SQL 语法中用于字符串链接,而在 WCDB 中则是用于"或"的逻辑运算。 -
<>运算符在 SQL 语法中用于不等比较,而在 WCDB 中则是直接使用较为习惯的!=运算符。
WCDB::Expression expression1 = expressionInt + expressionDouble;
printf("%s", expression1.getDescription().data()); // 输出 "(1 + 2.0)"
WCDB::Expression expression2 = expressionColumn >= expression1;
printf("%s", expression2.getDescription().data()); // 输出 "(identifier >= (1 + 2.0))"
// 基础类型 -1 可以直接转换为 Expression
WCDB::Expression expression3 = expressionColumn < -1 || expression2;
printf("%s", expression3.getDescription().data()); // 输出 "((identifier < -1) OR (identifier >= (1 + 2.0)))"
WCDB::Expression statementSelect = WCDB::StatementSelect()
.select(WCDB_FIELD(Sample::identifier))
.from("sampleTable")
.where(expression3);
printf("%s", statementSelect.getDescription().data()); // 输出 "SELECT identifier FROM sampleTable WHERE ((identifier < -1) OR (identifier >= (1 + 2.0)))"显然,每个类都要转换成 Expression 来进行这些操作,虽然也可以,但这就太麻烦了。
WCDB::Expression expression = WCDB::Expression(WCDB_FIELD(Sample::identifier)) > 1;
printf("%s", expression.getDescription().data());因此,WCDB 将 Expression支持的函数操作和运算符操作定义成了多种 ExpressionOperable 基类:
-
ExpressionUnaryOperable一元运算符; -
ExpressionBinaryOperable二元运算符; -
ExpressionInOperablein操作,包括一系列in和notIn操作; -
ExpressionBetweenOperablebetween操作 -
ExpressionCollateOperable给Expression指定字符串比对函数,具体见Collation。 -
CoreFunctionOperable给Expression调用SQLite内置函数,包括abs、hex、length等,具体见Core Function。 -
AggregateFunctionOperable给Expression调用SQLite内置聚合函数,包括avg、count、max、min等,具体见Aggregate Function。 -
FTSFunctionOperable给Expression调用FTS函数,包括highlight、matchinfo等。
继承这些ExpressionOperable 基类的类都可以与 Expression 的类似,使用函数或运算符进行语言集成查询,目前包括可以作为表达式左值的Column、Expression这两个。也因此,基于模型绑定,开发者可以完全摆脱通过拼装 Expression,更便捷地操作数据库。
WCDB::StatementSelect select = WCDB::StatementSelect().select(WCDB_FIELD(Sample::identifier))
.from("sampleTable")
.where(WCDB_FIELD(Sample::identifier) < -1 || WCDB_FIELD(Sample::identifier) >= 3.0);
printf("%s", select.getDescription().data()); // 输出 "SELECT identifier FROM sampleTable WHERE ((identifier < -1) OR (identifier >= 3.0))"Statement 在前文已经接触到不少了,如查询 StatementSelect、插入 StatementInsert 等。它是一个最基本的完整可被执行的 SQL 语句。
SQLite 共包含 27 种 Statement,WCDB 都支持了。根据语法规则创建的 Statement,可以通过HandleOperation::execute() 函数直接执行,也可以使用Database、Handle和Table下面的这些接口获取执行Statement的结果,
-
getValueFromStatement执行Statement并获取一个结果。 -
getOneRowFromStatement执行Statement并获取一行结果,结果为一维数组。 -
getOneColumnFromStatement执行Statement并获取一列结果,结果为一维数组。 -
getAllRowsFromStatement执行Statement并获取多行结果,以二维数组的方式返回。
试考虑,表中的数据可以想象为一个矩阵的存在,假设其数据如下:
| identifier | content |
|---|---|
| 1 | "sample1" |
| 2 | "sample1" |
| 3 | "sample2" |
| 4 | "sample2" |
| 5 | "sample2" |
以下是使用Statement进行查询操作的示例代码:
// 获取所有内容
WCDB::OptionalMultiRows allRows = database.getAllRowsFromStatement(WCDB::StatementSelect().select(Sample::allFields()).from("sampleTable"));
printf("%lld", allRows.value()[2][0].intValue()); // 输出 3
// 获取第二行
WCDB::OptionalOneRow secondRow = database.getOneRowFromStatement(WCDB::StatementSelect().select(Sample::allFields()).from("sampleTable").offset(1));
printf("%lld", secondRow.value()[0].intValue()); // 输出 2
// 获取第二行 content 列的值
WCDB::OptionalOneColumn contentColumn = database.getOneColumnFromStatement(WCDB::StatementSelect().select(WCDB_FIELD(Sample::content)).from("sampleTable"));
printf("%s", contentColumn.value()[3].textValue().data()); // 输出 "sample2"
// 获取 identifier 的最大值
WCDB::OptionalValue maxId = database.getValueFromStatement(WCDB::StatementSelect().select(WCDB_FIELD(Sample::identifier).max()).from("sampleTable"));
printf("%lld", maxId.value().intValue());// 输出 5
// 获取不同的 content 数
WCDB::OptionalValue distinctContentCount = database.getValueFromStatement(WCDB::StatementSelect().select(WCDB_FIELD(Sample::content).count().distinct()).from("sampleTable"));
printf("%lld", distinctContentCount.value().intValue());// 输出 2Statement 还可以通过 StatementOperation::prepare() 创建 WCDB::PreparedStatement 对象,来精细控制Statement的执行过程,这个我们会在高级接口的Handle一节详细介绍。
如前文所说,SQL 的复杂性决定了不可能介绍每一个类型及语句。因此,这里将介绍如何将一个已有的 SQL,转写为语言集成查询。开发者可以以此为例子,触类旁通。
对于已有的 SQL:
- 在语法中确定其所属的
Statement。 - 对照对应
Statement的语法,根据关键字对已有的 SQL 进行断句。 - 逐个通过语言集成查询的函数调用进行实现。
以如下 SQL 为例:
SELECT min(identifier) FROM sampleTable WHERE (identifier > 0 || identifier / 2 == 0 ) && description NOT NULL ORDER BY identifier ASC LIMIT 1, 100根据 Statement 的列表,显然这个 SQL 属于 StatementSelect。
WCDB::StatementSelect statementSelect = WCDB::StatementSelect()根据 StatementSelect 的语法规则,按关键词(即语法中的大写字母)进行断句:
SELECT min(identifier)
FROM sampleTable
WHERE (identifier > 0 || identifier / 2 == 0 ) && description NOT NULL
ORDER BY identifier ASC
LIMIT 1, 100根据每个关键词,找到语言集成查询中对应的函数,并完成其参数。
如 SELECT 关键词对应 StatementSelect 的 select() 函数和from() 函数,以及AggregateFunctionOperable中的min函数:
statementSelect.select(WCDB_FIELD(Sample::identifier).min().distinct())
statementSelect.from("sampleTable")WHERE 的参数虽然较复杂,但也都能找到对应的函数:
statementSelect.where((WCDB_FIELD(Sample::identifier) > 0 || WCDB_FIELD(Sample::identifier) / 2 == 0) && WCDB_FIELD(Sample::content).notNull())其他语句也同理:
statementSelect.order(WCDB_FIELD(Sample::identifier).asOrder(WCTOrderedAscending))
statementSelect.limit(1, 100)SQLite的官网给出了它支持的全部SQL语法的结构图,还是以最复杂的select语句为例:

WINQ的整体设计思路是把整个select语句包装成一个StatementSelect对象,把图中的圆角方框的连接点包装成StatementSelect对象的成员函数,如:with、select、from、where、groupby、having、orderBy、limit等等这些函数。
图中直角方框的内容则是设计成StatementSelect对象的成员函数的入参。一些简单的入参则是支持使用基础类型,比如from函数可以传入一个字符串表示表名,一些复杂的入参则是继续包装成一个对象,比如前面提到的ResultColumn和Expression,其他的还有CommonTableExpression、Join、WindowDef等等这些对象。
所以在编写复杂SQL语句时,可以先找到对应的Statement对象,然后找到需要调用的方法,最后再根据方法需要的入参传入具体值。按照这个思路,可以根据SQL的语法结构图编写出所有SQLite支持的SQL语句。
[C++-Object-Relational-Mapping-Builtin-Column-Codable-Type]:
- 欢迎使用 WCDB
- 基础教程
- 进阶教程
- 欢迎使用 WCDB
- 基础教程
- 进阶教程
- 欢迎使用 WCDB
- 基础教程
- 进阶教程
- 欢迎使用 WCDB
- 基础教程
- 进阶教程