IOS数据存储 之WCDB (二)WCDB.swift使用篇

IOS数据存储 之WCDB (二)WCDB.swift使用篇

  • 1.WCDB.Swfit基础使用
    • 1.1 WCDB.Swfit 简介
      • 1.1.1 模型绑定
      • 1.1.2 创建数据库与表
      • 1.1.3 操作数据
        • 1.1.3.1 插入操作
        • 1.1.3.2 查找操作
        • 1.1.3.3 更新操作
        • 1.1.3.4 删除操作
    • 1.2. 模型绑定
      • 1.2.1 Swift 模型绑定
      • 1.2.2 字段映射
        • 1.2.2.1 字段映射的类型
      • 1.2.3 字段约束
        • 1.2.3.1 自增属性
      • 1.2.4 索引
      • 1.2.5 表约束
      • 1.2.6 虚拟表映射
      • 1.2.7 数据库升级
      • 1.2.8 文件与代码模版
        • 1.2.8.1 文件模版
    • 1.3. 增删改查
      • 1.3.1 插入操作
      • 1.3.2 删除操作
      • 1.3.3 更新操作
      • 1.3.4 查找操作
        • 1.3.4.1 对象查找操作
        • 1.3.4.2 对象部分查询
        • 1.3.4.3 值查询操作
    • 1.4. 数据库,表,事务
      • 1.4.1 数据库
        • 1.4.1.1 初始化
        • 1.4.1.2 标签
        • 1.4.1.3 打开数据库
        • 1.4.1.4 关闭数据库
        • 1.4.1.5 关闭数据库与线程安全
        • 1.4.1.6 内存回收
        • 1.4.1.7 文件操作
      • 1.4.2 表
      • 1.4.3 事务
        • 1.4.3.1 性能
        • 1.4.3.2 原子性
        • 1.4.3.3 执行事务
    • 1.5. 语言集成查询
      • 1.5.1 Column
      • 1.5.2 ColumnResult
      • 1.5.3 Convertible
      • 1.5.4 手动转换
      • 1.5.5 CodingKeys 和 Property
      • 1.5.6 Expression
      • 1.5.7 ExpressionOperable
      • 1.5.8 Statement
      • 1.5.9 SQL 到语言集成查询
        • 1.5.9.1 归类
        • 1.5.9.2 断句
        • 1.5.9.3 调用语言集成查询
  • 2.WCDB.Swfit 高级功能
    • 2.1 加密与配置
      • 2.1.1 默认配置
      • 2.1.2 加密
      • 2.1.3 自定义配置
    • 2.2 高级接口
      • 2.2.1 链式调用
      • 2.2.2 遍历查询
      • 2.2.3 联表查询
      • 2.2.4 查询重定向
      • 2.2.5 核心层接口
    • 2.3 监控与错误处理
    • 2.4 自定义字段映射类型
    • 2.5 全文搜索
    • 2.6 损坏,备份,修复

  • 上一篇:IOS数据存储 之WCDB (一)讲解了WCDB的一些特性,重点讲解了对比FMDB的用法,但是这些用法基本都是面向OC语言的。本篇主要讲解Swift语言版本基本使用。

  • 使用WCDB.swift的demo 下载:点击下载kylWCDBSwiftDemo

1.WCDB.Swfit基础使用

1.1 WCDB.Swfit 简介

  • WCDB 的最基础的调用过程大致分为三个步骤:
  1. 模型绑定
  2. 创建数据库与表
  3. 操作数据

1.1.1 模型绑定

  • WCDB 基于 Swift 4.0Codable 协议实现模型绑定的过程。
  • 对于已经存在的 Sample 类:
class Sample {
    var identifier: Int? = nil
    var description: String? = nil
}
  • 可通过以下代码将 Sample 类的 identifierdescription 两个变量绑定到了表中同名字段:
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
    }
}
  • 这部分代码基本都是固定模版。

1.1.2 创建数据库与表

  • One line of codeWCDB 接口设计的一个基本原则,绝大部分的便捷接口都可以通过一行代码完成。
  • 创建数据库对象
    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)

1.1.3 操作数据

  • 基本的增删查改同样是 One line of code

1.1.3.1 插入操作

