• 字段映射
    • 字段映射的类型
  • 字段约束
    • 自增属性
  • 索引
  • 表约束
  • 虚拟表映射
  • 数据库升级
  • 文件与代码模版
    • 文件模版
    • 代码模版

    模型绑定(Object-relational Mapping,简称 ORM),通过对 Swift 类或结构进行绑定,形成类或结构 - 表模型类或结构对象 - 表的映射关系,从而达到通过对象直接操作数据库的目的。

    WCDB Swift 的模型绑定分为五个部分:

    • 字段映射
    • 字段约束
    • 索引
    • 表约束
    • 虚拟表映射
      这其中大部分是格式化的模版代码,我们在最后介绍文件模版和代码提示模版,以简化模型绑定的操作。

    字段映射

    WCDB Swift 的字段映射基于 Swift 4.0 的 Codable 协议实现。以下是一个字段映射的示例代码:

    1. class Sample: TableCodable {
    2. var identifier: Int? = nil
    3. var description: String? = nil
    4. var offset: Int = 0
    5. var debugDescription: String? = nil
    6.  
    7. enum CodingKeys: String, CodingTableKey {
    8. typealias Root = Sample
    9. static let objectRelationalMapping = TableBinding(CodingKeys.self)
    10. case identifier = "id"
    11. case description
    12. case offset = "db_offset"
    13. }
    14. }
    • 在类内定义 CodingKeys 的枚举类,并遵循 StringCodingTableKey
    • 枚举列举每一个需要定义的字段。
    • 对于变量名与表的字段名不一样的情况,可以使用别名进行映射,如 case identifier = "id"
    • 对于不需要写入数据库的字段,则不需要在 CodingKeys 内定义,如 debugDescription
    • 对于变量名与 SQLite 的保留关键字冲突的字段,同样可以使用别名进行映射,如 offset 是 SQLite 的关键字。
    与 Swift 的 Codable 协议不同的是,即便是所有字段都需要绑定,这里也必须列举每一个需要绑定的字段。因为 CodingKeys 除了用于模型绑定,还将用于语言集成查询,我们会在后面章节中介绍。

    字段映射定义完成后,调用 create(table:of:) 接口即可根据这个定义创建表。

    1. // 以下代码等效于 SQL:CREATE TABLE IF NOT EXISTS sampleTable(id INTEGER, description TEXT, db_offset INTEGER)
    2. try database.create(table: "sampleTable", of: Sample.self)

    字段映射的类型

    并非所有类型的变量都支持被绑定为字段。WCDB Swift 内建了常用类型的支持,包括:

    数据库中的类型类型
    32 位整型Bool, Int, Int8, Int16, Int32, UInt, UInt8, UInt16, UInt32
    64 位整型Int64, UInt64, Date
    浮点型Float, Double
    字符串类型String, URL
    二进制类型Data, Array, Dictionary, Set
    其中 Date 以时间戳的形式存储, ArrayDictionarySet 以 JSON 的形式存储。

    对于没有内建支持的类型,开发者可以手动为其添加支持。我们将在自定义字段映射类型一章进一步介绍。

    字段约束

    字段约束是 TableEncodable 的一个可选函数,可根据需求选择实现或不实现。它用于定于针对单个字段的约束,如主键约束、非空约束、唯一约束、默认值等。

    以下是一个字段约束的示例代码:

    1. class Sample: TableCodable {
    2. var identifier: Int? = nil
    3. var description: String? = nil
    4.  
    5. enum CodingKeys: String, CodingTableKey {
    6. typealias Root = Sample
    7. static let objectRelationalMapping = TableBinding(CodingKeys.self)
    8. case identifier
    9. case description
    10.  
    11. static var columnConstraintBindings: [CodingKeys: ColumnConstraintBinding]? {
    12. return [
    13. identifier: ColumnConstraintBinding(isPrimary: true),
    14. description: ColumnConstraintBinding(isNotNull: true, defaultTo: "defaultDescription"),
    15. ]
    16. }
    17. }
    18.  
    19. var isAutoIncrement: Bool = false // 用于定义是否使用自增的方式插入
    20. var lastInsertedRowID: Int64 = 0 // 用于获取自增插入后的主键值
    21. }

    字段约束通过 CodingKeys 到字段约束的字典实现,定义每个 CodingKeys 对应的约束。

    ColumnConstraintBinding 初始化函数的声明如下:

    1. ColumnConstraintBinding(
    2. isPrimary: Bool = false, // 该字段是否为主键。字段约束中只能同时存在一个主键
    3. orderBy term: OrderTerm? = nil, // 当该字段是主键时,存储顺序是升序还是降序
    4. isAutoIncrement: Bool = false, // 当该字段是主键时,其是否支持自增。只有整型数据可以定义为自增。
    5. onConflict conflict: Conflict? = nil, // 当该字段是主键时,若产生冲突,应如何处理
    6. isNotNull: Bool = false, // 该字段是否可以为空
    7. isUnique: Bool = false, // 该字段是否可以具有唯一性
    8. defaultTo defaultValue: ColumnDef.DefaultType? = nil // 该字段在数据库内使用什么默认值
    9. )

    以上约束按需进行定义或者不定义即可。定义完成后,同样调用 create(table:of:) 接口即可根据这个定义创建表。

    1. // 以下代码等效于 SQL:CREATE TABLE IF NOT EXISTS sampleTable(identifier INTEGER PRIMARY KEY, description TEXT NOT NULL DEFAULT 'defaultDescription')
    2. try database.create(table: "sampleTable", of: Sample.self)

    自增属性

    定义了 isPrimary: 的字段,支持以自增的方式进行插入数据。但仍可以通过非自增的方式插入数据。

    当需要进行自增插入时,对象需设置 isAutoIncrement 参数为 true,则数据库会使用 已有数据中最大的值+1 作为主键的值。

    1. let autoIncrementObject = Sample()
    2. autoIncrementObject.isAutoIncrement = true
    3.  
    4. // 插入自增数据
    5. try database.insert(objects: autoIncrementObject, intoTable: "sampleTable")
    6. print(autoIncrementObject.lastInsertedRowID) // 输出 1
    7.  
    8. // 再次插入自增数据
    9. try database.insert(objects: autoIncrementObject, intoTable: "sampleTable")
    10. print(autoIncrementObject.lastInsertedRowID) // 输出 2
    11.  
    12. // 插入非自增的指定数据
    13. let specificObject = Sample()
    14. specificObject.identifier = 10
    15. try database.insert(objects: specificObject, intoTable: "sampleTable")

    对于自增插入的数据,可以在类内定义 lastInsertedRowID 字段,并以此获取插入的值。

    若类只会使用自增的方式插入,而不需要指定值的方式插入,可以在定义时直接设置 isAutoIncrementtrue。如:var isAutoIncrement: Bool { return true }

    索引

    索引是 TableEncodable 的一个可选函数,可根据需求选择实现或不实现。它用于定于针对单个或多个字段的索引,索引后的数据在能有更高的查询效率。

    以下是一个定义索引的示例代码:

    1. class Sample: TableCodable {
    2. var identifier: Int? = nil
    3. var description: String? = nil
    4. var multiIndexPart1: Int = 0
    5. var multiIndexPart2: Int = 0
    6.  
    7. enum CodingKeys: String, CodingTableKey {
    8. typealias Root = Sample
    9. static let objectRelationalMapping = TableBinding(CodingKeys.self)
    10. case identifier
    11. case description
    12. case multiIndexPart1
    13. case multiIndexPart2
    14.  
    15. static var indexBindings: [IndexBinding.Subfix: IndexBinding]? {
    16. return [
    17. "_uniqueIndex": IndexBinding(isUnique: true, indexesBy: identifier),
    18. "_descendingIndex": IndexBinding(indexesBy: description.asIndex(orderBy: .descending)),
    19. "_multiIndex": IndexBinding(indexesBy: multiIndexPart1, multiIndexPart2.asIndex(orderBy: .ascending))
    20. ]
    21. }
    22. }
    23. }

    索引通过索引后缀与索引绑定的映射实现。

    • 对于需要特别指明索引存储顺序的字段,可以通过 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"。

    索引定义完成后,同样调用 create(table:of:) 接口即可根据这个定义创建表。

    1. // 以下代码等效于 SQL:
    2. // CREATE TABLE IF NOT EXISTS sampleTable(identifier INTEGER, description TEXT, multiIndexPart1 INTEGER, multiIndexPart2 INTEGER)
    3. // CREATE UNIQUE INDEX IF NOT EXISTS sampleTable_uniqueIndex on sampleTable(identifier)
    4. // CREATE INDEX IF NOT EXISTS sampleTable_descendingIndex on sampleTable(description DESC)
    5. // CREATE INDEX IF NOT EXISTS sampleTable_multiIndex on sampleTable(multiIndexPart1, multiIndexPart2 ASC)
    6. try database.create(table: "sampleTable", of: Sample.self)

    表约束

    表约束是 TableEncodable 的一个可选函数,可根据需求选择实现或不实现。它用于定于针对多个字段或表本身的约束。

    以下是一个表约束的示例代码:

    1. class Sample: TableCodable {
    2. var identifier: Int? = nil
    3. var multiPrimaryKeyPart1: Int = 0
    4. var multiPrimaryKeyPart2: Int = 0
    5. var multiUniquePart1: Int = 0
    6. var multiUniquePart2: Int = 0
    7.  
    8. enum CodingKeys: String, CodingTableKey {
    9. typealias Root = Sample
    10. static let objectRelationalMapping = TableBinding(CodingKeys.self)
    11. case identifier
    12. case multiPrimaryKeyPart1
    13. case multiPrimaryKeyPart2
    14. case multiUniquePart1
    15. case multiUniquePart2
    16.  
    17. static var tableConstraintBindings: [TableConstraintBinding.Name: TableConstraintBinding]? {
    18. let multiPrimaryBinding =
    19. MultiPrimaryBinding(indexesBy: multiPrimaryKeyPart1.asIndex(orderBy: .descending), multiPrimaryKeyPart2)
    20. let multiUniqueBinding =
    21. MultiUniqueBinding(indexesBy: multiUniquePart1, multiUniquePart2.asIndex(orderBy: .ascending))
    22. return [
    23. "MultiPrimaryConstraint": multiPrimaryBinding,
    24. "MultiUniqueConstraint": multiUniqueBinding
    25. ]
    26. }
    27. }
    28. }

    表约束通过约束名到表约束的映射实现。包含:

    • MultiPrimaryBinding: 联合主键约束
    • MultiUniqueBinding: 联合唯一约束
    • CheckBinding: 检查约束
    • ForeignKeyBinding: 外键约束
      约束的定义方式与索引类似。定义完成后,同样调用 create(table:of:) 接口即可根据这个定义创建表。
    1. // 以下代码等效于 SQL:
    2. // CREATE TABLE IF NOT EXISTS sampleTable(
    3. // identifier INTEGER,
    4. // multiPrimaryKeyPart1 INTEGER,
    5. // multiPrimaryKeyPart2 INTEGER,
    6. // multiUniquePart1 INTEGER,
    7. // multiUniquePart1 INTEGER,
    8. // CONSTRAINT MultiPrimaryConstraint PRIMARY KEY(multiPrimaryKeyPart1 DESC, multiPrimaryKeyPart2),
    9. // CONSTRAINT MultiUniqueConstraint UNIQUE(multiUniquePart1, multiUniquePart2 ASC)
    10. // )
    11. try database.create(table: "sampleTable", of: Sample.self)

    虚拟表映射

    虚拟表映射是 TableEncodable 的一个可选函数,可根据需求选择实现或不实现。它用于定于虚拟表以进行全文搜索等特性。

    普通表不需要用到虚拟表映射,因此这里暂且按下不表,我们会在全文搜索一章中进行介绍。

    数据库升级

    在开发过程中,经过多个版本的迭代后,经常会出现数据库字段升级的情况,如增加新字段、删除或重命名旧字段、新增索引等等。对于 SQLite 本身,其并不支持对字段的删除和重命名。新增加字段则需要考虑不同版本升级等情况。而这个问题通过模型绑定可以很好的解决。

    纵观上述字段映射、字段约束、索引和表约束等四个部分,都是通过调用 create(table:of:) 接口使其生效的。实际上,该接口会将 模型绑定的定义 与 表本身的结构 联系起来,并进行更新。

    对于字段映射:

    • 表已存在但模型绑定中未定义的字段,会被忽略。这可以用于删除字段。
    • 表不存在但模型绑定中有定义的字段,会被新增到表中。这可以用于新增字段。
    • 对于需要重命名的字段,可以通过别名的方式重新映射。
    忽略字段并不会删除字段。对于该字段旧内容,会持续存在在表中,因此文件不会因此变小。实际上,数据库作为持续增长的二进制文件,只有将其数据导出生成另一个新的数据库,才有可能回收这个字段占用的空间。对于新插入的数据,该字段内容为空,不会对性能产生可见的影响。

    对于索引,不存在的索引会被新增到数据库中。

    对于数据库已存在但模型绑定中未定义的索引,create(table:of:) 接口不会自动将其删除。如果需要删除,开发者需要调用 drop(index:) 接口。

    以下是数据库升级的一个例子:

    在第一个版本中,Sample 的模型绑定定义如下,并在数据库创建了以之对应的表 sampleTable。

    1. class Sample: TableCodable {
    2. var identifier: Int? = nil
    3. var description: String? = nil
    4. var createDate: Date? = nil
    5.  
    6. enum CodingKeys: String, CodingTableKey {
    7. typealias Root = Sample
    8. static let objectRelationalMapping = TableBinding(CodingKeys.self)
    9. case identifier
    10. case description
    11. case createDate
    12. }
    13. }
    14.  
    15. try database.create(table: "sampleTable", of: Sample.self)

    到了第二个版本,sampleTable 表进行了升级。

    1. class Sample: TableCodable {
    2. var identifier: Int? = nil
    3. var content: String? = nil
    4. var title: String? = nil
    5.  
    6. enum CodingKeys: String, CodingTableKey {
    7. typealias Root = Sample
    8. static let objectRelationalMapping = TableBinding(CodingKeys.self)
    9. case identifier
    10. case content = "description"
    11. case title
    12. }
    13. static var indexBindings: [IndexBinding.Subfix: IndexBinding]? {
    14. return [
    15. "_index": IndexBinding(indexesBy: title)
    16. ]
    17. }
    18. }
    19.  
    20. try database.create(table: "sampleTable", of: Sample.self)

    可以看到,通过修改模型绑定,并再次调用 create(table:of:)

    • description 字段通过别名的特性,被重命名为了 content
    • 已删除的 createDate 字段会被忽略。
    • 对于新增的 title 会被添加到表中。

    文件与代码模版

    模型绑定的大部分都是格式固定的代码,因此,WCDB Swift 提供了文件模版和代码模版两种方式,以简化模型绑定操作。文件和代码模版都在源代码的 tools/templates 目录下

    • 未获取 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 即可。TableCodableXctemplate

    代码模版

    在代码文件中的任意位置,输入 TableCodableClass 后选择代码模版即可。TableCodableSnippet

    文件和代码模版都是以 class 作为例子的,实际上 struct 甚至 enum 都可以进行模型绑定的。