微信移动端数据库组件 WCDB 系列(三) — 解析 WINQ 原理

article/2025/10/9 11:15:11

背景

高效、完整、易用是 WCDB 的基本原则。前几篇文章分享了 WCDB 的基本用法和修复工具,接下来将更深入地聊聊 WCDB 在易用性上的思考和实践。

对于各类客户端数据库,似乎都绕不开拼接字符串这一步。即便在 Realm 这样的 NoSQL 的数据库中,在进行查询时,也依赖于字符串的语法:

//Realm code
[Dog objectsWhere:@"age < 2"]

别看小小的字符串拼接,带来的麻烦可不小:

  • 代码冗余。为了拼接出匹配的 SQL 语句,业务层往往要写许多胶水代码来 format 字符串。这些代码冗长且没有什么“营养”。

  • 难以查错。对于编译器而言,SQL 只是一个字符串。这就意味着即便你只写错了一个字母,也得在代码 run 起来之后,通过 log 或断点才能发现错误。倘若 SQL 所在的代码文件依赖较多,即使改正一个敲错的字母,就得将整个工程重新编译一遍,简直是浪费生命。

  • SQL 注入。举一个简单的例子:

- (BOOL)insertMessage:(NSString*)message
{NSString* sql = [NSString stringWithFormat:@"INSERT INTO message VALUES('%@')", message];return [db executeUpdate:sql];
}

这是插入消息的 SQL。倘若对方发来这样的消息:');DELETE FROM message;--,那么这个插入的 SQL 就会被分成三段进行解析:

INSERT INTO message VALUES('');
DELETE FROM message;
--')

它会在插入一句空消息后,将 message 表内的所有消息删除。若 App 内存在这样的漏洞被坏人所用,后果不堪设想。

反注入的通常做法是:

  • 利用 SQLite 的绑定参数。通过绑定参数避免字符串拼接。
- (BOOL)insertMessage:(NSString*)message
{return [db executeUpdate:@"INSERT INTO message VALUES(?)", message];
}
  • 对于不适用绑定参数的 SQL,则可以将单引号替换成双单引号,避免传入的单引号提前截断 SQL。
- (BOOL)insertMessage:(NSString*)message
{NSString* sql = [NSString stringWithFormat:@"INSERT INTO message VALUES('%@')", [message stringByReplacingOccurrencesOfString:@"'" withString:@"''"]];return [db executeUpdate:sql];
}

尽管反注入并不难,但要求业务开发都了解、并且在开发过程中时时刻刻都警惕着 SQL 注入,是不现实的。

一旦错过了在框架层统一解决这些问题的机会,后面再通过代码规范、Code Review 等等人为的方式去管理,就难免会发生疏漏。

因此,WCDB 的原则是,问题应当更早发现更早解决。

  • 能在编译期发现的问题,就不要拖到运行时;
  • 能在框架层解决的问题,就不要再让业务去分担。

基于这个原则,我开始进行对 SQLite 的接口的抽象。

SQL 的组合能力

思考的过程注定不会是一片坦途,我遇到的第一个挑战就是:

问题一:SQL 应该怎么抽象?

SQL 是千变万化的,它可以是一个很简单的查询,例如:

SELECT * FROM message;

这个查询只是取出 message 表中的所有元素。假设我们可以封装成接口:

StatementSelect getAllFromTable(const char* tableName);

但 SQL 也可以是一个很复杂的查询,例如:

SELECT max(localID), count(content) FROM message
WHERE content IS NOT NULL AND createTime!=modifiedTime OR type NOT BETWEEN 0 AND 2
GROUP BY type
HAVING localID>0
ORDER BY createTime ASC
LIMIT (SELECT count(*) FROM contact, contact_extWHERE contact.username==contact_ext.username)

这个查询包含了条件、分组、分组过滤、排序、限制、聚合函数、子查询,多表查询。什么样的接口才能兼容这样的 SQL?

遇到这种两极分化的问题,我的思路通常是二八原则。即

  • 封装常用操作,覆盖 80%的使用场景。
  • 暴露底层接口,适配剩余 20%的特殊情况。

但更多的问题出现:

问题二:怎么定义常用操作?

  • 对于微信常用的操作,是否也适用于所有开发者?
  • 现在不使用的操作,以后是否会变成常用?

问题三:常用操作与常用操作的组合,是否仍属于常用操作?

查询某个字段的最大值或最小值,应该属于常用操作的:

SELECT max(localID) FROM message;
SELECT min(localID) FROM message;