//Prepare data
let object = Sample()
object.identifier = 1
object.description = "sample_insert"
//Insert
try database.insert(objects: object, intoTable: "sampleTable")

1.1.3.2 查找操作

let objects: [Sample] = try database.getObjects(fromTable: "sampleTable")

1.1.3.3 更新操作

//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 操作

1.1.3.4 删除操作

try database.delete(fromTable: "sampleTable")

1.2. 模型绑定

1.2.1 Swift 模型绑定

  • 模型绑定Object-relational Mapping,简称 ORM),通过对 Swift 类或结构进行绑定,形成类或结构 - 表模型、类或结构对象 - 表的映射关系,从而达到通过对象直接操作数据库的目的。
  • WCDB Swift 的模型绑定分为五个部分:
  1. 字段映射
  2. 字段约束
  3. 索引
  4. 表约束
  5. 虚拟表映射
  • 这其中大部分是格式化的模版代码,我们在最后介绍文件模版和代码提示模版,以简化模型绑定的操作。

1.2.2 字段映射

  • WCDB Swift 的字段映射基于 Swift 4.0Codable 协议实现。以下是一个字段映射的示例代码:
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"
    }
}
  1. 在类内定义 CodingKeys 的枚举类,并遵循 String 和 CodingTableKey。
  2. 枚举列举每一个需要定义的字段。
  3. 对于变量名与表的字段名不一样的情况,可以使用别名进行映射,如 case identifier = “id”
  4. 对于不需要写入数据库的字段,则不需要在 CodingKeys 内定义,如 debugDescription
  5. 对于变量名与 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)

1.2.2.1 字段映射的类型

  • 并非所有类型的变量都支持被绑定为字段。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 以时间戳的形式存储, Array、Dictionary、Set 以 JSON 的形式存储。

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

1.2.3 字段约束

  • 字段约束是 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)

1.2.3.1 自增属性

  • 定义了 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")
  • 对于自增插入的数据,可以在类内定义 lastInsertedRowID 字段,并以此获取插入的值。

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

1.2.4 索引

  • 索引是 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))
            ]
        }
    }
}
  • 索引通过索引后缀与索引绑定的映射实现。
  1. 对于需要特别指明索引存储顺序的字段,可以通过 asIndex(orderBy:) 函数指定,如 description.asIndex(orderBy: .descending)。
  2. 对于具有唯一性的索引,可以通过 isUnique: 参数指定,如 IndexBinding(isUnique: true, indexesBy: identifier)
  3. 对于由多个字段组成的联合索引,可以通过 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)

1.2.5 表约束

  • 表约束是 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
            ]
        }
    }
}
  • 表约束通过约束名到表约束的映射实现。包含:
  1. MultiPrimaryBinding: 联合主键约束
  2. MultiUniqueBinding: 联合唯一约束
  3. CheckBinding: 检查约束
  4. 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)

1.2.6 虚拟表映射

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

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

1.2.7 数据库升级

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

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

  • 对于字段映射:

  1. 表已存在但模型绑定中未定义的字段,会被忽略。这可以用于删除字段。
  2. 表不存在但模型绑定中有定义的字段,会被新增到表中。这可以用于新增字段。
  3. 对于需要重命名的字段,可以通过别名的方式重新映射。

忽略字段并不会删除字段。对于该字段旧内容,会持续存在在表中,因此文件不会因此变小。实际上,数据库作为持续增长的二进制文件,只有将其数据导出生成另一个新的数据库,才有可能回收这个字段占用的空间。对于新插入的数据,该字段内容为空,不会对性能产生可见的影响。

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

对于数据库已存在但模型绑定中未定义的索引,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:)
  1. description 字段通过别名的特性,被重命名为了 content。
  2. 已删除的 createDate 字段会被忽略。
  3. 对于新增的 title 会被添加到表中。

1.2.8 文件与代码模版

  • 模型绑定的大部分都是格式固定的代码,因此,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; 。

1.2.8.1 文件模版

文件模版安装完成后,在 Xcode 的菜单 File -> New -> File… 中创建新文件,选择 TableCodable。 在弹出的菜单中输入文件名,并选择 Language 为 Swift 即可。

IOS数据存储 之WCDB (二)WCDB.swift使用篇_第1张图片

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

