GRDB.swift 数据库框架从5.x升级到6.x的完整迁移指南
前言
GRDB.swift 是一个优秀的 Swift 数据库框架,它提供了简洁的 API 来操作 SQLite 数据库。随着版本 6 的发布,框架引入了一些重要的改进和变化。本文将全面解析从 GRDB 5 升级到 GRDB 6 需要注意的所有关键点,帮助开发者顺利完成迁移。
升级前的准备工作
在开始升级前,建议开发者先完成以下准备工作:
- 确保当前使用的是 GRDB 5 的最新稳定版本
- 修复所有已存在的弃用警告
- 备份项目代码
新版本要求
GRDB 6 对开发环境提出了更高的要求:
- Swift 版本:最低要求 Swift 5.7(之前是 5.3)
- Xcode 版本:需要 Xcode 14.0 或更高版本(之前是 12.0)
- 操作系统版本:
- iOS 11.0+(保持不变)
- macOS 10.13+(之前是 10.10)
- tvOS 11.0+(保持不变)
- watchOS 4.0+(之前是 2.0)
- SQLite 版本:3.19.3+(之前是 3.8.5)
主要关联类型的变化
GRDB 6 利用了 Swift 5.7 引入的主要关联类型特性(SE-0346),这使得请求协议的扩展更加简洁优雅。
请求协议扩展的改进
旧版本写法:
extension DerivableRequest where RowDecoder == Book {
func orderByTitle() -> Self {
order(Column("title").collating(.localizedCaseInsensitiveCompare))
}
}
新版本写法:
extension DerivableRequest<Book> {
func orderByTitle() -> Self {
order(Column("title").collating(.localizedCaseInsensitiveCompare))
}
}
查询接口请求的改进
得益于 SE-0361 改进,QueryInterfaceRequest
的扩展也得到了简化:
extension QueryInterfaceRequest<Player> {
func selectID() -> QueryInterfaceRequest<Int64> {
selectPrimaryKey()
}
}
记录(Record)相关的重要变更
GRDB 6 对记录协议进行了重构,以下是需要注意的关键变化:
1. 可抛出错误的初始化方法
FetchableRecord.init(row:)
现在可以抛出错误:
let player = try Player(row: row) // 必须添加 try 关键字
对于继承 Record
类的子类,需要更新初始化方法:
class Player: Record {
required init(row: Row) throws {
self.id = row["id"]
self.name = row["name"]
try super.init(row: row) // 调用父类方法也需要 try
}
}
2. 可抛出错误的编码方法
EncodableRecord.encode(to:)
现在也可以抛出错误:
class Player: Record {
override func encode(to container: inout PersistenceContainer) throws {
container["id"] = id
container["name"] = name
}
}
受此影响,以下 API 现在也需要处理错误:
let dictionary = try player.databaseDictionary
let changes = try newPlayer.databaseChanges(from: oldPlayer)
3. didInsert
方法签名变更
didInsert
方法的参数从 (with:for:)
变为了 InsertionSuccess
结构体:
struct Player: MutablePersistableRecord {
var id: Int64?
mutating func didInsert(_ inserted: InsertionSuccess) {
id = inserted.rowID
}
}
对于 Record
子类:
class Player: Record {
var id: Int64?
override func didInsert(_ inserted: InsertionSuccess) {
super.didInsert(inserted) // 必须调用 super
id = inserted.rowID
}
}
4. 持久化回调替代重写方法
GRDB 6 引入了"持久化回调"机制来替代直接重写持久化方法的方式。以下方法已被移除:
// 以下方法在 GRDB 6 中已移除
func insert(_ db: Database) throws
func update(_ db: Database, columns: Set<String>) throws
func save(_ db: Database) throws
func delete(_ db: Database) throws -> Bool
替代方案是使用新的回调方法,如 willSave
、willInsert
、didDelete
等。例如,验证逻辑可以这样实现:
struct Link: PersistableRecord {
var url: URL
func willSave(_ db: Database) throws {
if url.host == nil {
throw ValidationError("url must be absolute.")
}
}
}
5. IGNORE 冲突策略的处理
对于使用 IGNORE
冲突策略的情况,GRDB 6 的行为有所变化:
didInsert
现在总是会被调用,即使插入被忽略- 可以使用
insertAndFetch
方法来检测插入是否成功:
if let insertedPlayer = try player.insertAndFetch(db) {
// 插入成功
} else {
// 插入被忽略
}
其他重要变更
-
内存数据库初始化可能抛出错误:
let dbQueue = try DatabaseQueue()
-
selectID()
方法被移除: 可以自行实现:extension QueryInterfaceRequest<Player> { func selectID() -> QueryInterfaceRequest<Int64> { selectPrimaryKey() } }
-
Cursor.isEmpty
变为属性:if try cursor.isEmpty { ... }
-
事务钩子重命名:
db.afterNextTransaction { db in print("Transaction completed") }
-
数据库游标不再暴露 statement 属性:
let sql = cursor.sql // 替代 cursor.statement.sql
迁移建议
- 逐步迁移:不要一次性升级所有代码,可以逐个模块进行迁移
- 利用编译器错误:Xcode 的修复建议可以帮助解决大部分语法变更
- 测试覆盖:确保有充分的测试来验证迁移后的行为
- 查阅文档:遇到问题时参考 GRDB 的官方文档
结语
GRDB 6 带来了许多现代化改进,虽然需要一定的迁移工作,但这些变化使得 API 更加健壮和符合 Swift 的最新特性。通过本文的指导,开发者应该能够顺利完成从 GRDB 5 到 GRDB 6 的升级,并充分利用新版本提供的各种优势。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考