假设可以封装为

StatementSelect getMaxOfColumnFromTable(const char* columnName, const char* tableName);
StatementSelect getMinOfColumnFromTable(const char* columnName, const char* tableName);

但,SQL 是存在组合的能力的。同时查询最大值和最小值,是否仍属于常用操作?

SELECT max(localID), min(localID) FROM message;

若以此规则,继续封装为:

StatementSelect getMaxAndMinOfColumnFromTable(const char* columnName, const char* tableName);

那同时查询最大值、最小值和总数怎么办?

SELECT max(localID), min(localID), count(localID) FROM message;

显然,“常用接口”的定义在不断地扩大,接口的复杂性也在增加。以后维护起来,就会疲于加新接口,并且没有边界。

问题四:特殊场景所暴露的底层接口,应该以什么形式存在?

若底层接口还是接受字符串参数的传入,那么前面所思考的一切都是徒劳。

因此,这里就需要一个理论的基础,去支持 WCDB 封装是合理的,而不仅仅是堆砌接口。

于是,我就去找了 SQL 千变万化组合的根源 — SQL 语法规则。

SQL 语法规则

SQLite 官网提供了 SQL 的语法规则:http://www.sqlite.org/lang.html

例如,这是一个SELECT语句的语法规则:

SQLite 按照图示箭头流向的语法规则解析传入的 SQL 字符串。每个箭头都有不同的流向可选。

例如,SELECT后,可以直接接result-column,也可以插入DISTINCT或者ALL

语法规则中的每个字段都有其对应涵义,其中

  • SELECTDISTINCTALL等等大写字母是keyword,属于 SQL 的保留字。
  • result-column、``table-or-subqueryexpr等等小写字母是 token。token 可以再进一步地展开其构成的语法规则。

例如,在WHEREGROUP BYHAVINGLIMITOFFSET后所跟的参数都是expr,它的展开如下:

可以看到,expr有很多种构成方式,例如:

  • expr: literal-valueliteral-value可以进一步展开,它是纯粹的数值。
    • 如数字 1、数字 30、字符串”Hello”等都是literal-value,因此它们也是expr
  • expr: expr (binary operator) expr。两个expr通过二元操作符进行连接,其结果依然属于expr
    • 如 1+”Hello”。1 和”Hello”都是literal-value,因此它们都是expr,通过二元操作符”+”号连接,其结果仍然是一个expr。尽管 1+”Hello”看上去没有实质的意义,但它仍是 SQL 正确的语法。

以刚才那个复杂的 SQL 中的查询语句为例:

content IS NOT NULL 
AND createTime!=modifiedTime 
OR type NOT BETWEEN 0 AND 2
  1. content IS NOT NULL,符合 expr IS NOT NULL的语法,因此其可以归并为expr
  2. createTime!=modifiedTime,符合 expr (binary operator) expr的语法,因此其可以归并为expr
  3. type NOT BETWEEN 0 AND 2,符合 expr NOT BETWEEN expr AND expr的语法,因此其可以归并为expr
  4. 1. AND 2.,符合expr (binary operator) expr的语法,因此其可以归并为expr
  5. 4. OR 3.,符合expr (binary operator) expr的语法,因此其可以归并为expr

最终,这么长的条件语句归并为了一个expr,符合SELECT语法规则中WHERE expr的语法,因此是正确的 SQL 条件语句。

也正是基于此,可以得出:只要按照 SQL 的语法封装,就可以保留其组合的能力,就不会错过任何接口,落入疲于加接口的陷阱。

WCDB 的具体做法是:

  1. 将固定的 keyword,封装为函数名,作为连接。
  2. 将可以展开的 token,封装为类,并在类内实现其不同的组合。

以 SELECT 语句为例:

class StatementSelect : public Statement {
public://...StatementSelect &where(const Expr &where);StatementSelect &limit(const Expr &limit);StatementSelect &having(const Expr &having);//...
};

在语法规则中,WHERELIMIT等都接受expr作为参数。因此,不管 SQL 多么复杂,StatementSelect也只接受Expr的参数。而其组合的能力,则在Expr类内实现。

class Expr : public Describable {
public:Expr(const Column &column);template <typename T>Expr(const T &value,typename std::enable_if<std::is_arithmetic<T>::value ||std::is_enum<T>::value>::type * = nullptr): Describable(literalValue(value)){}Expr(const std::string &value);Expr operator||(const Expr &operand) const;Expr operator&&(const Expr &operand) const;Expr operator!=(const Expr &operand) const;Expr between(const Expr &left, const Expr &right) const;Expr notBetween(const Expr &left, const Expr &right) const;Expr isNull() const;Expr isNotNull() const;//...
};