IOS数据存储 之WCDB (二)WCDB.swift使用篇_第2张图片
文件和代码模版都是以 class 作为例子的,实际上 struct 甚至 enum 都可以进行模型绑定的。

1.3. 增删改查

  • 增删查改是数据库最常用的功能,因此 WCDB Swift 对其进行了特别的封装,使其通过一行代码即可完成操作。

1.3.1 插入操作

  • 插入操作有 “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” 函数的原型为:
// 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 会自动开启事务,进行批量化地插入,以获得更新的性能。

1.3.2 删除操作

  • 删除操作只有一个接口,其函数原型为:
func delete(fromTable table: String, // 表名
            where condition: Condition? = nil, // 符合删除的条件
            orderBy orderList: [OrderBy]? = nil, // 排序的方式
            limit: Limit? = nil, // 删除的个数
            offset: Offset? = nil // 从第几个开始删除
) throws

删除接口会删除表内的数据,并通过 conditionorderListlimitoffset 参数来确定需要删除的数据的范围。

这四个组合起来可以理解为:将 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")
  • 这里的 conditionlimitoffset 本质都是遵循 ExpressionConvertible 的对象,可以是数字、字符串、字段或其他更多的组合。同样地,我们会在语言集成查询进一步介绍。
  • 删除接口不会删除表本身,开发者需要调用 drop(table:) 接口删除表。

1.3.3 更新操作

  • 更新操作有 “update with object” 和 “update with row” 两个接口。它们的原型分别
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
  • 其中 propertyConvertibleListconditionlimitoffset 都在前文介绍过了,这里不再赘述。 两个接口除了 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)
  • 而 “with row” 接口则是通过 row 来对数据进行更新。row 是遵循 ColumnEncodable 协议的类型的数组。ColumnEncodable 协议会在后续自定义字段映射类型中进一步介绍。这里只需了解,能够进行字段映射的类型基本都遵循 ColumnEncodable 协议。
  • 因此,与 “with object” 对应的示例代码为:
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)

1.3.4 查找操作

  • 查找接口对应的操作有 8 个,分别为
  1. getObjects
  2. getObject
  3. getRows
  4. getRow
  5. getColumn
  6. getDistinctColumn
  7. getValue
  8. getDistinctValue

虽然接口较多,但大部分都是为了简化操作而提供的便捷接口。实现上其实与 update 类似,只有 “object” 和 “row” 两种方式。

1.3.4.1 对象查找操作

  • 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?
  • 其中 propertyConvertibleListconditionlimitoffset 都在前文介绍过了,这里不再赘述。 而 “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 类型,范型无法匹配,无法编译通过。

1.3.4.2 对象部分查询

  • 与 “insert”、“update” 类似,对象查找操作也支持指定字段,例如:
let objects: [Sample] = try database.getObjects(fromTable: "sampleTable", 
                                                on: Sample.Properties.identifier)

  • 这里只获取了 identifier 字段,而没有获取 description 的值。这就可能与 Swift 本身存在冲突。 Swift 规定了对象创建时,必须初始化所有成员变量。而进行对象部分查询时,则可能出现某部分变量没有变查询,因此无法初始化的情况。因此,对于可能不被查询的成员变量,应将其类型定义为可选值。 对于 Sample 类中,上述 “getObjects” 接口虽然没有获取 description 的值,但由于 descriptionString? 类型,因此不会出错。 而以下则是会出错的例子:
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)
  • 倘若开发者不确定哪些字段可能会被进行对象部分查询,可以将所有字段都定义为可选。

1.3.4.3 值查询操作

  • 其余的 6 个查询接口都是值查询操作,它们都属于 “getRows” 接口的简化接口。其接口声明如下:
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”
  • 在不考虑 condition、orderList、limit 和 offset 参数的情况下:
  1. “getRows” 接口获取整个矩阵的所有内容,即返回值为二维数组。
  2. “getRow” 接口获取某一横行的数据,即返回值为一维数组。
  3. “getColumn” 接口获取某一纵列的数据,即返回值为一维数组。
  4. “getDistinctColumn” 与 “getColumn” 类似,但它会过滤掉重复的值。
  5. “getValue” 接口获取矩阵中某一个格的内容。
  6. “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"

