地址在 https://github.com/lalawue/SwiftSQLiteORM.git。
之前在公司实现过一个 Swift SQLite 的 ORM 库,基于 GRDB,ORM 的优势就是方便,不用自己 CURD,也能享受到关系数据库的 ACID(原子性、一致性、完整性与持久性)能力。
只不过 ORM 的方式,搜索能力确实一般而已,比如不好自己组 SQL 搜索子句,以及 join 其他表,或者仅 select 输出某几列。
不过这个输出某几列没有实现的原因,倒不是不好组子句,只是 Swift 是有类型系统的,输出的额外的类型必须得先定义,这个其实跟已有的表结构可能完全不相同,所以省略不做的。
因为之前自己也重构过 Lua 的 SQLite 的 ORM,所以多少熟悉了这个部分(没办法,工作很大一部分就是熟能生巧,在限制场景和条件下找解决方案,一边摸索一边实践,顺利的话,条件满足后就可以输出,当然纯逻辑的的数学、理论物理等这些领域不熟悉,不发表意见)。
先给个定义的例子:
// nested struct will store as JSON string
struct ExampleNested: Codable {
let desc: String
let index: Int
}
struct ExampleType: DBTableDef {
let name: String
let data: ExampleNested
typealias ORMKey = Columns
/// keep blank or return nil for using hidden 'rowid' column
static var primaryKey: Columns? {
return .name
}
enum Columns: String, DBTableKey {
case name
case data
}
}
ExampleType
是需要支持 ORM 的结构,遵循 DBTableDef
,可以是 struct 或者是 class,不要求遵循 Codable 协议。
如上也就定义了数据库中的表结构,其中使用 DBTableKey
定义了数据库中的字段类型(支持仅部分属性进入数据库),这里还额外定义了 primary key 是 name,这样就支持了后续的 deletes([T])
的接口,可以直接传递需要删除的实例进去。
如果结构有嵌套的子结构,比如上面的 ExampleNested
,则需要支持 Codable 协议,因为非内建字段类型 ,默认是 Blob,将 encode 为 JSON 后保存,否则将 throw error。
使用如下:
do {
// insert / update
let c = ExampleType(name: "c", data: ExampleNested(desc: "c", index: 1))
let u = ExampleType(name: "u", data: ExampleNested(desc: "u", index: 2))
try DBMgnt.push([c, u])
// select
let arr = try DBMgnt.fetch(ExampleType.self, .eq(.name, c.name))
XCTAssert(arr.count == 1, "failed")
XCTAssert(arr[0].name == c.name, "failed")
// delete
try DBMgnt.deletes([c]) // require primaryKey in table definition
try DBMgnt.delete(ExampleType.self, .eq(.name, u.name))
let count = try DBMgnt.fetch(ExampleType.self, .eq(.name, c.name)).count
XCTAssert(count == 0, "failed")
// clear
try DBMgnt.clear(ExampleType.self)
// drop table
try DBMgnt.drop(ExampleType.self)
} catch {
if let err = error as? DBORMError {
fatalError("failed to try block: \(err.localizedDescription)")
} else {
fatalError("failed to try block: \(error.localizedDescription)")
}
}
CURD 基本上就是 push、fetch、deletes、delete、clear、drop 这样,其中 fetch、delete 接口支持 DBRecordFilter
,关联 DBTableDef
中的字段定义,计算操作、比较操作都是可以传递属性名称进去的,类型则遵循 SQLite select 的类型转换规定,我大体理解是跟字段定义相关,比如 SQLite 给的字段定义可以是 TEXT、INTEGER、REAL、BLOB。
另外,我只实现了部分内建字段的映射定义,用户可以根据自己需要通过 DBPrimitive
协议,定义某些类型支持映射:
/// database column type
/// - will perform relative type calculation in sql expression
/// - https://sqlite.org/datatype3.html
public enum DBStoreType {
/// Int64
case INTEGER
/// Double
case REAL
/// String, Numeric
case TEXT
/// Data
case BLOB
}
/// database store value type
/// - will sotre as type's DatabaseValueConvertible through GRDB
public enum DBStoreValue {
/// box with int64
case integer(Int64)
/// box with double
case real(Double)
/// box with String
case text(String)
/// box with data
case blob(Data)
}
/// type transform for store / restore from database
public protocol DBPrimitive: DefaultConstructor {
/// database column type
static var ormStoreType: DBStoreType { get }
/// return TypeInfo for mocking, for example objc wrapper NSUUID
static func ormTypeInfo() throws -> TypeInfo
/// mapping value to store in database
func ormToStoreValue() -> DBStoreValue?
/// restore value from database
static func ormFromStoreValue(_ value: DBStoreValue) -> Self?
}
其中的 DBStoreType
是数据库列类型,DBStoreValue
则是将自定义字段的数据通过 String、Int64、Double 或者 Data 传递到数据库,以及读取的时候,数据库也将返回这几种数据,给自定义类型来恢复。
比如最开始例子的 name 属性是 String 类型,将映射为 SQLite 下的 TEXT 列类型。
比如下面例子的 URL 将映射为 TEXT 列类型:
extension URL: DBPrimitive {
public init() {
// will be placed by database value later
self.init(string: "a://a.a")!
}
public static var ormStoreType: SwiftSQLiteORM.DBStoreType { .TEXT }
public func ormToStoreValue() -> SwiftSQLiteORM.DBStoreValue? {
return .text(self.absoluteString)
}
public static func ormFromStoreValue(_ value: SwiftSQLiteORM.DBStoreValue) -> URL? {
guard case .text(let string) = value else {
return nil
}
return URL(string: string)
}
}
上面的例子都没有涉及到如何 connect 数据库,以及建立 table 等,因为这些 ORM 库都自动做了,甚至支持 alter table。
比如需要多增加一个属性,并记录到数据库,只需要升级 DBTableDef
下的 tableVersion
就好,是一个 Double 类型的字段。
当然还支持指定 table 名称,以及使用独立的数据库文件,如下:
extension DBTableDef {
/// ...
/// specify table name or use type name
/// - should be unique in all scope
static var tableName: String { get }
/// schema version for table columns, default 0
/// - increase version after you add columns
static var tableVersion: Double { get }
/// specify database file name or use default
static var databaseName: String { get }
/// ...
}
ORM 需要将 struct、class 的指定字段,映射到 SQLite 中的表结构,对应列的类型需要能保存属性数据,便于后续的计算、比较操作,这里对 Swift 语法结构的获取,用的库是 Runtime,这个也是我在 github 上遇到了 SQLiteORM 后,读取其代码了解到的。
从 Runtime 这个库,可以拿到 Swift 结构的属性列表,以及每一个属性的类型,甚至可以构造这个属性的实例。
比如 fetch
接口操作之后,这个实例的字段数据,是依赖数据库字段读取到的,因为 DBTableDef
支持仅保存部分字段,因此其他字段可以通过下面的函数由用户自己填充,或者使用默认值。
/// Table ORM mapping definition
public protocol DBTableDef {
/// ...
/// update instance property value created by type reflection
/// - only ORMKey covered property can restore value from database column
/// - others property will use default value
static func ormUpdateNew(_ value: inout Self) -> Self
}
Swift 侧的表结构大致可以定义,但是数据库这边我偷懒了,不像 SQLiteORM 那样通过 SQLite 的接口组织操作,而是利用了 GRDB 来做保存、读取。
这次我用了 GRDB.swift/SQLCipher
来做底层存储操作接口,其中的加密用的 password phrase 是库自动生成后,保存到 keychain 里面的。所以每次重新安装后,password phrase 都会不一样,但不影响使用,用户侧也无需知道具体是什么。
GRDB 的读取是通过 Row 结构,使用上类似字典,保存的时候是通过 encode
,也是类似字典,将属性保存到 container 中:
/// insert / update
override func encode(to container: inout PersistenceContainer) {
/// ...
}
其中遇到的一些问题,以及不大好解决的问题记录大概有:
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 接口来完成,估计会更好一些,后面再说吧。
大概就是这样。