Expr通过构造函数和 C++的偏特化模版,实现了从字符串和数字等进行初始化的效果。同时,通过 C++运算符重载的特性,可以将 SQL 的运算符无损地移植到过来,使得语法上也可以更接近于 SQL。

在对应函数里,再进行 SQL 的字符串拼接即可。同时,所有传入的字符串都会在这一层预处理,以防注入。如:

Expr::Expr(const std::string &value) : Describable(literalValue(value))
{
}std::string Expr::literalValue(const std::string &value)
{//SQL anti-injectionreturn "'" + stringByReplacingOccurrencesOfString(value, "'", "''") + "'";
}Expr Expr::operator&&(const Expr &operand) const
{Expr expr;expr.m_description.append("(" + m_description + " AND " +operand.m_description + ")");return expr;
}

基于这个抽象方式,就可以对复杂查询中的条件语句进行重写为:

Column content("content");
Column createTime("createTime");
Column modifiedTime("modifiedTime");
Column type("type");
StatementSelect select;
//...
//WHERE content IS NOT NULL 
//      AND createTime!=modifiedTime 
//      OR type NOT BETWEEN 0 AND 2
select.where(Expr(content).isNotNull()&&Expr(createTime)!=Expr(modifiedTime)||Expr(type).notBetween(0, 2));
//...

首先通过Column创建对应数据库字段的映射,再转换为Expr,调用对应封装的函数或运算符,即可完成字符串拼接操作。

这个抽象便是 WCDB 的语言集成查询的特性 —— WINQ(WCDB、Integrated、Query)。

更进一步,由于 WCDB 在接口层的 ORM 封装,使得开发者可以直接通过className.propertyName的方式,拿到字段的映射。因此连上述的转换操作也可以省去,查询代码可以在一行代码内完成。

以下是 WCDB 在接口层和 WINQ 的支持下,对前面所提到的 SQL 语句的代码示例:

//SELECT * FROM message;
[db getAllObjectsOfClass:Message.classfromTable:@"message"];/*SELECT max(localID), count(content) FROM messageWHERE content IS NOT NULL AND createTime!=modifiedTime OR type NOT BETWEEN 0 AND 2GROUP BY typeHAVING localID>0ORDER BY createTime ASCLIMIT (SELECT count(*) FROM contact, contact_extWHERE contact.username==contact_ext.username)*/
[[[[[[db prepareSelectRowsOnResults:{Message.localID.max(), Message.content.count()}fromTable:@"message"]where:Message.content.isNotNull() && Message.createTime != Message.modifiedTime || Message.type.notBetween(0, 2)]groupBy:{Message.type}]having:Message.localID > 0]orderBy:Message.createTime.order(WCTOrderedAscending)]limit:[[[WCTSelectBase alloc] initWithResultList:Contact.AnyProperty.count()fromTables:@[ @"contact", @"contact_ext" ]]where:Contact.username.inTable(@"contact") == ContactExt.username.inTable(@"contact_ext")]];/*SELECT max(localID) FROM message;*/
[db getOneValueOnResult:Message.localID.max()fromTable:@"message"];
/*SELECT min(localID) FROM message;*/
[db getOneValueOnResult:Message.localID.min()fromTable:@"message"];
/*SELECT max(localID), min(localID) FROM message;*/
[db getOneRowOnResults:{Message.localID.max(), Message.localID.min()}fromTable:@"message"];
/*SELECT max(localID), min(localID), count(localID) FROM message*/
[db getOneRowOnResults:{Message.localID.max(), Message.localID.min(), Message.localID.count()}fromTable:@"message"];

总结

WCDB 通过 WINQ 抽象 SQLite 语法规则,使得开发者可以告别字符串拼接的胶水代码。通过和接口层的 ORM 结合,使得即便是很复杂的查询,也可以通过一行代码完成。并借助 IDE 的代码提示和编译检查的特性,大大提升了开发效率。同时还内建了反注入的保护。

代码提示

编译时检查

虽然 WINQ 在实现上使用了 C++11 特性和模版等,但在使用过程并不需要涉及。对于熟悉 SQL 的开发,只需按照本能即可写出 SQL 对应的 WINQ 语句。最终达到提高 WCDB 易用性的目的。