1.4. 数据库,表,事务

  • WCDB Swift 的三个基础类:数据库 - Database、表 - Table 和 事务 - Transaction。它们同时拥有以下特性:
  1. 支持增删查改的便捷接口
  2. 支持链式接口
  3. 数据和状态共享
  4. 线程安全
  • 数据和状态共享,意味着对于 同一个路径的数据库 的 不同基础类,它们的标签、数据库是否打开、是否在进行读写操作等所有状态和数据都始终保持一致。
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 会根据调用情况,并发执行操作,以达到更高的性能。

WCDB Swift 支持 多线程读操作 或 单线程写多线程读 并发执行。

1.4.1 数据库

  • Database 是 WCDB Swift 中最基础的类,几乎所有操作都由该类发起。

1.4.1.1 初始化

  • Database 可以通过文件路径或文件 URL 创建一个数据库。
let filePath = "~/Intermediate/Directories/Will/Be/Created/sample.db"
let databaseWithPath = Database(withPath: filePath)

let fileURL = URL(fileURLWithPath: filePath)
let databaseWithFileURL = Database(withFileURL: fileURL)
  • 对于已经存在的数据库,也可以通过 tag 来获取。
let myTag = 1
databaseWithPath.tag = myTag

// 若该 tag 不存在,则初始化会失败
let databaseWithTag = try Database(withExistingTag: myTag)

1.4.1.2 标签

  • 通过设置标签,可以区分不同的数据库。
let myTag1 = 1
let database1 = Database(withPath: path1)
database1.tag = myTag1

let myTag2 = 2
let database2 = Database(withPath: path2)
database2.tag = myTag2
  • 同时,基于基础类数据共享的机制,对于同一路径的基础类,它们会共享同一个 tag。
print(database1.tag) // 输出 1

let anotherDatabase1 = Database(withPath: path1)
print(anotherDatabase1.tag) // 输出 1

1.4.1.3 打开数据库

  • 延迟初始化是 WCDB Swift 的原则之一,绝大部分数据只会在需要用到时才创建并初始化。数据库的打开就是其中一个例子。
  • 数据库会在第一次进行操作时,自动打开并初始化。开发者不需要手动调用。
let database = Database(withPath: filePath)
print(database.isOpened) // 输出 false
try database.create(table: "sampleTable", of: Sample.self) // 数据库此时会被自动打开
print(database.isOpened) // 输出 true
  • 同时,也可以通过 canOpen 接口测试数据库能否正常打开。
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 同一路径的数据库共享数据和状态等。

1.4.1.4 关闭数据库

  • 与打开数据库相对应,关闭数据库一般情况下也不需要开发者手动调用。当没有指向 Database 所共享的 Core 时,数据库会自动关闭,并回收内存。
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
  • 同时,也可以调用 close 接口,手动关闭数据库。
let database = Database(withPath: filePath)
print(database.canOpen) // 输出 true
print(database.isOpened) // 输出 true
database.close()
print(database.isOpened) // 输出 false

WCDB Swift 也提供了 blockade、unblockade 和 isBlockaded 接口用于分步执行关闭数据库操作,可参考相关接口文档

1.4.1.5 关闭数据库与线程安全

某些情况下,开发者需要确保数据库完全关闭后才能进行操作,如移动文件操作。

数据库是二进制文件,移动文件的过程中若数据发生了变化,则移动后的文件数据可能会不完整、损坏。因此,WCDB Swift 提供了 close: 接口。

try database.close(onClosed: {
    try database.moveFiles(toDirectory: otherDirectory)
})

onClosed 参数内,可确保数据库完全关闭,不会有其他线程的数据访问、操作数据库,因此可以安全地操作文件。

1.4.1.6 内存回收

  • purge 接口用于回收暂不使用的内存。
// 回收 database 数据库中暂不使用的内存
database.purge() 
// 回收所有已创建的数据库中暂不使用的内存
Database.purge() 
  • 在 iOS 平台上,当内存不足、收到系统警告时,WCDB Swift 会自动调用 Database.purge() 接口以减少内存占用。

1.4.1.7 文件操作

// 获取所有与该数据库相关的文件路径
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)
})

