一、简介
SQLite提供了C/C++的接口,直接在工程中使用的话需要维护一些指针的声明周期,有时候需要自己再去封装一些接口给其他人使用。SQLite.swift是一个使用swift语言设计的SQLite的封装库,提供了一些面向对象的接口,让使用者可以更方便的对SQLite数据库进行操作,在使用Swift进行开发的时候,不妨可以试一试这个库。详细信息可以查看SQLite.swift的Github地址。
去年在小组的开源项目学习中选择了这个库,进行了一些学习,主要目的是为了学习一些经典的开源代码,同时为了公司的swift战略,提前学习下swift相关知识。本文将从源码角度讲解一下SQLite.swift的实现原理,并介绍一些开源项目中的一些语法技巧的应用,阅读这需要了解Swift基本语法,以及SQL相关语法。
二、关键代码解析
2.1 对SQL语句的抽象
创建表的代码如下
// 创建1个表对象,表名为users
let users = Table("users")
// 创建3个列,id、name、email
let id = Expression<Int64>("id")
let name = Expression<String?>("name")
let email = Expression<String>("email")
// 使用db执行语句,创建一个表users,包括3列,
// "id" INTEGER PRIMARY KEY NOT NULL,
// "name" TEXT,
// "email" TEXT NOT NULL UNIQUE
try db.run(users.create { t in
t.column(id, primaryKey: true)
t.column(name)
t.column(email, unique: true)
})
这是什么意思呢,其实很简单,所有代码的唯一目的,就是构建一个创建users表的语句,最终的SQL语句如下:
CREATE TABLE IF NOT EXISTS "users" ("id" INTEGER PRIMARY KEY NOT NULL, "name" TEXT NOT NULL, "email" TEXT UNIQUE)
看到这句SQL是不是会发现很熟悉,那么SQL语句有什么特点呢?可以先简单看一下下图中SQLite中是如何规定创建表的语法
有点复杂,简单来讲创建表的语句结构定义如下,基本上就是“CREATE TABLE”关键字后跟一个新的表名以及括号内的一堆定义和约束,对于前边的创建表的语句,如下形式格式化后,就可以和语法结构定义一致:
CREATE TABLE IF NOT EXISTS "users" (
"id" INTEGER PRIMARY KEY NOT NULL,
"name" TEXT NOT NULL,
"email" TEXT UNIQUE
)
那么有结构,就可以进行抽象。这里介绍两个SQLite.swift中的协议的概念:
Expressible:SQLite.swift将SQL语句中的每一个模块(如CREATE、TABLE、"users"等等),都抽象成了一个Expressible对象,Expression:Expression是Expressible的实现类,是对SQL语句中一部分的抽象,以及与该部分关联的数据(如列数据id、name等)。
那么如何用Expressible表示一个创建表的语句呢,看一下前面user.create方法中的代码:
let clauses: [Expressible?] = [
Expression<Void>(literal: "CREATE"),
modifier.map { Expression<Void>(literal: $0.rawValue) },
Expression<Void>(literal: identifier),
ifNotExists ? Expression<Void>(literal: "IF NOT EXISTS") : nil,
name
"".wrap(builder.definitions) as Expression<Void>
]
解析如下,clauses是一个Expressible?的数组,包含了5个部分:
Expression<Void>(literal: "CREATE"):代表CREATE这个字符串modifier.map { Expression<Void>(literal: $0.rawValue) }:modifier是一个optional的枚举值,在创建表这里可能是"tempory"或者是nil,这里的操作则是将该枚举包装成Expression对象或者为nilExpression<Void>(literal: identifier):identifier是user类的中的静态变量,这里是字符串TABLEifNotExists ? Expression<Void>(literal: "IF NOT EXISTS") : nil:根据ifNotExists字段判断是否要创建一个内容为IF NOT EXISTS的Expression对象name:表的名称,在这里是"users""".wrap(builder.definitions) as Expression<Void>:这里的builder的definitions实际上也是一个Expressible数组,包含了需要创建的所有列(这里就包含id、name、email这三个列),这一步的操作将所有列转化为了一个Expression对象,内容是字符串(\"id\" INTEGER PRIMARY KEY NOT NULL, \"name\" TEXT NOT NULL, \"email\" TEXT UNIQUE)
所以最后clauses这个对象,就是一个包含了创建数据库表SQL语句每一个部分的数组。之后会调用以下代码,将这个对象转换成SQL语句:
" ".join(clauses.compactMap { $0 }).asSQL()
这句话的是将clauses数组中的空对象去除,用字符串空格拼接后,最后调用asSQL将语句中的?替换成对应关联的数据,最终得到了上面的SQL语句
CREATE TABLE IF NOT EXISTS "users" ("id" INTEGER PRIMARY KEY NOT NULL, "name" TEXT NOT NULL, "email" TEXT UNIQUE)
简单总结一下:
SQL语句具有相对固定的结构,而SQLite.swift中将这种结构进行了封装,使用者只需要创建一些关键对象,调用相关的方法(比如create),即可生成对应的SQL语句。
2.2 扩展的应用
SQLite.swift中使用了非常多的extension,将不同的方法放入到不同的文件中,或着实现一些工具方法。这样做有好处,可以将不同功能的代码分散在不同的模块中,不好之处是在阅读代码上需要一些理解成本。对源码中的一些扩展举例如下:
-
对定义的类、结构体扩展
在
Query.swift中定义了Table结构体,在Schema.swift中定义了Table的扩展,实现了create,addColumn,rename等方法。
// Query.swift文件
public struct Table : SchemaType {
public static let identifier = "TABLE"
public var clauses: QueryClauses
public init(...) {...}
}
// Schema.swift文件
extension Table {
public func create(...) -> String {...}
public func addColumn<V : Value>(...) -> String {...}
public func rename(...) -> String {...}
...
}
-
对系统类扩展
前面在拼接字符串时使用的
join方法,功能是将expressible数组转换成一个Expression,内容为使用当前字符串进行拼接,一般的调用方法是"".join(xxx)。
extension String {
func join(_ expressions: [Expressible]) -> Expressible {
var (template, bindings) = ([String](), [Binding?]())
for expressible in expressions {
let expression = expressible.expression
template.append(expression.template)
bindings.append(contentsOf: expression.bindings)
}
return Expression<Void>(template.joined(separator: self), bindings)
}
}
其实在Objective-c中对扩展的应用已经非常普遍,在swift中又得到了进一步的加强。
2.3 泛型的应用
泛型也是在SQLite.swift中使用非常普遍的能力,前面所提到的Expression结构体,就使用了泛型:
let id = Expression<Int64>("id")
let name = Expression<String?>("name")
let email = Expression<String>("email")
可以看到,通过泛型创建了不同类型的列,其中Int64类型的id,String类型可为NULL的name,String类型不可为空的email。
代码中Expression的定义如下:
public struct Expression<Datatype> : ExpressionType {
// 当前的类型
public typealias UnderlyingType = Datatype
// Expression实际的内容
public var template: String
// 内容中待绑定的数据
public var bindings: [Binding?]
}
下面看一个泛型在SQLite.swift中的应用,根据UnderlyingType对不同的泛型实例扩展方法
// Swift中Optional的定义,也就是swift语法中的?类型,其实是一个枚举
@frozen public enum Optional<Wrapped> : ExpressibleByNilLiteral {
case none
case some(Wrapped)
}
// SQLite.swift对于Optional类型重新进行定义
public protocol _OptionalType {
associatedtype WrappedType
}
extension Optional : _OptionalType {
public typealias WrappedType = Wrapped
}
extension ExpressionType where UnderlyingType : _OptionalType, UnderlyingType.WrappedType : Value {
associatedtype UnderlyingType = Void
}
这里主要的目的则是为了获取到Optional对象的类型,然后根据不同的条件来匹配Extension。
首先对系统的Optional扩展,实现了自定义的_OptionalType,将Optional中的Wrapped重新命名为WrappedType,这一步是而为了可以取到当前对象的类型。然后通过where来限定这个ExpressionType的扩展满足的条件则为:
-
当前
ExpressionType实例的泛型类型实现了_OptionalType,也就是是一个Optional类型 -
这个泛型类型中的解包的类型,实现了
Value协议,Value协议也是SQLite.swift定义的类型,系统的Double、Int64、String等类型都实现了扩展实现了该协议。
extension String : Binding, Value {}
所以对于前面所定义的name,就符合这个ExpressionType的扩展条件:
let name = Expression<String?>("name")
这个例子可以说是对泛型、扩展的应用,在Objective-c中是很难实现的。
2.4 运算符重载
在简介的例子中,有这样两个代码,第一次看到的可能不知道是干什么的
name <- "Alice"
email <- "alice@mac.com"
实际上,这里就是重载了运算符<-,看下具体代码:
public func <-<V : Value>(column: Expression<V>, value: V) -> Setter {
return Setter(column: column, value: value)
}
public struct Setter {
let column: Expressible
let value: Expressible
}
可以看到,<-实际上就是创建了一个Setter对象,包含一个column对象和一个value对象,<-左边的就是column,<-右边的就是value。
在官方文档中,也对已经支持的运算符做了说明。
Infix Setters
| Operator | Types | ||
|---|---|---|---|
<- | Value -> Value | ||
+= | Number -> Number | ||
-= | Number -> Number | ||
*= | Number -> Number | ||
/= | Number -> Number | ||
%= | Int -> Int | ||
<<= | Int -> Int | ||
>>= | Int -> Int | ||
&= | Int -> Int | ||
| ` | =` | Int -> Int | |
^= | Int -> Int | ||
+= | String -> String |
Postfix Setters
| Operator | Types |
|---|---|
++ | Int -> Int |
-- | Int -> Int |
三、类图框架
通过前边的简介,应该已经大致了解SQLite.swift的原理,以及一些技巧的运用,接下来通过整体类图,介绍一些关键的协议以及结构体/类:
Expressible
Expressible这个单词是一个形容词,意思为可表达的。在SQLite.swift中的大部分类(结构体)直接或间接实现了该协议,其含义则表示这些结构体或类,都是一个可表达的实体,可以作为SQL语句中的一部分。其中主要包括了几个部分:
QueryType:是Table、View、VirtualTable的基类;ExpressionType:Expression(列或一些固定的成分)、Insert等的基类;Value:各种数据类型的基类,如Double、String等。
Expressible中有一个Expression属性,代表所有可表达的对象,都可以转化为一个字符串表达式,而一个SQL则由不同的表达式组成,包含了固定的基本部分(如CREATE、TABLE)、需要操作的列(如列名)、以及对应的数据,分别由上面的QueryType、ExpressionType、Value来组成。
这里就可以看出,SQLite.swift的设计精髓就是将SQL语句中的每一个部分,都抽象成了一个Expressible对象
Connection
在类图中,还有几个跟Expressible没有关系的类,Connection和Statement,他们主要的职责是负责数据库的创建、连接、执行语句等功能。在程序开始时,我们需要创建一个Connection对象,用来创建或打开数据库,并在数据库的操作过程中持有该对象。使用Connection提供的run方法可以执行SQL语句。
执行过程中,可以通过返回值Statement来判断语句执行的结果,或者遍历Statement对象来获取查询结果。
四、关键流程
这里列举一些关键流程
4.1 创建表
初始化表的顺序如下:
- 使用路径创建
Connection对象,打开数据库连接; - 使用表名创建
Table对象,并调用create方法获取创建表的SQL语句; - 使用
Connection的run方法执行SQL语句; Connection内部则会创建Statement对象,并调用run方法真正执行SQL语句;Statement在初始化时,会调用SQLite的sqlite3_prepare_v2函数准备执行SQL,在run方法内部则会调用sqlite3_step函数执行SQL语句;- 最后则会返回给调用者
Statement对象,以供查询执行结果。
4.2 insert
插入语句的顺序如下:
- 创建
Expression<String>对象,表示一个String类型的列,列名为name; - 通过
<-运算符,将字符串Alice绑定到列name上,调用方式为name <- "Alice",该方法会生成一个Setter对象; - 调用
Table的insert方法,得到一个Insert对象,该对象实际表达的就是将Alice插入到name列的SQL语句; - 调用
Connection对象的run方法,此时调用的顺序和创建表时类似,最终会执行SQL的API,并返回一个Statement对象。
这里Setter本身是一个实现了Expressible协议的对象,在insert方法中,就是创建了一个Expressible数组,代表了插入的SQL语句,最终生成了Insert对象,本身也是一个Expressible的实现类,生成的代码如下:
fileprivate func insert(_ or: OnConflict?, _ values: [Setter]) -> Insert {
let insert = values.reduce((columns: [Expressible](), values: [Expressible]())) { insert, setter in
(insert.columns + [setter.column], insert.values + [setter.value])
}
let clauses: [Expressible?] = [
Expression<Void>(literal: "INSERT"),
or.map { Expression<Void>(literal: "OR \($0.rawValue)") },
Expression<Void>(literal: "INTO"),
tableName(),
"".wrap(insert.columns) as Expression<Void>,
Expression<Void>(literal: "VALUES"),
"".wrap(insert.values) as Expression<Void>,
whereClause
]
return Insert(" ".join(clauses.compactMap { $0 }).expression)
}
五、总结
SQLite.swift的学习和理解成本会比较高,和过往的SQL使用认知不同,需要在SQL的基础上,再学习SQLite.swift的使用方式后,才可以使用。个人认为这个库在实际工程中使用成本有待商榷,但我们可以学习其中的代码设计思想,以及swift语法技巧的应用。