同时,基于 C++的实现也使得 WINQ 在性能可以期待。

后续我们还将分享 WCDB 在多线程管理上的思考,欢迎访问 WCDB 的 Github 仓库,先睹为快!

作者:张三华
责编:唐门教主


了解最新移动开发、VR/AR 干货技术分享,请关注 mobilehub 微信公众号(ID: mobilehub)。


http://chatgpt.dhexx.cn/article/T3S6PA8t.shtml

相关文章

WCDB源码解析

源文链接&#xff1a;http://xiangwangfeng.com/2018/01/08/WCDB-源码解析 起因 最近开了个新项目&#xff0c;项目的主程童鞋引入了 WCDB 代替原先自制的 KeyValueStore 和 FMDB。问为何&#xff0c;答曰&#xff1a;好用&#xff0c;线程安全又高效。又问具体实现细节&#x…

IOS数据存储 之WCDB (二)WCDB.swift使用篇

IOS数据存储 之WCDB &#xff08;二&#xff09;WCDB.swift使用篇 1.WCDB.Swfit基础使用1.1 WCDB.Swfit 简介1.1.1 模型绑定1.1.2 创建数据库与表1.1.3 操作数据1.1.3.1 插入操作1.1.3.2 查找操作1.1.3.3 更新操作1.1.3.4 删除操作 1.2. 模型绑定1.2.1 Swift 模型绑定1.2.2 字段…

Android使用WCDB+Room 总结

最近项目有需要用到wcdb数据库&#xff0c;并且保证和IOS互通数据&#xff0c;在网上找很多相关资料&#xff0c;最后还是靠自己一点点摸索成功&#xff0c;现在做个总结。 一、在gradle 里加上 WCDB 相关的 room 组件 def room_version "2.3.0"// wcdb数据库和roo…

IOS数据存储 之WCDB (一)

IOS数据存储 之WCDB &#xff08;一&#xff09; 1. WCDB 简介1.1 使用WCDB框架3大优势1.2 WCDB 的一些基础概念1.2.1 类字段绑定&#xff08;ORM&#xff09;1.2.2 WINQ&#xff08;WCDB语言集成查询&#xff09;1.2.2.1 字段映射与运算符1.2.2.2 字段组合1.2.2.3 AllProperti…

iOS开发 数据存储之WCDB的介绍