1.4.2 表

  • Table 指代数据库中的一个表。可以通过 getTable 接口获取。
let table = try database.getTable(named: "sampleTable", of: Sample.self) //  表不存在时会出错
  • 表相当于指定了表名和模型绑定类的 Database,其实质只是后者的简化版。
// 返回值需指定为 [Sample] 类型以匹配范型
let objectsFromDatabase: [Sample] = try database.getObjects(fromTable: "sampleTable")

// table 已经指定了表名和模型绑定的类,因此可以直接获取
let objectsFromTable = try table.getObjects()

1.4.3 事务

  • 事务一般用于 提升性能 和 保证数据原子性。Database 和 Table 都能直接发起事务,也可以通过 Transaction 更好地控制事务。
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)
})

1.4.3.1 性能

  • 事务提升性能的实质是批量处理。
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")

1.4.3.2 原子性

  • 试考虑以下代码:
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
})

1.4.3.3 执行事务

  • WCDB Swift 提供了三种事务:
  1. 普通事务
  2. 可控事务
  3. 嵌入事务
  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 以回滚事务
})
  1. 可控事务

可控事务在普通事务的基础上,可以通过返回值控制提交或回滚事务

// 普通事务
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")
    })
})
  1. 嵌入事务
  1. 嵌入事务在普通事务的基础上,支持嵌套调用。
  2. 当外层不存在事务时,嵌入事务和普通事务没有区别。
  3. 当外层存在事务时,嵌入事务会跟随外层事务的行为提交或回滚事务。

insert(objects:intoTable:)、insertOrReplace(objects:intoTable:)、create(table:of:) 等 WCDB Swift 自带的接口都使用了嵌入事务
WCDB Swift 也提供了 begin、commit 和 rollback 接口用于分步执行事务,可参考相关接口文档

1.5. 语言集成查询

  • 语言集成查询(WCDB Integrated Language Query,简称 WINQ),是 WCDB 的一项基础特性。它使得开发者能够通过 Swift 的语法特性去完成 SQL 语句。
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 参数,用于数据库查询。

1.5.1 Column

  • Column 代表数据库内的一个字段,如 Insert 语句中的 column-name,指定了需要插入的数据所属的字段。它可通过字段名创建。
let identifierColumn = Column(named: "identifier")
let statementInsert = StatementInsert().insert(intoTable: "sampleTable", 
                                               with: identifierColumn)
                                       .values(1)
print(statementInsert.description) // 输出 "INSERT INTO sampleTable(identifier) VALUES(1)"

1.5.2 ColumnResult

  • 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"

1.5.3 Convertible

  • 细心观察语法中的描述可以发现,许多节点的参数可以不止一种。以刚才提到的 ColumnResult 为例。
    IOS数据存储 之WCDB (二)WCDB.swift使用篇_第3张图片

可以看到,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 接口协议较多,这里不一一赘述。开发者也无需逐一了解,在使用时再查阅接口即可。

1.5.4 手动转换

  • 除了通过接口参数的 Convertible 协议转换,也可以手动调用 asXXX 接口转换。如 ColumnResultConvertible 的协议为:
protocol ColumnResultConvertible {
    func asColumnResult() -> ColumnResult
} 
  • 对于符合 ColumnResultConvertible 接口的类,直接调用即可:
let identifierColumn = Column(named: "identifier")
let identifierColumnResult = identifierColumn.asColumnResult()

1.5.5 CodingKeys 和 Property

  • CodingKeysProperty 严格来说不属于语言集成查询的一部分,它们是语言集成查询和模型绑定结合的产物。

  • 当需要通过对象来操作数据库时,如"getObject“或者”update with object"等,WCDB Swift 不仅需要知道查找数据的哪个字段(即 Column 所完成的事情),还需要知道这个字段对应模型绑定中的哪个变量。

let property = Sample.Properties.identifier.asProperty()
let objects: [Sample] = try database.getObjects(on: property, fromTable: "sampleTable")
  • CodingKeysProperty 就是存储了数据库字段和模型绑定字段的映射关系。不同的是,后者可以分离这个映射,具体用法可参考高级用法一章的查询重定向。

  • 基于模型绑定,开发者可以完全摆脱通过字符串创建 Column,更便捷地操作数据库。

