地址在 https://github.com/lalawue/SwiftSQLiteORM.git。
之前在公司实现过一个 Swift SQLite 的 ORM 库,基于 GRDB,ORM 的优势就是方便,不用自己 CURD,也能享受到关系数据库的 ACID(原子性、一致性、完整性与持久性)能力。
只不过 ORM 的方式,搜索能力确实一般而已,比如不好自己组 SQL 搜索子句,以及 join 其他表,或者仅 select 输出某几列。
不过这个输出某几列没有实现的原因,倒不是不好组子句,只是 Swift 是有类型系统的,输出的额外的类型必须得先定义,这个其实跟已有的表结构可能完全不相同,所以省略不做的。
因为之前自己也重构过 Lua 的 SQLite 的 ORM,所以多少熟悉了这个部分(没办法,工作很大一部分就是熟能生巧,在限制场景和条件下找解决方案,一边摸索一边实践,顺利的话,条件满足后就可以输出,当然纯逻辑的的数学、理论物理等这些领域不熟悉,不发表意见)。
先给个定义的例子:
ExampleType
是需要支持 ORM 的结构,遵循 DBTableDef
,可以是 struct 或者是 class,不要求遵循 Codable 协议。
如上也就定义了数据库中的表结构,其中使用 DBTableKey
定义了数据库中的字段类型(支持仅部分属性进入数据库),这里还额外定义了 primary key 是 name,这样就支持了后续的 deletes([T])
的接口,可以直接传递需要删除的实例进去。
如果结构有嵌套的子结构,比如上面的 ExampleNested
,则需要支持 Codable 协议,因为非内建字段类型 ,默认是 Blob,将 encode 为 JSON 后保存,否则将 throw error。
使用如下:
CURD 基本上就是 push、fetch、deletes、delete、clear、drop 这样,其中 fetch、delete 接口支持 DBRecordFilter
,关联 DBTableDef
中的字段定义,计算操作、比较操作都是可以传递属性名称进去的,类型则遵循 SQLite select 的类型转换规定,我大体理解是跟字段定义相关,比如 SQLite 给的字段定义可以是 TEXT、INTEGER、REAL、BLOB。
另外,我只实现了部分内建字段的映射定义,用户可以根据自己需要通过 DBPrimitive
协议,定义某些类型支持映射:
其中的 DBStoreType
是数据库列类型,DBStoreValue
则是将自定义字段的数据通过 String、Int64、Double 或者 Data 传递到数据库,以及读取的时候,数据库也将返回这几种数据,给自定义类型来恢复。
比如最开始例子的 name 属性是 String 类型,将映射为 SQLite 下的 TEXT 列类型。
比如下面例子的 URL 将映射为 TEXT 列类型:
上面的例子都没有涉及到如何 connect 数据库,以及建立 table 等,因为这些 ORM 库都自动做了,甚至支持 alter table。
比如需要多增加一个属性,并记录到数据库,只需要升级 DBTableDef
下的 tableVersion
就好,是一个 Double 类型的字段。
当然还支持指定 table 名称,以及使用独立的数据库文件,如下:
ORM 需要将 struct、class 的指定字段,映射到 SQLite 中的表结构,对应列的类型需要能保存属性数据,便于后续的计算、比较操作,这里对 Swift 语法结构的获取,用的库是 Runtime,这个也是我在 github 上遇到了 SQLiteORM 后,读取其代码了解到的。
从 Runtime 这个库,可以拿到 Swift 结构的属性列表,以及每一个属性的类型,甚至可以构造这个属性的实例。
比如 fetch
接口操作之后,这个实例的字段数据,是依赖数据库字段读取到的,因为 DBTableDef
支持仅保存部分字段,因此其他字段可以通过下面的函数由用户自己填充,或者使用默认值。
Swift 侧的表结构大致可以定义,但是数据库这边我偷懒了,不像 SQLiteORM 那样通过 SQLite 的接口组织操作,而是利用了 GRDB 来做保存、读取。
这次我用了 GRDB.swift/SQLCipher
来做底层存储操作接口,其中的加密用的 password phrase 是库自动生成后,保存到 keychain 里面的。所以每次重新安装后,password phrase 都会不一样,但不影响使用,用户侧也无需知道具体是什么。
GRDB 的读取是通过 Row 结构,使用上类似字典,保存的时候是通过 encode
,也是类似字典,将属性保存到 container 中:
其中遇到的一些问题,以及不大好解决的问题记录大概有:
SQLite 不支持保存 UInt64,所以这个类型,我是保存为了 TEXT,记录、恢复当然是没有问题的,只是在计算、比较的时候,我大概理解 SQLite 应该是 cast 为了 TEXT 来进行比较的吧。对比之下,GRDB 几乎是不支持 UInt64。
NSDecimalNumber 其实在 Swift 里面就是 Decimal,但在 ObjC 里面,是 NSNumber 的子类,在 GRDB 里面,是被认为是 NSNumber,然后内部比较后来保存的,GRDB 对于 NSDecimalNumber 仅支持保存 >=Int64.min 和 <=Int64.max 的值。
而在本库 NSDecimalNumber 是作为 TEXT 保存,短板也是类似 UInt64。
同样的 NSNumber 本库也是作为 TEXT 保存,估计计算、比较的时候会有点问题吧,但基本存取操作在边界上是没啥问题的。
等于是将计算、比较任务,都扔给 SQLite 列类型来处理了,比如 Decimal 跟 Real 的比较,等于是 SQLite 中 TEXT 字段和输入 REAL 值的比较了。
也许这部分后续可以改进一下,利用 GRDB 的 argument 参数,使用 SQLite 的 bind 接口来完成,估计会更好一些,后面再说吧。
大概就是这样。