上一篇:IOS数据存储 之WCDB (一)讲解了WCDB的一些特性,重点讲解了对比FMDB的用法,但是这些用法基本都是面向OC语言的。本篇主要讲解Swift语言版本基本使用。
使用WCDB.swift的demo 下载:点击下载kylWCDBSwiftDemo
WCDB
的最基础的调用过程大致分为三个步骤:
- 模型绑定
- 创建数据库与表
- 操作数据
WCDB
基于 Swift 4.0
的 Codable
协议实现模型绑定的过程。class Sample {
var identifier: Int? = nil
var description: String? = nil
}
Sample
类的 identifier
和 description
两个变量绑定到了表中同名字段:class Sample: TableCodable {
var identifier: Int? = nil
var description: String? = nil
enum CodingKeys: String, CodingTableKey {
typealias Root = Sample
static let objectRelationalMapping = TableBinding(CodingKeys.self)
case identifier
case description
}
}
One line of code
是 WCDB
接口设计的一个基本原则,绝大部分的便捷接口都可以通过一行代码完成。let database = Database(withPath: "~/Intermediate/Directories/Will/Be/Created/sample.db")
// 以下代码等效于 SQL:CREATE TABLE IF NOT EXISTS sampleTable(identifier INTEGER, description TEXT)
try database.create(table: "sampleTable", of: Sample.self)
//Prepare data
let object = Sample()
object.identifier = 1
object.description = "sample_insert"
//Insert
try database.insert(objects: object, intoTable: "sampleTable")
let objects: [Sample] = try database.getObjects(fromTable: "sampleTable")
//Prepare data
let object = Sample()
object.description = "sample_update"
//Update
try database.update(table: "sampleTable",
on: Sample.Properties.description,
with: object,
where: Sample.Properties.identifier > 0)
//类似 Sample.Properties.identifier > 0 的语法是 WCDB 的特性,它能通过 Swift 语法来进行 SQL 操作
try database.delete(fromTable: "sampleTable")
Object-relational Mapping
,简称 ORM
),通过对 Swift
类或结构进行绑定,形成类或结构 - 表模型、类或结构对象 - 表的映射关系,从而达到通过对象直接操作数据库的目的。
- 字段映射
- 字段约束
- 索引
- 表约束
- 虚拟表映射
WCDB Swift
的字段映射基于 Swift 4.0
的 Codable
协议实现。以下是一个字段映射的示例代码:class Sample: TableCodable {
var identifier: Int? = nil
var description: String? = nil
var offset: Int = 0
var debugDescription: String? = nil
enum CodingKeys: String, CodingTableKey {
typealias Root = Sample
static let objectRelationalMapping = TableBinding(CodingKeys.self)
case identifier = "id"
case description
case offset = "db_offset"
}
}
- 在类内定义 CodingKeys 的枚举类,并遵循 String 和 CodingTableKey。
- 枚举列举每一个需要定义的字段。
- 对于变量名与表的字段名不一样的情况,可以使用别名进行映射,如 case identifier = “id”
- 对于不需要写入数据库的字段,则不需要在 CodingKeys 内定义,如 debugDescription
- 对于变量名与 SQLite 的保留关键字冲突的字段,同样可以使用别名进行映射,如 offset 是 SQLite 的关键字。
与 Swift 的 Codable 协议不同的是,即便是所有字段都需要绑定,这里也必须列举每一个需要绑定的字段。因为 CodingKeys 除了用于模型绑定,还将用于语言集成查询,我们会在后面章节中介绍。
create(table:of:)
接口即可根据这个定义创建表。// 以下代码等效于 SQL:CREATE TABLE IF NOT EXISTS sampleTable(id INTEGER, description TEXT, db_offset INTEGER)
try database.create(table: "sampleTable", of: Sample.self)
数据库中的类型 | 类型 |
---|---|
32 位整型 | Bool, Int, Int8, Int16, Int32, UInt, UInt8, UInt16, UInt32 |
64 位整型 | Int64, UInt64, Date |
浮点型 | Float, Double |
字符串类型 | String, URL |
二进制类型 | Data, Array, Dictionary, Set |
其中 Date 以时间戳的形式存储, Array、Dictionary、Set 以 JSON 的形式存储。
TableEncodable
的一个可选函数,可根据需求选择实现或不实现。它用于定于针对单个字段的约束,如主键约束、非空约束、唯一约束、默认值等。class Sample: TableCodable {
var identifier: Int? = nil
var description: String? = nil
enum CodingKeys: String, CodingTableKey {
typealias Root = Sample
static let objectRelationalMapping = TableBinding(CodingKeys.self)
case identifier
case description
static var columnConstraintBindings: [CodingKeys: ColumnConstraintBinding]? {
return [
identifier: ColumnConstraintBinding(isPrimary: true),
description: ColumnConstraintBinding(isNotNull: true, defaultTo: "defaultDescription"),
]
}
}
var isAutoIncrement: Bool = false // 用于定义是否使用自增的方式插入
var lastInsertedRowID: Int64 = 0 // 用于获取自增插入后的主键值
}
CodingKeys
到字段约束的字典实现,定义每个 CodingKeys
对应的约束。ColumnConstraintBinding
初始化函数的声明如下:ColumnConstraintBinding(
isPrimary: Bool = false, // 该字段是否为主键。字段约束中只能同时存在一个主键
orderBy term: OrderTerm? = nil, // 当该字段是主键时,存储顺序是升序还是降序
isAutoIncrement: Bool = false, // 当该字段是主键时,其是否支持自增。只有整型数据可以定义为自增。
onConflict conflict: Conflict? = nil, // 当该字段是主键时,若产生冲突,应如何处理
isNotNull: Bool = false, // 该字段是否可以为空
isUnique: Bool = false, // 该字段是否可以具有唯一性
defaultTo defaultValue: ColumnDef.DefaultType? = nil // 该字段在数据库内使用什么默认值
)
create(table:of:)
接口即可根据这个定义创建表。// 以下代码等效于 SQL:CREATE TABLE IF NOT EXISTS sampleTable(identifier INTEGER PRIMARY KEY, description TEXT NOT NULL DEFAULT 'defaultDescription')
try database.create(table: "sampleTable", of: Sample.self)
isPrimary
: 的字段,支持以自增的方式进行插入数据。但仍可以通过非自增的方式插入数据。isAutoIncrement
参数为 true
,则数据库会使用 已有数据中最大的值+1 作为主键的值。let autoIncrementObject = Sample()
autoIncrementObject.isAutoIncrement = true
// 插入自增数据
try database.insert(objects: autoIncrementObject, intoTable: "sampleTable")
print(autoIncrementObject.lastInsertedRowID) // 输出 1
// 再次插入自增数据
try database.insert(objects: autoIncrementObject, intoTable: "sampleTable")
print(autoIncrementObject.lastInsertedRowID) // 输出 2
// 插入非自增的指定数据
let specificObject = Sample()
specificObject.identifier = 10
try database.insert(objects: specificObject, intoTable: "sampleTable")
若类只会使用自增的方式插入,而不需要指定值的方式插入,可以在定义时直接设置
isAutoIncrement
为true
。如:var isAutoIncrement: Bool { return true }
TableEncodable
的一个可选函数,可根据需求选择实现或不实现。它用于定于针对单个或多个字段的索引,索引后的数据在能有更高的查询效率。class Sample: TableCodable {
var identifier: Int? = nil
var description: String? = nil
var multiIndexPart1: Int = 0
var multiIndexPart2: Int = 0
enum CodingKeys: String, CodingTableKey {
typealias Root = Sample
static let objectRelationalMapping = TableBinding(CodingKeys.self)
case identifier
case description
case multiIndexPart1
case multiIndexPart2
static var indexBindings: [IndexBinding.Subfix: IndexBinding]? {
return [
"_uniqueIndex": IndexBinding(isUnique: true, indexesBy: identifier),
"_descendingIndex": IndexBinding(indexesBy: description.asIndex(orderBy: .descending)),
"_multiIndex": IndexBinding(indexesBy: multiIndexPart1, multiIndexPart2.asIndex(orderBy: .ascending))
]
}
}
}
- 对于需要特别指明索引存储顺序的字段,可以通过
asIndex(orderBy:)
函数指定,如description.asIndex(orderBy: .descending)。
- 对于具有唯一性的索引,可以通过
isUnique:
参数指定,如 IndexBinding(isUnique: true, indexesBy: identifier)
。- 对于由多个字段组成的联合索引,可以通过
indexesBy:
进行指定,如(indexesBy: multiIndexPart1, multiIndexPart2.asIndex(orderBy: .ascending))
create(table:of:)
接口即可根据这个定义创建表。// 以下代码等效于 SQL:
// CREATE TABLE IF NOT EXISTS sampleTable(identifier INTEGER, description TEXT, multiIndexPart1 INTEGER, multiIndexPart2 INTEGER)
// CREATE UNIQUE INDEX IF NOT EXISTS sampleTable_uniqueIndex on sampleTable(identifier)
// CREATE INDEX IF NOT EXISTS sampleTable_descendingIndex on sampleTable(description DESC)
// CREATE INDEX IF NOT EXISTS sampleTable_multiIndex on sampleTable(multiIndexPart1, multiIndexPart2 ASC)
try database.create(table: "sampleTable", of: Sample.self)
TableEncodable
的一个可选函数,可根据需求选择实现或不实现。它用于定于针对多个字段或表本身的约束。class Sample: TableCodable {
var identifier: Int? = nil
var multiPrimaryKeyPart1: Int = 0
var multiPrimaryKeyPart2: Int = 0
var multiUniquePart1: Int = 0
var multiUniquePart2: Int = 0
enum CodingKeys: String, CodingTableKey {
typealias Root = Sample
static let objectRelationalMapping = TableBinding(CodingKeys.self)
case identifier
case multiPrimaryKeyPart1
case multiPrimaryKeyPart2
case multiUniquePart1
case multiUniquePart2
static var tableConstraintBindings: [TableConstraintBinding.Name: TableConstraintBinding]? {
let multiPrimaryBinding =
MultiPrimaryBinding(indexesBy: multiPrimaryKeyPart1.asIndex(orderBy: .descending), multiPrimaryKeyPart2)
let multiUniqueBinding =
MultiUniqueBinding(indexesBy: multiUniquePart1, multiUniquePart2.asIndex(orderBy: .ascending))
return [
"MultiPrimaryConstraint": multiPrimaryBinding,
"MultiUniqueConstraint": multiUniqueBinding
]
}
}
}
- MultiPrimaryBinding: 联合主键约束
- MultiUniqueBinding: 联合唯一约束
- CheckBinding: 检查约束
- ForeignKeyBinding: 外键约束
create(table:of:)
接口即可根据这个定义创建表。// 以下代码等效于 SQL:
// CREATE TABLE IF NOT EXISTS sampleTable(
// identifier INTEGER,
// multiPrimaryKeyPart1 INTEGER,
// multiPrimaryKeyPart2 INTEGER,
// multiUniquePart1 INTEGER,
// multiUniquePart1 INTEGER,
// CONSTRAINT MultiPrimaryConstraint PRIMARY KEY(multiPrimaryKeyPart1 DESC, multiPrimaryKeyPart2),
// CONSTRAINT MultiUniqueConstraint UNIQUE(multiUniquePart1, multiUniquePart2 ASC)
// )
try database.create(table: "sampleTable", of: Sample.self)
虚拟表映射是 TableEncodable
的一个可选函数,可根据需求选择实现或不实现。它用于定于虚拟表以进行全文搜索等特性。
普通表不需要用到虚拟表映射,因此这里暂且按下不表,我们会在全文搜索一章中进行介绍。
在开发过程中,经过多个版本的迭代后,经常会出现数据库字段升级的情况,如增加新字段、删除或重命名旧字段、新增索引等等。 对于 SQLite 本身,其并不支持对字段的删除和重命名。新增加字段则需要考虑不同版本升级等情况。而这个问题通过模型绑定可以很好的解决。
纵观上述字段映射、字段约束、索引和表约束等四个部分,都是通过调用 create(table:of:) 接口使其生效的。 实际上,该接口会将 模型绑定的定义 与 表本身的结构 联系起来,并进行更新。
对于字段映射:
- 表已存在但模型绑定中未定义的字段,会被忽略。这可以用于删除字段。
- 表不存在但模型绑定中有定义的字段,会被新增到表中。这可以用于新增字段。
- 对于需要重命名的字段,可以通过别名的方式重新映射。
忽略字段并不会删除字段。对于该字段旧内容,会持续存在在表中,因此文件不会因此变小。实际上,数据库作为持续增长的二进制文件,只有将其数据导出生成另一个新的数据库,才有可能回收这个字段占用的空间。对于新插入的数据,该字段内容为空,不会对性能产生可见的影响。
对于数据库已存在但模型绑定中未定义的索引,
create(table:of:)
接口不会自动将其删除。如果需要删除,开发者需要调用drop(index:)
接口。
class Sample: TableCodable {
var identifier: Int? = nil
var description: String? = nil
var createDate: Date? = nil
enum CodingKeys: String, CodingTableKey {
typealias Root = Sample
static let objectRelationalMapping = TableBinding(CodingKeys.self)
case identifier
case description
case createDate
}
}
try database.create(table: "sampleTable", of: Sample.self)
到了第二个版本,sampleTable 表进行了升级。
class Sample: TableCodable {
var identifier: Int? = nil
var content: String? = nil
var title: String? = nil
enum CodingKeys: String, CodingTableKey {
typealias Root = Sample
static let objectRelationalMapping = TableBinding(CodingKeys.self)
case identifier
case content = "description"
case title
}
static var indexBindings: [IndexBinding.Subfix: IndexBinding]? {
return [
"_index": IndexBinding(indexesBy: title)
]
}
}
try database.create(table: "sampleTable", of: Sample.self)
- description 字段通过别名的特性,被重命名为了 content。
- 已删除的 createDate 字段会被忽略。
- 对于新增的 title 会被添加到表中。
未获取 WCDB 的 Github 仓库的开发者,可以在命令执行 curl https://raw.githubusercontent.com/Tencent/wcdb/master/tools/templates/install.sh -s | sh
已获取 WCDB 的 Github 仓库的开发者,可以手动执行 cd path-to-your-wcdb-dir/tools/templates; sh install.sh; 。
文件模版安装完成后,在 Xcode 的菜单 File -> New -> File… 中创建新文件,选择 TableCodable。 在弹出的菜单中输入文件名,并选择 Language 为 Swift 即可。
在代码文件中的任意位置,输入 TableCodableClass 后选择代码模版即可。
文件和代码模版都是以 class 作为例子的,实际上 struct 甚至 enum 都可以进行模型绑定的。
插入操作有 “insert” 和 “insertOrReplace” 两个接口。故名思义,前者只是单纯的插入数据,当数据出现冲突时会失败,而后者在主键一致时,新数据会覆盖旧数据。
以已经完成模型绑定的类 Sample 为例:
class Sample: TableCodable {
var identifier: Int? = nil
var description: String? = nil
enum CodingKeys: String, CodingTableKey {
typealias Root = Sample
static let objectRelationalMapping = TableBinding(CodingKeys.self)
case identifier
case description
static var columnConstraintBindings: [CodingKeys: ColumnConstraintBinding]? {
return [
identifier: ColumnConstraintBinding(isPrimary: true),
]
}
}
}
try database.create(table: "sampleTable", of: Sample.self)
let object = Sample()
sample.identifier = 1
sample.description = "insert"
try database.insert(objects: object, intoTable: "sampleTable") // 插入成功
try database.insert(objects: object, intoTable: "sampleTable") // 插入失败,因为主键 identifier = 1 已经存在
sample.description = "insertOrReplace"
try database.insertOrReplace(objects: object, intoTable: "sampleTable") // 插入成功
// insert 和 insertOrReplace 函数只有函数名不同,其他参数都一样。
func insert<Object: TableEncodable>(
objects: [Object], // 需要插入的对象。WCDB Swift 同时实现了可变参数的版本,因此可以传入一个数组,也可以传入一个或多个对象。
on propertyConvertibleList: [PropertyConvertible]? = nil, // 需要插入的字段
intoTable table: String // 表名
) throws
这里需要特别注意的是 propertyConvertibleList
参数,它是 遵循 PropertyConveritble
协议的对象的数组。我们会在语言集成查询进一步介绍。这里只需了解,它可以传入模型绑定中定义的字段,如 Sample.Properties.identifier
。
当不传入 propertyConvertibleList
参数时,"insert
" 或 “insertOrReplace
” 接口会使用所有定义的字段进行插入。而 propertyConvertibleList
不为空时,"insert
" 或 “insertOrReplace
” 只会插入指定的字段,这就构成了部分插入。
let object = Sample()
sample.identifier = 1
sample.description = "insert"
try database.insert(objects: object, on: Sample.Properties.identifier, intoTable: "sampleTable") // 部分插入,没有指定 description。
这个例子中,指定了只插入 identifier
字段,因此其他没有指定的字段,会使用 模型绑定中定义的默认值 或 空 来代替。这里 description
没有定义默认值,因此其数据为空。
插入是最常用且比较容易操作卡顿的操作,因此 WCDB Swift 对其进行了特殊处理。 当插入的对象数大于 1 时,WCDB Swift 会自动开启事务,进行批量化地插入,以获得更新的性能。
func delete(fromTable table: String, // 表名
where condition: Condition? = nil, // 符合删除的条件
orderBy orderList: [OrderBy]? = nil, // 排序的方式
limit: Limit? = nil, // 删除的个数
offset: Offset? = nil // 从第几个开始删除
) throws
删除接口会删除表内的数据,并通过 condition
、orderList
、limit
和 offset
参数来确定需要删除的数据的范围。
这四个组合起来可以理解为:将 table
表内,满足 condition
的数据,按照 orderList
的方式进行排序,然后从头开始第 offset
行数据后的 limit
行数据删除。
// 删除 sampleTable 中所有 identifier 大于 1 的行的数据
try database.delete(fromTable: "sampleTable",
where: Sample.Properties.identifier > 1)
// 删除 sampleTable 中 identifier 降序排列后的前 2 行数据
try database.delete(fromTable: "sampleTable",
orderBy: Sample.Properties.identifier.asOrder(by: .descending),
limit: 2)
// 删除 sampleTable 中 description 非空的数据,按 identifier 降序排列后的前 3 行的后 2 行数据
try database.delete(fromTable: "sampleTable",
where: Sample.Properties.description.isNotNull(),
orderBy: Sample.Properties.identifier.asOrder(by: .descending),
limit: 2,
offset: 3)
// 删除 sampleTable 中的所有数据
try database.delete(fromTable: "sampleTable")
condition
、limit
和 offset
本质都是遵循 ExpressionConvertible
的对象,可以是数字、字符串、字段或其他更多的组合。同样地,我们会在语言集成查询进一步介绍。drop(table:)
接口删除表。func update<Object: TableEncodable>(
table: String,
on propertyConvertibleList: [PropertyConvertible],
with object: Object,
where condition: Condition? = nil,
orderBy orderList: [OrderBy]? = nil,
limit: Limit? = nil,
offset: Offset? = nil) throws
func update(
table: String,
on propertyConvertibleList: [PropertyConvertible],
with row: [ColumnEncodableBase],
where condition: Condition? = nil,
orderBy orderList: [OrderBy]? = nil,
limit: Limit? = nil,
offset: Offset? = nil) throws
其中 propertyConvertibleList
、condition
、limit
和 offset
都在前文介绍过了,这里不再赘述。 两个接口除了 with 之后的参数,其他都一致。
“with object” 故名思义,通过 object 对象进行更新。以下是更新操作的示例代码:
let object = Sample()
object.description = "update"
// 将 sampleTable 中所有 identifier 大于 1 且 description 字段不为空 的行的 description 字段更新为 "update"
try database.update(table: "sampleTable"
on: Sample.Properties.description,
with: object,
where: Sample.Properites.identifier > 1 && Sample.Properties.description.isNotNull())
// 将 sampleTable 中前三行的 description 字段更新为 "update"
try database.update(table: "sampleTable"
on: Sample.Properties.description,
with: object,
limit: 3)
let row: [ColumnCodableBase] = ["update"]
// 将 sampleTable 中所有 identifier 大于 1 且 description 字段不为空 的行的 description 字段更新为 "update"
try database.update(table: "sampleTable"
on: Sample.Properties.description,
with: row,
where: Sample.Properites.identifier > 1 && Sample.Properties.description.isNotNull())
// 将 sampleTable 中前三行的 description 字段更新为 "update"
try database.update(table: "sampleTable"
on: Sample.Properties.description,
with: row,
limit: 3)
- getObjects
- getObject
- getRows
- getRow
- getColumn
- getDistinctColumn
- getValue
- getDistinctValue
虽然接口较多,但大部分都是为了简化操作而提供的便捷接口。实现上其实与 update 类似,只有 “object” 和 “row” 两种方式。
getObjects
” 和 “getObject
” 都是对象查找的接口,他们直接返回已进行模型绑定的对象。它们的函数原型为:func getObjects<Object: TableDecodable>(
on propertyConvertibleList: [PropertyConvertible],
fromTable table: String,
where condition: Condition? = nil,
orderBy orderList: [OrderBy]? = nil,
limit: Limit? = nil,
offset: Offset? = nil) throws -> [Object]
func getObject<Object: TableDecodable>(
on propertyConvertibleList: [PropertyConvertible],
fromTable table: String,
where condition: Condition? = nil,
orderBy orderList: [OrderBy]? = nil,
offset: Offset? = nil) throws -> Object?
propertyConvertibleList
、condition
、limit
和 offset
都在前文介绍过了,这里不再赘述。 而 “getObject
” 等价于 limit: 1 时的 “getObjects” 接口。不同的是,它直接返回 Object
对象,而不是一个数组,使用上更便捷。 以下是对象查找操作的示例代码:// 返回 sampleTable 中的所有数据
let allObjects: [Sample] = try database.getObjects(fromTable: "sampleTable")
// 返回 sampleTable 中 identifier 小于 5 或 大于 10 的行的数据
let objects: [Sample] = try database.getObjects(fromTable: "sampleTable",
where: Sample.Properties.identifier < 5 || Sample.Properties.identifier > 10)
// 返回 sampleTable 中 identifier 最大的行的数据
let object: Sample? = try database.getObject(fromTable: "sampleTable",
orderBy: Sample.Properties.identifier.asOrder(by: .descending))
由于对象查找操作使用了范型,因此需要显式声明返回值的类型以匹配范型。否则会报错
let allObjects = try database.getObjects(fromTable: "sampleTable")
// 没有显式声明allObjects
类型,范型无法匹配,无法编译通过。
let objects: [Sample] = try database.getObjects(fromTable: "sampleTable",
on: Sample.Properties.identifier)
identifier
字段,而没有获取 description
的值。这就可能与 Swift
本身存在冲突。 Swift 规定了对象创建时,必须初始化所有成员变量。而进行对象部分查询时,则可能出现某部分变量没有变查询,因此无法初始化的情况。因此,对于可能不被查询的成员变量,应将其类型定义为可选值。 对于 Sample 类中,上述 “getObjects
” 接口虽然没有获取 description
的值,但由于 description
是 String
? 类型,因此不会出错。 而以下则是会出错的例子:class PartialSample: TableCodable {
var identifier: Int? = nil
var description: String = ""
enum CodingKeys: String, CodingTableKey {
typealias Root = PartialSample
static let objectRelationalMapping = TableBinding(CodingKeys.self)
case identifier
case description
}
}
// 由于 description 是 String 类型,"getObject" 过程无法对其进行初始化,因此以下调用会出错。
// 正确的方式应将 `var description: String` 改为 `var description: String?`
let partialObjects: [PartialSample] = try database.getObjects(fromTable: "sampleTable", on: Sample.Properties.identifier)
func getRows(on columnResultConvertibleList: [ColumnResultConvertible],
fromTable table: String,
where condition: Condition? = nil,
orderBy orderList: [OrderBy]? = nil,
limit: Limit? = nil,
offset: Offset? = nil) throws -> FundamentalRowXColumn
其中 condition、orderList、limit 和 offset,前文已经介绍,这里不再赘述。 columnResultConvertibleList 是遵循 ColumnResultConvertible 协议的对象数组,我们会在语言集成查询进一步介绍。
这里只需了解 Sample.Properties.identifier.max() 是遵循 ColumnResultConvertible 协议的对象,用于查找 identifier 列的最大值。
试考虑,表中的数据可以想象为一个矩阵的存在,假设其数据如下:
identifier | description |
---|---|
1 | “sample1” |
2 | “sample1” |
3 | “sample2” |
4 | “sample2” |
5 | “sample2” |
- “getRows” 接口获取整个矩阵的所有内容,即返回值为二维数组。
- “getRow” 接口获取某一横行的数据,即返回值为一维数组。
- “getColumn” 接口获取某一纵列的数据,即返回值为一维数组。
- “getDistinctColumn” 与 “getColumn” 类似,但它会过滤掉重复的值。
- “getValue” 接口获取矩阵中某一个格的内容。
- “getDistinctValue” 与 “getValue” 类似,但它会过滤掉重复的值。
// 获取所有内容
let allRows = try database.getRows(fromTable: "sampleTable")
print(allRows[row: 2, column: 0].int32Value) // 输出 3
// 获取第二行
let secondRow = try database.getRow(fromTable: "sampleTable", offset: 1)
print(secondRow[0].int32Value) // 输出 2
// 获取 description 列
let descriptionColumn = try database.getColumn(on: Sample.Properties.description, fromTable: "sampleTable")
print(descriptionColumn) // 输出 "sample1", "sample1", "sample1", "sample2", "sample2"
// 获取不重复的 description 列的值
let distinctDescriptionColumn = try database.getDistinctColumn(on: Sample.Properties.description, fromTable: "sampleTable")
print(distinctDescriptionColumn) // 输出 "sample1", "sample2"
// 获取第二行 description 列的值
let value = try database.getValue(on: Sample.Properties.description, offset: 1)
print(value.stringValue) // 输出 "sample1"
// 获取 identifier 的最大值
let maxIdentifier = try database.getValue(on: Sample.Properties.identifier.max(), fromTable: "sampleTable")
// 获取不重复的 description 的值
let distinctDescription = try database.getDistinctValue(on: Sample.Properties.description, fromTable: "sampleTable")
print(distinctDescription.stringValue) // 输出 "sample1"
- 支持增删查改的便捷接口
- 支持链式接口
- 数据和状态共享
- 线程安全
let myTag = 1
let database = Database(withPath: filePath)
database.tag = myTag
print(database.tag) // 输出 1
let databaseAlias = Database(withPath: filePath)
print(databaseAlias.tag) // 输出 1
let table = try database.getTable(named: "sampleTable", of: Sample.self)
print(table.tag) // 输出 1
let transaction = try database.getTransaction()
print(transaction.tag) // 输出 1
database.tag = 2
print(database.tag) // 输出 2
print(databaseAlias.tag) // 输出 2
print(table.tag) // 输出 2
print(transaction.tag) // 输出 2
基础类共享数据和状态的本质是,它们共享同一个 Core,而所有操作都在这个 Core 上发生。
WCDB Swift 支持 多线程读操作 或 单线程写多线程读 并发执行。
let filePath = "~/Intermediate/Directories/Will/Be/Created/sample.db"
let databaseWithPath = Database(withPath: filePath)
let fileURL = URL(fileURLWithPath: filePath)
let databaseWithFileURL = Database(withFileURL: fileURL)
let myTag = 1
databaseWithPath.tag = myTag
// 若该 tag 不存在,则初始化会失败
let databaseWithTag = try Database(withExistingTag: myTag)
let myTag1 = 1
let database1 = Database(withPath: path1)
database1.tag = myTag1
let myTag2 = 2
let database2 = Database(withPath: path2)
database2.tag = myTag2
print(database1.tag) // 输出 1
let anotherDatabase1 = Database(withPath: path1)
print(anotherDatabase1.tag) // 输出 1
let database = Database(withPath: filePath)
print(database.isOpened) // 输出 false
try database.create(table: "sampleTable", of: Sample.self) // 数据库此时会被自动打开
print(database.isOpened) // 输出 true
let database1 = Database(withPath: filePath)
print(database1.isOpened) // 输出 false
print(database1.canOpen) // 输出 true。仅当数据库无法打开时,如路径无法创建等,该接口会返回 false
print(database1.isOpened) // 输出 true
let database2 = Database(withPath: filePath)
print(database2.isOpened) // 输出 true。WCDB Swift 同一路径的数据库共享数据和状态等。
do {
let database1 = Database(withPath: filePath)
try database1.create(table: "sampleTable", of: Sample.self) // 数据库此时会被自动打开
print(database1.isOpened) // 输出 true
} // 作用域结束,database1 deinit、关闭数据库并回收内存
let database2 = Database(withPath: filePath)
print(database2.isOpened) // 输出 false
let database1 = Database(withPath: filePath)
{
let database2 = Database(withPath: filePath)
try database2.create(table: "sampleTable", of: Sample.self) // 数据库此时会被自动打开
print(database2.isOpened) // 输出 true
} // 作用域结束,database2 deinit,但 database1 仍持有该路径的数据库,因此不会被关闭。
print(database1.isOpened) // 输出 true
let database = Database(withPath: filePath)
print(database.canOpen) // 输出 true
print(database.isOpened) // 输出 true
database.close()
print(database.isOpened) // 输出 false
WCDB Swift 也提供了 blockade、unblockade 和 isBlockaded 接口用于分步执行关闭数据库操作,可参考相关接口文档
某些情况下,开发者需要确保数据库完全关闭后才能进行操作,如移动文件操作。
数据库是二进制文件,移动文件的过程中若数据发生了变化,则移动后的文件数据可能会不完整、损坏。因此,WCDB Swift 提供了 close: 接口。
try database.close(onClosed: {
try database.moveFiles(toDirectory: otherDirectory)
})
在 onClosed
参数内,可确保数据库完全关闭,不会有其他线程的数据访问、操作数据库,因此可以安全地操作文件。
purge
接口用于回收暂不使用的内存。// 回收 database 数据库中暂不使用的内存
database.purge()
// 回收所有已创建的数据库中暂不使用的内存
Database.purge()
Database.purge()
接口以减少内存占用。// 获取所有与该数据库相关的文件路径
print(database.paths)
// 获取所有与该数据库相关的文件占用的大小
try database.close(onClosed: {
// 数据库未关闭状态下也可获取文件大小,但不够准确,开发者可自行选择是否关闭
let filesSize = try database.getFilesSize()
print(filesSize)
})
// 删除所有与该数据库相关的文件
try database.close(onClosed: {
try database.removeFiles()
})
// 将所有与该数据库相关的文件移动到另一个目录
try database.close(onClosed: {
try database.moveFiles(toDirectory: otherDirectory)
})
let table = try database.getTable(named: "sampleTable", of: Sample.self) // 表不存在时会出错
// 返回值需指定为 [Sample] 类型以匹配范型
let objectsFromDatabase: [Sample] = try database.getObjects(fromTable: "sampleTable")
// table 已经指定了表名和模型绑定的类,因此可以直接获取
let objectsFromTable = try table.getObjects()
try database.run(transaction: {
try database.insert(objects: object, intoTable: "sampleTable")
})
let table = try database.getTable(named: "sampleTable", of: Sample.self)
table.run(transaction: {
try database.insert(objects: object)
})
// 与 Database、Table 类似,开发者可以保存 Transaction 变量
let transaction = try database.getTransaction()
transacton.run(transaction: {
print(transaction.isInTransction) // 输出 true
try transaction.insert(objects: object)
})
let object = Sample()
object.isAutoIncrement = true
let objects = Array(repeating: object, count: 100000)
// 单独插入,效率很差
for object in objects {
try database.insert(objects: object, intoTable: "sampleTable")
}
// 事务插入,性能较好
try database.run(transaction: {
for object in objects {
try database.insert(objects: object, intoTable: "sampleTable")
}
})
// insert(objects:intoTable:) 接口内置了事务,并对批量数据做了针对性的优化,性能更好
try database.insert(objects: objects, intoTable: "sampleTable")
DispatchQueue(label: "other thread").async {
try database.delete(fromTable: "sampleTable")
}
try database.insert(object: object, intoTable: "sampleTable")
let objects = try database.getObjects(fromTable: "sampleTable")
print(objects.count) // 可能输出 0 或 1
getObjects(fromTable:)
无法取出刚才插入的数据,且这种多线程低概率的 bug 是很难查的。DispatchQueue(label: "other thread").async {
try database.delete(fromTable: "sampleTable")
}
try database.run(transaction: {
try database.insert(objects: object, intoTable: "sampleTable")
let objects = try database.getObjects(fromTable: "sampleTable")
print(objects.count) // 输出 1
})
- 普通事务
- 可控事务
- 嵌入事务
// 普通事务
try database.run(transaction: {
try database.insert(objects: object, intoTable: "sampleTable")
})
// 可控事务
try database.run(controllableTransaction: {
try database.insert(objects: object, intoTable: "sampleTable")
return true // 返回 true 以提交事务,返回 false 以回滚事务
})
可控事务在普通事务的基础上,可以通过返回值控制提交或回滚事务
// 普通事务
try database.run(transaction: {
try database.run(transaction: { // 出错,事务不能嵌套
try database.insert(objects: object, intoTable: "sampleTable")
})
})
// 嵌入事务
try database.run(transaction: {
try database.run(embeddedTransaction: { // 嵌入事务可以嵌套
try database.insert(objects: object, intoTable: "sampleTable")
})
})
- 嵌入事务在普通事务的基础上,支持嵌套调用。
- 当外层不存在事务时,嵌入事务和普通事务没有区别。
- 当外层存在事务时,嵌入事务会跟随外层事务的行为提交或回滚事务。
insert(objects:intoTable:)、insertOrReplace(objects:intoTable:)、create(table:of:) 等 WCDB Swift 自带的接口都使用了嵌入事务
WCDB Swift 也提供了 begin、commit 和 rollback 接口用于分步执行事务,可参考相关接口文档
let objects: [Sample] = try database.getObjects(fromTable: "sampleTable", where: Sample.Properties.idetifier > 1)
where:
参数后的 Sample.Properties.idetifier > 1
就是语言集成查询的其中一个写法。其虽然是 identifier
和数字 1 的比较,但其结果并不为 Bool 值,而是 Expression
。该 Expression
作为 SQL 的 where
参数,用于数据库查询。let identifierColumn = Column(named: "identifier")
let statementInsert = StatementInsert().insert(intoTable: "sampleTable",
with: identifierColumn)
.values(1)
print(statementInsert.description) // 输出 "INSERT INTO sampleTable(identifier) VALUES(1)"
ColumnResult
通常代表数据库查询中的结果,如 Select 语句中的 result-column,指定了期望查询的结果。let identifierColumn = Column(named: "identifier")
let identifierColumnResult = ColumnResult(with: identifierColumn)
let statementSelect = StatementSelect().select(identifierColumnResult).from("sampleTable")
print(statementSelect.description) // 输出 "SELECT identifier FROM sampleTable"
可以看到,Expression 也可以转换为 ColumnResult。
我们再回到 StatementSelect
语句的 select 函数,倘若它只接受 ColumnResult
类作为参数,那么每次调用时,都需要将 Expression
转换为 ColumnResult
。
// 以下为示例代码,并非 WCDB Swift 真正的实现
class StatementSelect {
func select(_ columnResult: ColumnResult...) -> StatementSelect
// ...
}
let identifierColumn = Column(named: "identifier")
let identifierExpression = Expression(identifierColumn)
let identifierColumnResult = ColumnResult(with: identifierExpression)
let statementSelect = StatementSelect().select(identifierColumnResult).from("sampleTable")
print(statementSelect.description) // 输出 "SELECT identifier FROM sampleTable"
可以看到,需要 3 重转换,才能将 Column 转换为我们需要的 ColumnResult。
为了解决这个问题,WCDB Swift 定义了 Convertible 协议,用于语法中可互相转换的类型。
// StatementSelect.swift
func select(distinct: Bool = false,
_ columnResultConvertibleList: ColumnResultConvertible...
) -> StatementSelect
基于 Convertible
协议,select
接口的参数也为 ColumnResultConvertible
,即所有可转换为 ColumnResult
的类型,都能作为 select 函数的参数。
在 SQL 语法中,Expression
是能转换为 ColumnResult
的类型;而 Column
是能转换为 Expression
的类型,因此其也同时是能转换为 ColumnResult
的类型。
// WCDB Swift 内部的代码示例
protocol ExpressionConvertible: ColumnResultConvertible { /* ... */ }
struct Column: ExpressionConvertible { /* ... */ }
struct Expression: ColumnResultConvertible { /* ... */ }
因此,原来的 select 语句可以直接简写为:
let identifierColumn = Column(named: "identifier")
let statementSelect = StatementSelect().select(identifierColumn).from("sampleTable")
print(statementSelect.description) // 输出 "SELECT identifier FROM sampleTable"
WCDB Swift 内的 Convertible
接口协议较多,这里不一一赘述。开发者也无需逐一了解,在使用时再查阅接口即可。
Convertible
协议转换,也可以手动调用 asXXX 接口转换。如 ColumnResultConvertible
的协议为:protocol ColumnResultConvertible {
func asColumnResult() -> ColumnResult
}
let identifierColumn = Column(named: "identifier")
let identifierColumnResult = identifierColumn.asColumnResult()
CodingKeys
和 Property
严格来说不属于语言集成查询的一部分,它们是语言集成查询和模型绑定结合的产物。
当需要通过对象来操作数据库时,如"getObject
“或者”update with object
"等,WCDB Swift 不仅需要知道查找数据的哪个字段(即 Column 所完成的事情),还需要知道这个字段对应模型绑定中的哪个变量。
let property = Sample.Properties.identifier.asProperty()
let objects: [Sample] = try database.getObjects(on: property, fromTable: "sampleTable")
而 CodingKeys
和 Property
就是存储了数据库字段和模型绑定字段的映射关系。不同的是,后者可以分离这个映射,具体用法可参考高级用法一章的查询重定向。
基于模型绑定,开发者可以完全摆脱通过字符串创建 Column,更便捷地操作数据库。
let statementSelect = StatementSelect().select(Sample.Properties.identifier).from("sampleTable")
print(statementSelect.description) // 输出 "SELECT identifier FROM sampleTable"
Sample.Properties.identifier
和 Sample.CodingKeys.identifier
的写法没有区别。Properties
只是 CodingKeys
的别名 - typealias Properties = CodingKeys
。这是为了让阅读代码时语义更加清晰。let expressionInt = Expression(with: 1)
let expressionDouble = Expression(with: 2.0)
let expressionString = Expression(with: "3")
let expressionData = Expression(with: "4".data(using: .ascii)!)
let expressionColumn = Expression(with: Column(named: "identifier"))
除此之外,还有一个内建的绑定参数 Expression.bindParameter 也是 Expression 类型。
let expression = expressionColumn.between(expressionInt, expressionDouble)
print(expression.description) // 输出 "identifier BETWEEN 1 AND 2.0"
- || 运算符在 SQL 语法中用于字符串链接,而在 WCDB Swift 中则是用于"或"的逻辑运算。
- <> 运算符在 SQL 语法中用于不等比较,而在 WCDB Swift 中则是直接使用较为习惯的 != 运算符。
let expression1 = expressionInt + expressionDouble
print(expression1.description) // 输出 "(1 + 2.0)"
let expression2 = expressionColumn >= expression1
print(expression2.description) // 输出 "(identifier >= (1 + 2.0))"
// 基础类型 -1 可以直接转换为 Expression
let expression3 = expressionColumn < -1 || expression2
print(expression3.description) // 输出 "((identifier < -1) OR (identifier >= (1 + 2.0)))"
let statementSelect = StatementSelect().select(Sample.Properties.identifier)
.from("sampleTable")
.where(expression3)
print(statementSelect.description) // 输出 "SELECT identifier FROM sampleTable WHERE ((identifier < -1) OR (identifier >= (1 + 2.0)))"
let expression = Sample.Properties.identifier.asExpression() > 1
print(expression)
let statementSelect = StatementSelect().select(Sample.Properties.identifier)
.from("sampleTable")
.where(Sample.Properties.identifier < -1 || Sample.Properties.identifier >= 3.0)
print(statementSelect.description) // 输出 "SELECT identifier FROM sampleTable WHERE ((identifier < -1) OR (identifier >= 3.0))"
Statement 在前文已经接触到不少了,如查询 StatementSelect、插入 StatementInsert 等。它是一个最基本的完整可被执行的 SQL 语句。
SQLite 共包含 27 种 Statement,WCDB Swift 基本都支持。根据语法规则创建的 Statement,可以通过基础类的 exec(? 函数直接执行,可以通过 prepare(? 创建 CoreStatement 对象做进一步的操作。我们会在高级接口的核心层接口一节详细介绍。
- 在语法中确定其所属的 Statement。
- 对照对应 Statement 的语法,根据关键字对已有的 SQL 进行断句。
- 逐个通过语言集成查询的函数调用进行实现。
SELECT identifier.min() FROM sampleTable WHERE (identifier > 0 || identifier / 2 == 0 ) && description NOT NULL ORDER BY identifier ASC LIMIT 1, 100
let statementSelect = StatementSelect()
SELECT identifier.min()
FROM sampleTable
WHERE (identifier > 0 || identifier / 2 == 0 ) && description NOT NULL
ORDER BY identifier ASC
LIMIT 1, 100
// distinct 参数的默认值为 false,也可以忽略不写
statementSelect.select(distinct: false, Sample.Properties.identifier.min())
statementSelect.from("sampleTable")
statementSelect.where((Sample.Properties.identifier > 0 || Sample.Properties.identifier / 2 == 0) && Sample.Properties.description.isNotNull())
statementSelect.order(by: Sample.Properties.identifier.asOrder(by: .ascending))
statementSelect.limit(from: 1, to: 100)
- PRAGMA locking_mode=“NORMAL”
- PRAGMA synchronous=“NORMAL”
- PRAGMA journal_mode=“WAL”
- PRAGMA fullfsync=1
// 该接口等同于配置 PRAGMA cipher="YourPassword",PRAGMA cipher_page_size=4096
func setCipher(key optionalKey: Data?, // 密码
pageSize: Int = 4096) // 加密页大小
其中,pageSize 是加密的页大小参数,SQLCipher 在 iOS 上默认使用 4096,macOS 上默认使用 1024。而 WCDB Swift 则在所有平台上都适用 4096,以获得更好的性能。开发者一般不需要做特别的修改。
值得注意的是,设置密码是一件很慎重的事情。对于已经创建且存在数据的数据库,无论是原本未加密想要改为加密,还是已经加密想要修改密码,都是成本非常高的操作,因此不要轻易使用。更多相关信息可以参考官方文档
func setConfig(named name: String, // 配置名称,同名的配置会互相覆盖
with callback: @escaping Config, // 配置的函数
orderBy order: ConfigOrder) // 配置执行的顺序,顺序较小的配置优先执行。WCDB Swift 自带的配置从 0 开始。
database.setConfig(named: "secure_delete", with: { (handle: Handle) throws in
let statementPragmaSecureDelete = StatementPragma().pragma(.secureDelete, to: true)
try handle.exec()
}, orderBy: 10)
Database
、Table
或 Transaction
操作数据库的方式。它们是经过封装的便捷接口,其实质都是通过调用链式接口完成的。let select: Select = try database.prepareSelect(of: Sample.self, fromTable: "sampleTable")
let objects: [Sample] = select.where(Sample.Properties.identifier > 1).limit(from: 2, to: 3).allObjects()
let delete: Delete = try database.prepareDelete(fromTable: "sampleTable")
.where(Sample.Properties.identifier != 0)
try delete.execute()
print(delete.changes) // 获取该操作删除的行数
Insert
、Update
、Delete
、Select
、RowSelect
或 MultiSelect
对象。 这些对象的基本接口都返回其 self,因此可以链式连续调用。 最后调用对应的函数使其操作生效,如 allObjects
、execute(with:)
等。let select: Select = try database.prepareSelect(of: Sample.self, fromTable: "sampleTable")
.where(Sample.Properties.identifier > 1)
.limit(from: 2, to: 3)
while let object = try select.nextObject() {
print(object)
}
Insert
、Update
、Delete
、Select
和 RowSelect
都有其对应的增删查改接口。而 MultiSelect
则不同。 MultiSelect
用于联表查询,在某些场景下可提供性能,获取更多关联数据等。以下是联表查询的示例代码:try database.create(table: "sampleTable", of: Sample.self)
try database.create(table: "sampleTableMulti", of: SampleMulti.self)
let multiSelect = try database.prepareMultiSelect(
on:
Sample.Properties.identifier.in(table: "sampleTable"),
Sample.Properties.description.in(table: "sampleTable"),
SampleMulti.Properties.identifier.in(table: "sampleTableMulti"),
SampleMulti.Properties.description.in(table: "sampleTableMulti"),
fromTables: tables,
where: Sample.Properties.identifier.in(table: "sampleTable") == SampleMulti.Properties.identifier.in(table: "sampleTableMulti")
)
while let multiObject = try multiSelect.nextMultiObject() {
let sample = multiObject["sampleTable"] as? Sample
let sampleMulti = multiObject["sampleTableMulti"] as? SampleMulti
// ...
}
sampleTable
” 和 “sampleTableMulti
” 表联合起来,取出它们中具有相等 identifier
值的数据。 多表查询时,所有同名字段都需要通过 in(table:)
接口指定表名,否则会因为无法确定其属于哪个表从而出错。 查询到的 multiObject
是表名到对象的映射,取出后进行类型转换即可。let object = try database.getObject(on: Sample.Properties.identifier.max().as(Sample.Properties.identifier),
fromTable: "sampleTable")
print(object.identifier)
WCDB Swift
通过其封装的各种接口,简化了最常用的增删查改操作。但 SQL
的操作千变万化,仍会有部分功能无法通过便捷接口满足。此时则可以使用核心层接口。 核心层接口通过 CoreStatement
,其本质 SQLite C 接口部分接口的封装,结合语言集成查询可以实现几乎所有 SQL 能够完成的操作。// 1. 创建 Statement
let statement = StatementSelect().select(Column.any).from("sampleTable")
// 2. 创建 CoreStatement
let coreStatement = try database.prepare(statement)
// 3. 逐步执行 CoreStatment
while try coreStatement.step() {
// 4. 获取值
let identifier: Int? = coreStatement.value(atIndex: 0)
let description: String? = coreStatement.value(atIndex: 1)
// ...
}
// 5. 释放 CoreStatement。其 deinit 时也会自动释放
// try coreStatement.finalize()