• 数据库
    • 初始化
    • 标签
    • 打开数据库
    • 关闭数据库
      • 关闭数据库与线程安全
    • 内存回收
    • 文件操作
  • 事务
    • 性能
    • 原子性
    • 执行事务

    本文主要介绍 WCDB Swift 的三个基础类:数据库 - Database、表 - Table 和 事务 - Transaction。它们同时拥有以下特性:

    • 支持增删查改的便捷接口
    • 支持链式接口
    • 数据和状态共享
    • 线程安全
      数据和状态共享,意味着对于 同一个路径的数据库 的 不同基础类,它们的标签、数据库是否打开、是否在进行读写操作等所有状态和数据都始终保持一致。
    1. let myTag = 1
    2. let database = Database(withPath: filePath)
    3. database.tag = myTag
    4. print(database.tag) // 输出 1
    5.  
    6. let databaseAlias = Database(withPath: filePath)
    7. print(databaseAlias.tag) // 输出 1
    8.  
    9. let table = try database.getTable(named: "sampleTable", of: Sample.self)
    10. print(table.tag) // 输出 1
    11.  
    12. let transaction = try database.getTransaction()
    13. print(transaction.tag) // 输出 1
    14.  
    15. database.tag = 2
    16. print(database.tag) // 输出 2
    17. print(databaseAlias.tag) // 输出 2
    18. print(table.tag) // 输出 2
    19. print(transaction.tag) // 输出 2
    基础类共享数据和状态的本质是,它们共享同一个 Core,而所有操作都在这个 Core 上发生。

    线程安全,意味着开发者可以在 任意线程任意基础类 调用 任意接口,而不需要考虑数据库本身的线程安全问题。同时,WCDB Swift 会根据调用情况,并发执行操作,以达到更高的性能。

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

    数据库

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

    初始化

    Database 可以通过文件路径或文件 URL 创建一个数据库。

    1. let filePath = "~/Intermediate/Directories/Will/Be/Created/sample.db"
    2. let databaseWithPath = Database(withPath: filePath)
    3.  
    4. let fileURL = URL(fileURLWithPath: filePath)
    5. let databaseWithFileURL = Database(withFileURL: fileURL)

    对于已经存在的数据库,也可以通过 tag 来获取。

    1. let myTag = 1
    2. databaseWithPath.tag = myTag
    3.  
    4. // 若该 tag 不存在,则初始化会失败
    5. let databaseWithTag = try Database(withExistingTag: myTag)

    标签

    通过设置标签,可以区分不同的数据库。

    1. let myTag1 = 1
    2. let database1 = Database(withPath: path1)
    3. database1.tag = myTag1
    4.  
    5. let myTag2 = 2
    6. let database2 = Database(withPath: path2)
    7. database2.tag = myTag2

    同时,基于基础类数据共享的机制,对于同一路径的基础类,它们会共享同一个 tag

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

    打开数据库

    延迟初始化是 WCDB Swift 的原则之一,绝大部分数据只会在需要用到时才创建并初始化。数据库的打开就是其中一个例子。

    数据库会在第一次进行操作时,自动打开并初始化。开发者不需要手动调用

    1. let database = Database(withPath: filePath)
    2. print(database.isOpened) // 输出 false
    3. try database.create(table: "sampleTable", of: Sample.self) // 数据库此时会被自动打开
    4. print(database.isOpened) // 输出 true

    同时,也可以通过 canOpen 接口测试数据库能否正常打开。

    1. let database1 = Database(withPath: filePath)
    2. print(database1.isOpened) // 输出 false
    3. print(database1.canOpen) // 输出 true。仅当数据库无法打开时,如路径无法创建等,该接口会返回 false
    4. print(database1.isOpened) // 输出 true
    5. let database2 = Database(withPath: filePath)
    6. print(database2.isOpened) // 输出 true。WCDB Swift 同一路径的数据库共享数据和状态等。

    关闭数据库

    与打开数据库相对应,关闭数据库一般情况下也不需要开发者手动调用。当没有指向 Database 所共享的 Core 时,数据库会自动关闭,并回收内存。

    1. do {
    2. let database1 = Database(withPath: filePath)
    3. try database1.create(table: "sampleTable", of: Sample.self) // 数据库此时会被自动打开
    4. print(database1.isOpened) // 输出 true
    5. } // 作用域结束,database1 deinit、关闭数据库并回收内存
    6. let database2 = Database(withPath: filePath)
    7. print(database2.isOpened) // 输出 false
    1. let database1 = Database(withPath: filePath)
    2. {
    3. let database2 = Database(withPath: filePath)
    4. try database2.create(table: "sampleTable", of: Sample.self) // 数据库此时会被自动打开
    5. print(database2.isOpened) // 输出 true
    6. } // 作用域结束,database2 deinit,但 database1 仍持有该路径的数据库,因此不会被关闭。
    7. print(database1.isOpened) // 输出 true

    同时,也可以调用 close 接口,手动关闭数据库。

    1. let database = Database(withPath: filePath)
    2. print(database.canOpen) // 输出 true
    3. print(database.isOpened) // 输出 true
    4. database.close()
    5. print(database.isOpened) // 输出 false
    WCDB Swift 也提供了 blockadeunblockadeisBlockaded 接口用于分步执行关闭数据库操作,可参考相关接口文档

    关闭数据库与线程安全

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

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

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

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

    内存回收

    purge 接口用于回收暂不使用的内存。

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

    文件操作

    1. // 获取所有与该数据库相关的文件路径
    2. print(database.paths)
    3. // 获取所有与该数据库相关的文件占用的大小
    4. try database.close(onClosed: {
    5. // 数据库未关闭状态下也可获取文件大小,但不够准确,开发者可自行选择是否关闭
    6. let filesSize = try database.getFilesSize()
    7. print(filesSize)
    8. })
    9. // 删除所有与该数据库相关的文件
    10. try database.close(onClosed: {
    11. try database.removeFiles()
    12. })
    13. // 将所有与该数据库相关的文件移动到另一个目录
    14. try database.close(onClosed: {
    15. try database.moveFiles(toDirectory: otherDirectory)
    16. })

    Table 指代数据库中的一个表。可以通过 getTable 接口获取。

    1. let table = try database.getTable(named: "sampleTable", of: Sample.self) // 表不存在时会出错

    表相当于指定了表名和模型绑定类的 Database,其实质只是后者的简化版。

    1. // 返回值需指定为 [Sample] 类型以匹配范型
    2. let objectsFromDatabase: [Sample] = try database.getObjects(fromTable: "sampleTable")
    3.  
    4. // table 已经指定了表名和模型绑定的类,因此可以直接获取
    5. let objectsFromTable = try table.getObjects()

    事务

    事务一般用于 提升性能保证数据原子性DatabaseTable 都能直接发起事务,也可以通过 Transaction 更好地控制事务。

    1. try database.run(transaction: {
    2. try database.insert(objects: object, intoTable: "sampleTable")
    3. })
    4.  
    5. let table = try database.getTable(named: "sampleTable", of: Sample.self)
    6. table.run(transaction: {
    7. try database.insert(objects: object)
    8. })
    9.  
    10. // 与 Database、Table 类似,开发者可以保存 Transaction 变量
    11. let transaction = try database.getTransaction()
    12. transacton.run(transaction: {
    13. print(transaction.isInTransction) // 输出 true
    14. try transaction.insert(objects: object)
    15. })

    性能

    事务提升性能的实质是批量处理。

    1. let object = Sample()
    2. object.isAutoIncrement = true
    3. let objects = Array(repeating: object, count: 100000)
    4.  
    5. // 单独插入,效率很差
    6. for object in objects {
    7. try database.insert(objects: object, intoTable: "sampleTable")
    8. }
    9.  
    10. // 事务插入,性能较好
    11. try database.run(transaction: {
    12. for object in objects {
    13. try database.insert(objects: object, intoTable: "sampleTable")
    14. }
    15. })
    16.  
    17. // insert(objects:intoTable:) 接口内置了事务,并对批量数据做了针对性的优化,性能更好
    18. try database.insert(objects: objects, intoTable: "sampleTable")

    原子性

    试考虑以下代码:

    1. DispatchQueue(label: "other thread").async {
    2. try database.delete(fromTable: "sampleTable")
    3. }
    4.  
    5. try database.insert(object: object, intoTable: "sampleTable")
    6. let objects = try database.getObjects(fromTable: "sampleTable")
    7. print(objects.count) // 可能输出 0 或 1

    在多线程下,删除操作发生的时机是不确定的。倘若它发生在 插入完成之后取出数据之前 的瞬间,则 getObjects(fromTable:) 无法取出刚才插入的数据,且这种多线程低概率的 bug 是很难查的。

    而事务可以保证一段操作的原子性:

    1. DispatchQueue(label: "other thread").async {
    2. try database.delete(fromTable: "sampleTable")
    3. }
    4.  
    5. try database.run(transaction: {
    6. try database.insert(objects: object, intoTable: "sampleTable")
    7. let objects = try database.getObjects(fromTable: "sampleTable")
    8. print(objects.count) // 输出 1
    9. })

    执行事务

    WCDB Swift 提供了三种事务,普通事务、可控事务和嵌入事务。

    1. // 普通事务
    2. try database.run(transaction: {
    3. try database.insert(objects: object, intoTable: "sampleTable")
    4. })
    5. // 可控事务
    6. try database.run(controllableTransaction: {
    7. try database.insert(objects: object, intoTable: "sampleTable")
    8. return true // 返回 true 以提交事务,返回 false 以回滚事务
    9. })

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

    1. // 普通事务
    2. try database.run(transaction: {
    3. try database.run(transaction: { // 出错,事务不能嵌套
    4. try database.insert(objects: object, intoTable: "sampleTable")
    5. })
    6. })
    7. // 嵌入事务
    8. try database.run(transaction: {
    9. try database.run(embeddedTransaction: { // 嵌入事务可以嵌套
    10. try database.insert(objects: object, intoTable: "sampleTable")
    11. })
    12. })

    嵌入事务在普通事务的基础上,支持嵌套调用。

    当外层不存在事务时,嵌入事务和普通事务没有区别。

    当外层存在事务时,嵌入事务会跟随外层事务的行为提交或回滚事务。

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