Skip to content

Swift 数据库、表、事务

qiuwenchen edited this page Mar 7, 2024 · 11 revisions

本文主要介绍 WCDB Swift 的三个基础类:数据库 - Database、表 - Table 和 事务。

数据库

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

初始化

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

let filePath = "~/Intermediate/Directories/Will/Be/Created/sample.db"
let databaseWithPath = Database(at: filePath)

let fileURL = URL(fileURLWithPath: filePath)
let databaseWithFileURL = Database(at: fileURL)

标签

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

let myTag1 = 1
let database1 = Database(at: path1)
database1.tag = myTag1

let myTag2 = 2
let database2 = Database(at: path2)
database2.tag = myTag2

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

print(database1.tag) // 输出 1

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

打开数据库

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

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

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

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

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

关闭数据库

与打开数据库相对应,关闭数据库一般情况下也不需要开发者手动调用。当同个路径下的Database对象全部释放时,数据库会自动关闭,并回收内存。

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

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

let database = Database(at: filePath)
print(database.canOpen) // 输出 true
print(database.isOpened) // 输出 true
database.close()
print(database.isOpened) // 输出 false

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

关闭数据库与线程安全

某些情况下,开发者需要确保数据库完全关闭后才能进行操作,如将数据库整体zip打包发送。

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

try database.close(onClosed: {
    //执行独占的数据库操作,如zip压缩打包
})

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

内存回收

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

// 回收 database 数据库中暂不使用的内存
database.purge() 
// 回收所有已创建的数据库中暂不使用的内存
Database.purge() 

在 iOS 平台上,当内存不足、收到系统警告时,WCDB Swift 会自动调用 Database.purge() 接口以减少内存占用。

文件操作

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

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

let table = database.getTable(named: "sampleTable", of: Sample.self) 

表相当于指定了表名和模型绑定类的 Database,其实质只是后者的简化版。增删查改中提到的所有接口Table都具备,而且这些接口调用时都不需要再传表名和 ORM 类型,下面是使用示例

// 返回值需指定为 [Sample] 类型以匹配范型
let objectsFromDatabase: [Sample] = try database.getObjects(fromTable: "sampleTable")

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

因为执行数据读写时Table使用起来比Database更加简洁,而且也有利于以表为单位来管理数据读写逻辑,所以我们推荐尽量使用Table来进行数据读写

事务

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

let table = try database.getTable(named: "sampleTable", of: Sample.self)
try database.run(transaction: { _ in 
    try table.insert(objects: object)
  	try table.delete(where: Sampe.Properties.identifier == 1)
})

性能

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

let object = Sample()
object.isAutoIncrement = true
let objects = Array(repeating: object, count: 100000)

// 单独插入,效率很差
for object in objects {
    try table.insert(objects: object)
}

// 事务插入,性能较好
try database.run(transaction: { _ in 
    for object in objects {
        try table.insert(objects: object)
    }
})

// insert(objects:) 接口内置了事务,并对批量数据做了针对性的优化,性能更好
try table.insert(objects: objects)

原子性

试考虑以下代码:

DispatchQueue(label: "other thread").async {
    try table.delete()
}

try table.insert(object: object)
let objects = try table.getObjects()
print(objects.count) // 可能输出 0 或 1

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

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

DispatchQueue(label: "other thread").async {
    try table.delete()
}

try database.run(transaction: { _ in 
    try table.insert(object: object)
		let objects = try table.getObjects()
    print(objects.count) // 输出 1
})

执行事务

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

// 普通事务
try database.run(transaction: { _ in 
    try table.insert(objects: object)
})
// 可控事务
try database.run(controllableTransaction: {
    try table.insert(objects: object)
    return true // 返回 true 以提交事务,返回 false 以回滚事务
})

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

try database.run(transaction: { _ in 
    try database.run(controllableTransaction: { _ in // 普通事务可控事务可以互相嵌套执行
        try table.insert(objects: object)
    })
})

普通事务可控事务可以互相嵌套执行,相当于执行sqlite中的嵌套事务。

insert(objects:intoTable:)insertOrReplace(objects:intoTable:)create(table:of:) 等 WCDB Swift 自带的接口都使用了嵌入事务

WCDB Swift 也提供了 begincommitrollback 接口用于分步执行事务,可参考相关接口文档

关于可中断事务,因为使用较为复杂,后面在高级接口中会有介绍。

Clone this wiki locally