let statementSelect = StatementSelect().select(Sample.Properties.identifier).from("sampleTable")
print(statementSelect.description) // 输出 "SELECT identifier FROM sampleTable"
  • Sample.Properties.identifierSample.CodingKeys.identifier 的写法没有区别。Properties 只是 CodingKeys 的别名 - typealias Properties = CodingKeys。这是为了让阅读代码时语义更加清晰。

1.5.6 Expression

  • Expression 可以算是 SQL 里最复杂的一个了。它在许多语句中都出现,比如查询语句的 where、groupBy、having、limit、offset,更新语句的 value、where、limit、offset等等。同时,它的完整定义也很长,甚至还包含了递归。
  • 从单个 Expression 的语法角度来看,它支持从数字、字符串、二进制、字段创建。而 WCDB Swift 将其扩展为支持所有字段映射类型,包括[内建的类型][Swift-Object-Relational-Mapping-Builtin-Column-Codable-Type]和自定义的类型。
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 类型。

  • 多个 Expression 之间可以通过函数或运算符的方式进行操作,如:
let expression = expressionColumn.between(expressionInt, expressionDouble) 
print(expression.description) // 输出 "identifier BETWEEN 1 AND 2.0"
  • Expression 语法所支持的运算符有十多个,WCDB Swift 基本都支持,但在语义上改为更符合 Swift 的习惯。例如:
  1. || 运算符在 SQL 语法中用于字符串链接,而在 WCDB Swift 中则是用于"或"的逻辑运算。
  2. <> 运算符在 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)))"

1.5.7 ExpressionOperable

  • 显然,每个类都要转换成 Expression 来进行这些操作,虽然也可以,但这就太麻烦了。
let expression = Sample.Properties.identifier.asExpression() > 1
print(expression)
  • 因此,WCDB Swift 定义了 ExpressionOperable 协议。遵循该协议的类都可以与 Expression 的类似,使用函数或运算符进行语言集成查询,包括 Column、Expression、Property 和 CodingKeys 等。也因此,基于模型绑定,开发者可以完全摆脱通过拼装 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))"