一.介绍 WCDB是一个高效、完整、易用的移动数据库框架,基于SQLCipher,支持iOS,macOS和Android 二.基本特性 易用,WCDB支持一句代码即可将数据取出并组合为object WINQ(WCDB语言集成查询):通过WINQ,开发者无须为了拼接SQL的字符串而写一大坨胶水代码ORM(Object Relational Ma…

开源微信小程序自助建站系统源码 含精美的多行业模板和搭建教程

分享一个微信小程序自助建站系统源码&#xff0c;含各行各业的小程序模板和搭建教程&#xff0c;可一键切换模板&#xff0c;自由DIY&#xff0c;搭建属于你自己的小程序。 特色功能一览&#xff1a; 11、支持创建多个小程序&#xff01;&#xff08;没有数量限制&#xff0c;后…

强大易用的开源建站工具Halo

最近无意间看到别人的博客外观非常美观&#xff0c;便萌生了偷师学艺的想法…所以就看到看了Halo这个开源的建站项目,其实使用起来非常简单&#xff0c;但是想要做一个类似的开源建站工具&#xff0c;谈何容易 访问官网 https://halo.run/ 使用docker部署 搜索镜像halo do…

14个免费好用的建站工具

有时候&#xff0c;我们想建立一个独立网站的时候&#xff0c;苦于自己技能不够&#xff0c;而迟迟没有行动&#xff0c;其实&#xff0c;我们真正的去构建一个独立网站的时候&#xff0c;我们并不需要多复杂的技术。我们也不一定要成为非常专业的程序员&#xff0c;因为现在&a…

推荐一款免费开源的建站系统 - AnqiCMS

安企内容管理系统(AnqiCMS)&#xff0c;是一款使用 GoLang 开发的企业站内容管理系统&#xff0c;它部署简单&#xff0c;软件安全&#xff0c;界面优雅&#xff0c;小巧&#xff0c;执行速度飞快&#xff0c;使用 AnqiCMS 搭建的网站可以防止众多安全问题发生。AnqiCMS 的设计…

介绍一款开源、高性价比的在线教育建站系统

今天给大家介绍一款开源在线教育建站系统——edusoho&#xff0c;项目是用PHP开发&#xff0c;所以基本上会搭建php站点就可以完成本次的搭建。 先看看安装之后的登录界面。 去官网下载源码 笔者下载企培开源版&#xff1a;edusoho-ct-21.4.5.zip 系统说明 1.系统&#xff1…

国内好用的五款开源建站系统

推荐5款优秀的开源建站系统,都有免费版本,有需要可以去试试。蝉知 蝉知系统是一款开源的的企业营销自助建站系统。它专为企业营销设计,伪静态网址、关键词、语义化结构,内置流量统计。 蝉知功能全面,文章发布、会员管理、论坛评论、产品展示等,并内置商城系统,商品、订…

免费开源的建站程序大全,不会编程也可以自助搭建网站了哦

想建网站又不会编程的小伙伴有福啦&#xff0c;本期推荐一些开源的cms建站程序&#xff0c;不需要写后端的任何逻辑代码&#xff0c;轻轻松松就可以建立自己的网站了&#xff0c;当然&#xff0c;要想网站有自己的个性&#xff0c;模版还是需要自己写的&#xff0c;只需要会简单…

绝了!小说建站项目完整开源

超棒的开源小说文学建站 CMS 系统&#xff0c;作为面试项目有牌面儿&#xff01; 编程导航开源仓库&#xff1a;https://github.com/liyupi/code-nav 大家好&#xff0c;我是鱼皮&#xff0c;今天给大家推荐一个优秀的开源 Java 全栈项目。 小说精品屋&#xff0c;是一套非常完…

最新首发自助建站系统源码,傻瓜式一键建站系统源码,高度开源支持专业在线自助建站服务平台软件

一佰互联,巅云门户自助建站系统v8建站平台版&#xff0c;历经3年不断打磨终于上线了。专业PS级大师级高端响应式智能建站平台软件&#xff0c;只为网络公司而生&#xff0c;采用国内知名开源php框架,Thinkphp6vue.js前端数据响应系统,实现了在线自助开通网站&#xff0c;企业站…

四大免费开源建站系统

原文&#xff1a;四大免费开源建站系统 - 知乎 第一&#xff1a; WordPress WordPress的主流客户是企业/个人的官网。一家公司不一定会在网上卖东西&#xff0c;但一定会需要一个官网。用WordPress做官网可谓是性价比最优选择。如果没有预算&#xff0c;你可以自己买几十美金的…

有哪些免费好用的开源建站程序/系统,推荐下?

我推荐WordPress建站程序&#xff0c;学习入门门槛很低&#xff0c;全世界近三分之一的网站都是采用wordpress,所以没有理由不去学习它。 我从一名小白变为wordpress建站老手&#xff0c;对比过很多建站程序&#xff0c;还是觉得wordpress能帮我节省时间和精力&#xff0c;让我…

开源自助建站系统源码完整源码+搭建教程 傻瓜式一键建站系统源码

一键傻瓜式自助建站系统源码&#xff0c;目前包含七百多套完整网站模板&#xff0c;全部都是响应式网站模板&#xff0c;傻瓜一键自助建站。开发组合PHPmysql&#xff0c;功能强大。 一键自助建站系统源码带安装教程&#xff0c;源码下载&#xff1a;春哥技术博客获取。自助建站…

五款开源免费的建站系统推荐

最近研究了下开源的建站系统&#xff0c;推荐5款国内的吧&#xff0c;都有免费版本&#xff0c;有需要可以去试试。 ECTouch ECTouch是一款开源免费的移动商城网店系统。能够帮助企业和个人快速构建手机移动商城并减少二次开发带来的成本。 ECTouch采用PHPMYSQL方式运行&…

Excel如何按照指定顺序排列

需求&#xff1a;要求按照指定的code出相应的value值&#xff0c;后台可能查出来的code对应的值无法与给定的顺序匹配上&#xff0c;当然可以用代码解决&#xff0c;但是如果想偷懒可以直接用Excel强大的自定义排序解决。 Step1&#xff1a;在Excel中自定义排序&#xff1a;点击…

(19)写一个函数,用”起泡法“对输入的10数字符按由小到大顺序排列

用”起泡法“对输入的10数字符按由小到大顺序排列 起泡法&#xff1a;即每一次将相邻两个数进行比较&#xff0c;若符合条件则交换两个数的值。每进行一趟排序&#xff0c;最大的一个数变成最后一个数。以此类推&#xff0c;直至整个数组按照由小到大排列。 举例&#xff1a;…