WCDB Swift 的模型绑定分为五个部分:
- 字段映射
- 字段约束
- 索引
- 表约束
- 虚拟表映射
这其中大部分是格式化的模版代码,我们在最后介绍文件模版和代码提示模版,以简化模型绑定的操作。
字段映射
WCDB Swift 的字段映射基于 。以下是一个字段映射的示例代码:
- 在类内定义
CodingKeys
的枚举类,并遵循String
和CodingTableKey
。 - 枚举列举每一个需要定义的字段。
- 对于变量名与表的字段名不一样的情况,可以使用别名进行映射,如
case identifier = "id"
- 对于不需要写入数据库的字段,则不需要在
CodingKeys
内定义,如debugDescription
- 对于变量名与 SQLite 的保留关键字冲突的字段,同样可以使用别名进行映射,如
offset
是 SQLite 的关键字。
字段映射定义完成后,调用 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)
并非所有类型的变量都支持被绑定为字段。WCDB Swift 内建了常用类型的支持,包括:
其中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:)
接口即可根据这个定义创建表。
当需要进行自增插入时,对象需设置 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")
对于自增插入的数据,可以在类内定义 lastInsertedRowID
字段,并以此获取插入的值。
索引
索引是 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))
完整的索引名为表名+索引后缀,如:表 "sampleTable" 的索引分别为 "sampleTable_uniqueIndex"、"sampleTable_descendingIndex" 和 "sampleTable_multiIndex"。
索引定义完成后,同样调用 接口即可根据这个定义创建表。
- // 以下代码等效于 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
的一个可选函数,可根据需求选择实现或不实现。它用于定于针对多个字段或表本身的约束。
以下是一个表约束的示例代码:
表约束通过约束名到表约束的映射实现。包含:
MultiPrimaryBinding
: 联合主键约束MultiUniqueBinding
: 联合唯一约束CheckBinding
: 检查约束ForeignKeyBinding
: 外键约束
约束的定义方式与索引类似。定义完成后,同样调用create(table:of:)
接口即可根据这个定义创建表。
- // 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:)
接口。
以下是数据库升级的一个例子:
在第一个版本中,Sample
的模型绑定定义如下,并在数据库创建了以之对应的表 sampleTable。
- 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)
可以看到,通过修改模型绑定,并再次调用 create(table:of:)
description
字段通过别名的特性,被重命名为了content
。- 已删除的
createDate
字段会被忽略。 - 对于新增的
title
会被添加到表中。
文件与代码模版
模型绑定的大部分都是格式固定的代码,因此,WCDB Swift 提供了文件模版和代码模版两种方式,以简化模型绑定操作。文件和代码模版都在源代码的 tools/templates
目录下
- 已获取 WCDB 的 Github 仓库的开发者,可以手动执行
cd path-to-your-wcdb-dir/tools/templates; sh install.sh;
。
文件模版安装完成后,在 Xcode 的菜单 File
-> New
-> File…
中创建新文件,选择 TableCodable
。在弹出的菜单中输入文件名,并选择 Language 为 Swift 即可。