1.5.8 Statement

  • Statement 在前文已经接触到不少了,如查询 StatementSelect、插入 StatementInsert 等。它是一个最基本的完整可被执行的 SQL 语句。

  • SQLite 共包含 27 种 Statement,WCDB Swift 基本都支持。根据语法规则创建的 Statement,可以通过基础类的 exec(? 函数直接执行,可以通过 prepare(? 创建 CoreStatement 对象做进一步的操作。我们会在高级接口的核心层接口一节详细介绍。

1.5.9 SQL 到语言集成查询

  • 如前文所说,SQL 的复杂性决定了不可能介绍每一个类型及语句。因此,这里将介绍如何将一个已有的 SQL,转写为语言集成查询。开发者可以以此为例子,触类旁通。
  • 对于已有的 SQL:
  1. 在语法中确定其所属的 Statement。
  2. 对照对应 Statement 的语法,根据关键字对已有的 SQL 进行断句。
  3. 逐个通过语言集成查询的函数调用进行实现。
  • 以如下 SQL 为例:
SELECT identifier.min() FROM sampleTable WHERE (identifier > 0 || identifier / 2 == 0 ) && description NOT NULL ORDER BY identifier ASC LIMIT 1, 100

1.5.9.1 归类

  • 根据 Statement 的列表,显然这个 SQL 属于 StatementSelect。
let statementSelect = StatementSelect()

1.5.9.2 断句

  • 根据 StatementSelect 的语法规则,按关键词(即语法中的大写字母)进行断句:
SELECT identifier.min() 
FROM sampleTable 
WHERE (identifier > 0 || identifier / 2 == 0 ) && description NOT NULL 
ORDER BY identifier ASC 
LIMIT 1, 100

1.5.9.3 调用语言集成查询

  • 根据每个关键词,找到语言集成查询中对应的函数,并完成其参数。
    如 SELECT 关键词对应 StatementSelect 的 select(distinct:_? 函数:
// distinct 参数的默认值为 false,也可以忽略不写
statementSelect.select(distinct: false, Sample.Properties.identifier.min())
statementSelect.from("sampleTable")
  • WHERE 的参数虽然较复杂,但也都能找到对应的函数:
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)

2.WCDB.Swfit 高级功能

2.1 加密与配置

  • 配置主要用控制数据库操作的行为。WCDB Swift 对 SQLite 的数据库做了基本的配置,如 journal_mode、fullfsync 等,以满足 WCDB Swift 的需求。同时,开发者也可以根据自己的需求,自定义配置项。

2.1.1 默认配置

  • WCDB Swift 提供的默认配置为:
  1. PRAGMA locking_mode=“NORMAL”
  2. PRAGMA synchronous=“NORMAL”
  3. PRAGMA journal_mode=“WAL”
  4. PRAGMA fullfsync=1

2.1.2 加密

  • SQLite 支持的配置列表可参考其官方文档。
  • 加密功能可通过 setCipher(key:pageSize:) 接口开启,其函数原型为:
// 该接口等同于配置 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,以获得更好的性能。开发者一般不需要做特别的修改。

  • 对于在 macOS 上通过其他方式创建的加密数据库,在 WCDB Swift 使用时,可手动设置为 1024 以确保解密成功。更多关于 cipher_page_size 的信息,可以参考其官方文档。
  • 开启加密会有部分性能损耗,详情请参考 Benchmark 与性能一章。

值得注意的是,设置密码是一件很慎重的事情。对于已经创建且存在数据的数据库,无论是原本未加密想要改为加密,还是已经加密想要修改密码,都是成本非常高的操作,因此不要轻易使用。更多相关信息可以参考官方文档

2.1.3 自定义配置

  • 除了上述配置,开发者还可以根据自己的需求,通过 setConfig(named:with:orderBy: 接口设置其他配置。详细的配置列表可以参考 SQLite 的官方文档 和 SQLCipher 的官方文档。
  • 设置配置的接口函数原型如下:
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)

2.2 高级接口

  • 本文将介绍 WCDB Swift 的一些高级接口,包括链式调用与联表查询、查询重定向 和 核心层接口。

2.2.1 链式调用

  • 在增删查改一章中,已经介绍了通过 DatabaseTableTransaction 操作数据库的方式。它们是经过封装的便捷接口,其实质都是通过调用链式接口完成的。
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) // 获取该操作删除的行数
  • 链式接口都以 prepareXXX 开始,根据不同的调用返回 InsertUpdateDeleteSelectRowSelectMultiSelect 对象。 这些对象的基本接口都返回其 self,因此可以链式连续调用。 最后调用对应的函数使其操作生效,如 allObjectsexecute(with:) 等。
  • 通过链式接口,可以更灵活的控制数据库操作。

2.2.2 遍历查询

  • WCDB Swift 只会获取并生成遍历到的对象。开发者可以在随时中断查询,以避免浪费性能。
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)
}

2.2.3 联表查询

  • 在链式类中,InsertUpdateDeleteSelectRowSelect 都有其对应的增删查改接口。而 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 是表名到对象的映射,取出后进行类型转换即可。

2.2.4 查询重定向

  • 查询的数据有时候不是字段本身,但仍需将其取出为对象时,可以使用查询重定向。 它可以将查询接口重定向到一个指定字段,从而简化操作。
let object = try database.getObject(on: Sample.Properties.identifier.max().as(Sample.Properties.identifier),
                                    fromTable: "sampleTable")
print(object.identifier)
  • 示例代码查询了 identifier 的最大值,然后重新定向其到 Sample 的 identifier 字段,因此可以直接以对象的形式取出该值。

2.2.5 核心层接口

  • 在之前的所有教程中,WCDB Swift 通过其封装的各种接口,简化了最常用的增删查改操作。但 SQL 的操作千变万化,仍会有部分功能无法通过便捷接口满足。此时则可以使用核心层接口。 核心层接口通过 CoreStatement,其本质 SQLite C 接口部分接口的封装,结合语言集成查询可以实现几乎所有 SQL 能够完成的操作。
  • 以下是通过 CoreStatement 获取数据的示例代码。
// 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()

2.3 监控与错误处理

2.4 自定义字段映射类型

2.5 全文搜索

2.6 损坏,备份,修复

你可能感兴趣的:(IOS数据存储,WCDB.Swift使用篇)