用代码生成的思路做Sqlite的C++ ORM

有一个未发布的小项目,涉及到本地数据库读写和加密,本来在Apple平台上用Swift实现,为了移植到其他平台的考虑,正在换成C++实现非GUI部分。在此前的Swift版本,我把Sqlite的C API封装为一个「穷人版」的ORM类。但这个实现使用起来遇到过度设计的问题,想调整一下做些非常规查询很麻烦。

因为要换成C++,考虑到C++模板的功能比Swift强大,也许可以做得更多。一开始的想法非常宏大(个人项目的常见毛病),想的是用C++模板表达和SQL关系代数同等的约束(举个例子,比如用了HAVING子句,SELECT的范围就会受限制)。但这个坑太大,经过缩减变为:给定一组要查询的字段名,和一组字段类型与字段名关联的表定义,查询时通过字段名即可自动推导出结果类型。但这个坑依然很大,用到了不少可变参数模板,并不容易调试,实际使用起来的效果也不尽如人意。

整理一下思路,有了新想法:前面的设计,是在试图用静态类型约束描述一组动态概念(SQL)。比如我在SQL里查询3个字段,而在获取结果时试图读4个字段,正常封装的做法是抛异常或者返回错误,我想要在编译时就发现这是个错误。但既然要做的查询就那些,为什么不可以把动态的部分和静态部分隔离开,类似Rust里的unsafe?这样我只要保证动态部分逻辑上不会出错就行了。如何保证呢?用脚本生成访问的代码就好了。

所以我拿起最熟悉的Ruby,整出了如下的DSL:

model = ModelGenerator.new

model.migrate do |step|
  step.define_table 'users', if_not_exists: true do |table|
    table.primary_key :uuid, 'id'
    table.integer 'role', nullable: false
    table.text 'username', nullable: false
    table.text 'password_digest', nullable: false
    table.real 'updated', nullable: false
    table.real 'created', nullable: false
  end

  step.define_table 'posts', if_not_exists: true do |table|
    table.primary_key :uuid, 'id'
    table.text 'title', nullable: false
    table.text 'content', nullable: false
    table.integer 'type', nullable: false
    table.real 'created', nullable: false
    table.foreign_key 'post_user_id', 'users', 'id'
  end
end

model.alias_model 'user', 'users'
model.alias_model 'post', 'posts'

model.generate!

这个DSL可以生成一个Repository类,其中包含常见的对users和posts的增删改查方法。如果想加入新的方法,DSL也有特殊方法指定要查询的字段。得益于Sqlite的数据类型只有四类(BLOB、TEXT、INTEGER和REAL),它们都可以轻松映射到C++的std::vector<std::byte>std::stringint64_tdouble类型。多个查询字段的结果则会被返回为一个std::tuple类型。

DSL还定义了迁移的概念。后面如果对数据库表有任何修改,往下添加新的migrate块调用就可以。生成器会生成一个临时的Sqlite数据库,把定义的所有修改都在临时数据库里运行一遍,最后通过Sqlite API得到最终的表定义。通过这种方式,我们就不用模拟任何表的迁移过程了——再怎么模拟还能有真的在Sqlite里跑一遍方便吗?

当然这个DSL目前还欠缺很多东西。因为是项目自用,有什么欠缺的后面需要了再加上。但我对这个思路很满意,整个生成器核心只有一百多行Ruby代码,剩下几百行是针对C++的ERB模板。理论上,后面还可以生成Java、C#、Swift等语言的代码。

其实我比较认可Svelte这种工具的思路。良好的转译器能够省下很多麻烦。理想情况里,通过一个核心语言(可以是Vala这样专门设计的语言,也可以是已有语言如JavaScript的子集),经过编译能够在不同平台生成核心的业务和GUI代码,比如在Windows生成C#,Android生成Kotlin,iOS生成Swift代码等。剩下的部分只需要在每个平台补充少量支持代码。这个思路可能因为不够通用,也还是需要一些原生技能,所以还没看到有什么例子。(有Svelte Native,但和我预想的似乎不同)

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注