
GRDB.swift 数据库观察完全指南ValueObservation、DatabaseRegionObservation 与 TransactionObserver 实战解析【免费下载链接】GRDB.swiftA toolkit for SQLite databases, with a focus on application development项目地址: https://gitcode.com/GitHub_Trending/gr/GRDB.swift数据库变化是驱动现代应用界面刷新的核心信号。本指南以 GRDB.swift 官方文档 DatabaseObservation.md 为主线系统讲解 GRDB 面向应用开发的四大观察机制——ValueObservation值观察、DatabaseRegionObservation区域观察、afterNextTransaction事务回调以及底层TransactionObserver事务观察者协议并深入对应源码与测试帮助读者掌握从感知数据库变化到以最小成本刷新 UI的完整实战方案。观察体系总览SQLite 通知能力在 GRDB 中的四种用法GRDB 的观察能力建立在 SQLite 的一项原生特性之上SQLite 会向其宿主应用通知数据库中执行的变化以及事务的提交commit与回滚rollback。GRDB 将这一特性包装为四个由高到低、层层递进的使用层次见 DatabaseObservation.md 的 Overview观察方式观察粒度典型场景ValueObservation数据库值监听玩家列表、统计数字等值的变化拿到新鲜值直接渲染DatabaseRegionObservation数据库区域事务提交后立刻得到通知且保证在其他线程写入之前回调Database/afterNextTransaction(onCommit:onRollback:)单次事务只在某条数据成功落盘后执行一次副作用如启动定位监控TransactionObserver底层事务与逐条变更需要逐行、逐列、甚至在提交前介入的进阶需求其中前三种高层次的 API 全部由第四种——TransactionObserver协议——在底层支撑实现理解底层协议有助于你驾驭所有观察功能。ValueObservation观察数据库值的实时变化ValueObservation是 GRDB 面向应用开发最常用的观察类型它跟踪数据库请求结果的变化并在数据库发生变化时通知新鲜值见 Extension/ValueObservation.md。三步上手保证唯一数据库连接常驻观察期间必须保持一个DatabaseQueue或DatabasePool处于打开状态观察会持有该连接直到停止见ValueObservation.swift源码中start(in:)的实现连接由观察生命周期托底。用闭包定义被观察的值// 跟踪所有玩家 let observation ValueObservation.tracking { db in try Player.fetchAll(db) } // 等价简写 let observation ValueObservation.tracking(Player.fetchAll)被观察的值没有类型限制可以执行多次请求、跨多张表、甚至使用原生 SQL。源码中的ValueObservationTrackingMode枚举见 ValueObservation.swift区分了三种跟踪模式显式常量区域constantRegion、从取回结果推导的常量区域constantRegionRecordedFromSelection、以及每次 fetch 都会变化的非恒定区域nonConstantRegionRecordedFromSelection后者典型如Player.fetchOne(db, id: Int.random(in: 1...1000))。启动观察并处理结果let cancellable observation.start(in: dbQueue) { error in // 处理错误 } onChange: { (players: [Player]) in print(Fresh players, players) }停止观察调用start返回的DatabaseCancellable的cancel()当该对象被释放时观察也会自动停止。cancellable.cancel()消费方式的三个出口ValueObservation可以被转换成AsyncSequence、Combine Publisher 或 RxSwift ObservableRx 需借助社区库 RxGRDBAsync 序列默认在协作线程池上调度源码见values(in:scheduling:bufferingPolicy:)ValueObservation.swiftdo { for try await players in observation.values(in: dbQueue) { print(Fresh players, players) } } catch { // 处理错误 }Combine Publisher默认异步调度在主队列见publisher(in:scheduling:)ValueObservation.swiftlet cancellable observation.publisher(in: dbQueue).sink { completion in // 处理完成 } receiveValue: { (players: [Player]) in print(Fresh players, players) }行为契约必须知道的六条规则ValueObservation的行为遵循以下固定契约先通知初始值观察启动时立即产生一次初始值之后才是后续变化。只通知已提交到磁盘的变化回滚的事务不会产生任何通知。默认每次变更都通知只要被取回值的任意组成部分列、行等被修改就触发可通过指定跟踪区域见下文收紧。默认在主 Actor 上异步通知初始值与后续错误、变化都在主 Actor 上异步投递可通过调度参数改变。允许合并通知短时间内多次连续变更可能被合并为一次通知。可能连续出现相同值可用removeDuplicates()过滤重复。重要ValueObservation并不适合所有场景。如果应用必须处理每一次变化、不允许合并或者需要在数据库文件被进一步修改之前处理变化应改用DatabaseRegionObservation如果需要在提交到磁盘之前介入则应使用TransactionObserver原文出处见 Extension/ValueObservation.md。调度Scheduling控制回调发生在哪条线程默认情况下错误与变化回调都是MainActor隔离的源码中MainActor public func start(...)的默认参数为.mainActor见 ValueObservation.swiftlet cancellable observation.start(in: dbQueue) { error in // 此闭包运行在主 Actor } onChange: { value in // 此闭包运行在主 Actor print(Fresh value, value) }通过向start()传入scheduling参数可定制调度策略.immediate全部值在主 Actor 上通知且第一个值在观察启动时立即同步送达。这对图形应用极其有用——无需等待异步首值、无需实现空态或加载屏即可立即配置视图但代价是首次 fetch 期间 UI 无响应只建议用于非常快的数据库请求let cancellable observation .start(in: dbQueue, scheduling: .immediate) { error in // 主 Actor 上调用 } onChange: { value in // 主 Actor 上调用 print(Fresh value, value) } // - 执行到这里时 Fresh value 已经打印过了.async(onQueue:)在指定的 DispatchQueue 上异步调度。必须提供串行队列——并发队列如DispatchQueue.global(qos: .default)会打乱新鲜值的通知顺序let myQueue: DispatchQueue let cancellable observation .start(in: dbQueue, scheduling: .async(myQueue)) { error in // 在 myQueue 上异步调用 } onChange: { value in // 在 myQueue 上异步调用 print(Fresh value, value) }.task在 Swift 协作线程池上异步调度也是把观察转为 async 序列时的隐式调度器let sharedObservation observation.shared(in: dbQueue, scheduling: .task) do { for try await players in sharedObservation.values() { print(Fresh players, players) } }此外scheduling还影响数据库 fetch 本身的执行线程.immediate下初始 fetch 总在观察启动时同步执行于主 Actor默认.async下初始 fetch 总是异步执行绝不阻塞主线程默认情况下新鲜值在数据库被修改后立即fetch——主线程改库会在主线程触发 fetch。若要保证新鲜值绝不在主线程 fetch需要使用DatabasePool配合tracking(regions:fetch:)或trackingConstantRegion(_:)创建的优化观察使用前务必通读这两个方法的文档否则可能漏掉某些数据库变化。共享观察一份 fetch多点通知共享ValueObservation可以节省数据库资源当一次数据库变化发生时新鲜值只 fetch 一次然后广播给所有客户端见shared(in:scheduling:extent:)// SharedValueObservation[Player] let sharedObservation ValueObservation .tracking { db in try Player.fetchAll(db) } .shared(in: dbQueue)ValueObservation与SharedValueObservation几乎等价但后者没有map这类操作符需要转换时可借助 Combinelet cancellable try sharedObservation .publisher() // 把共享观察转为 Combine Publisher .map { ... } // 使用 Combine 的 map .sink(...)指定跟踪区域观察与取回解耦标准的tracking(_:)是跟踪取回的值而tracking(region:_:fetch:)让你把被观察的区域与被取回的值完全分离。例如只在玩家score列变化时去取回 id1 玩家的整行let observation ValueObservation.tracking( // 定义被跟踪的数据库区域player 表 id1 的 score 列 region: Player.select(\.score).filter(id: 1), // 定义该区域变化后要取回什么id1 的玩家 fetch: { db in try Player.fetchOne(db, id: 1) } )任何遵循DatabaseRegionConvertible的类型——FetchRequest、DatabaseRegion、Table等——都可作为跟踪区域。DatabaseRegion的底层实现见 DatabaseRegion.swift它内部以[CaseInsensitiveIdentifier: TableRegion]描述哪些表 × 哪些列 × 哪些行号且表名不区分大小写nil字典则代表覆盖整个数据库的fullDatabase区域。无法被自动检测的变化notifyChanges(in:)以下场景ValueObservation不会自动 fetch 并通知新鲜值外部数据库连接执行的变化非 GRDB 编译执行的DELETE/INSERT/UPDATE之外的 SQLite 语句造成的变化数据库 schema 变更、sqlite_master等内部系统表的变更WITHOUT ROWID表上的变更。遇到上述情况可在写事务中显式调用Database/notifyChanges(in:)手动通知try dbQueue.write { db in // 通知观察数据库发生了某些变化 try db.notifyChanges(in: .fullDatabase) // 通知观察player 表发生了变化 try db.notifyChanges(in: Player.all()) // 等价写法 try db.notifyChanges(in: Table(player)) }性能要点减少数据库争用ValueObservation的触发依据是可能影响被跟踪值的事务精确地说它跟踪的是DatabaseRegion而非值本身。例如跟踪玩家的最高分时任何触及player.score列的事务无论插入、更新还是删除都会触发观察即便最高分并未改变。这是性能考量的关键认知原文出处见 Extension/ValueObservation.md活跃的观察会占用受限的数据库资源触发时 fetch 新鲜值可能延迟其他组件的读写访问造成数据库争用。GRDB 给出的优化建议是控制观察数量不要对列表中的每个元素单独建观察而应把整个列表放进一个观察里及时停止观察例如UIViewController在viewWillAppear启动观察、viewWillDisappear停止SwiftUI 应用可借助 GRDBQuery 的View.queryObservation(_:)尽量共享观察ValueObservation.start的每次调用都会触发独立的值刷新多个组件关注同一值时用shared(in:scheduling:extent:)用map(_:)做后处理把取原始值 计算拆成tracking加mapmap的执行不阻塞数据库访问也不阻塞主线程// 普通写法fetch 与计算耦合在事务内 let observation ValueObservation.tracking { db - MyValue in let players try Player.fetchAll(db) return computeMyValue(players) } // 优化写法计算移出 fetch let observation ValueObservation .tracking { db try Player.fetchAll(db) } .map { players in computeMyValue(players) }截断型 WAL checkpoint 的副作用在DatabasePool上、且数据库缺失 wal 文件时启动观察即使内容未变化也会在启动时通知两次值——因为无法创建检测启动期间变化所需的 wal 快照。若应用执行截断型 checkpointDatabase/checkpoint(_:on:)或PRAGMA wal_checkpoint可在启动观察前用一个空事务如创建再丢弃临时表重建非空的 wal 文件来规避。DatabaseRegionObservation事务级区域观察当应用需要在处理完每一次影响指定区域的事务后立刻得到通知且保证通知发生在任何其他线程有机会进一步写入之前时应使用DatabaseRegionObservation见 Extension/DatabaseRegionObservation.md。文档特别强调这种提交后、其他人写入前的强保证大多数应用并不真正需要它们更想要的是新鲜值——所以先评估ValueObservation是否满足需求再决定是否使用本 API。用法用一条或多条请求创建观察然后从DatabaseQueue/DatabasePool启动// 跟踪完整的 player 表 let observation DatabaseRegionObservation(tracking: Player.all()) let cancellable try observation.start(in: dbQueue) { error in // 处理错误 } onChange: { (db: Database) in print(Players were changed) }写库即触发通知try dbQueue.write { db in try Player(name: Arthur).insert(db) } // 打印 Players were changed同样可用cancellable.cancel()或释放对象来停止同样可转成 Combine PublisherRx 借助 RxGRDB。DatabaseRegionObservation接受任何DatabaseRegionConvertible类型作为跟踪目标// 观察 player 表的 score 列QueryInterface 写法 let observation DatabaseRegionObservation( tracking: Player.select(\.score)) // 观察 player 表的 score 列原生 SQL 写法 let observation DatabaseRegionObservation( tracking: SQLRequest(SELECT score FROM player)) // 同时观察 player 与 team 两张表 let observation DatabaseRegionObservation( tracking: Table(player), Table(team)) // 观察整个数据库 let observation DatabaseRegionObservation( tracking: .fullDatabase)DatabaseRegionObservation同样存在无法检测的变化清单外部连接、非 GRDB 语句、schema 变更、WITHOUT ROWID表处理方式与ValueObservation一致——在写事务中调用db.notifyChanges(in:)显式通知。相关测试可参考 Tests/GRDBTests/Core/DatabaseRegionObservationTests.swift 与 Combine 版 Tests/GRDBTests/GRDBCombineTests/DatabaseRegionObservationPublisherTests.swift。afterNextTransaction只关心这一次事务是否成功Database/afterNextTransaction(onCommit:onRollback:)用于在下一个或当前事务完成时执行一次回调非常适合把数据库与其他资源文件、系统服务同步。典型场景落盘成功才启动副作用源码文档给出的经典示例见 TransactionObserver.swift是CLLocationManager的监控启动——只有当CLRegion成功写入数据库并提交后才开始监控/// 向数据库插入 region并在成功插入后开始监控。 func startMonitoring(_ db: Database, region: CLRegion) throws { // 确保数据库位于事务savepoint内 try db.inSavepoint { // 把 region 存入数据库 try insert(...) // 当且仅当插入最终被提交时开始监控 db.afterNextTransaction { _ in // locationManager 偏好主队列 DispatchQueue.main.async { locationManager.startMonitoring(for: region) } } return .commit } }如果事务最终回滚显式回滚或因错误回滚onCommit不会被执行从而避免数据库没存上却启动了监控的不一致状态。实现与约束从源码看afterNextTransaction本质上是把一个内部TransactionHandler观察者以.nextTransaction的 extent 注册到连接上见 TransactionObserver.swiftTransactionHandler忽略所有变更事件只在databaseDidCommit/databaseDidRollback中触发回调。使用注意onCommit与onRollback在写入分派队列上串行执行与其他数据库更新互斥前置条件数据库连接不能是只读的源码中GRDBPrecondition(!isReadOnly, ...)会直接触发断言因为只读事务不会被通知给观察者onRollback有默认空实现可按需只提供onCommit。TransactionObserver支撑一切的底层协议TransactionObserver是所有观察功能的底层支撑见 Extension/TransactionObserver.md。它在事务提交到磁盘之前就通知逐条变更插入、更新、删除并在提交、回滚时给出对应回调。注册观察者观察者通过DatabaseWriter/add(transactionObserver:extent:)作用于队列/池或Database/add(transactionObserver:extent:)作用于连接注册let observer MyObserver() dbQueue.add(transactionObserver: observer)默认情况下数据库对观察者持有弱引用观察者不被保留一旦被释放即自动停止接收通知。回调时序提交与回滚的完整生命周期变更通过databaseDidChange(with:)通知包括外键ON DELETE/ON UPDATE与 SQL 触发器引发的间接变更事务完成通过databaseWillCommit()、databaseDidCommit(_:)、databaseDidRollback(_:)通知。被通知的变更在事务提交databaseDidCommit之前并未真正写入磁盘databaseDidRollback则确认其失效try dbQueue.write { db in try db.execute(sql: INSERT ...) // 1. didChange try db.execute(sql: UPDATE ...) // 2. didChange } // 3. willCommit, 4. didCommit try dbQueue.inTransaction { db in try db.execute(sql: INSERT ...) // 1. didChange try db.execute(sql: UPDATE ...) // 2. didChange return .rollback // 3. didRollback } try dbQueue.write { db in try db.execute(sql: INSERT ...) // 1. didChange throw SomeError() } // 2. didRollback显式事务之外的语句也不会漏掉try dbQueue.writeWithoutTransaction { db in try db.execute(sql: INSERT ...) // 1. didChange, 2. willCommit, 3. didCommit try db.execute(sql: UPDATE ...) // 4. didChange, 5. willCommit, 6. didCommit }Savepoint 场景被挂起savepoint 内部的变更只有在其 release 之后才被通知确保通知的事件都是有机会被提交的事件try dbQueue.inTransaction { db in try db.execute(sql: INSERT ...) // 1. didChange try db.execute(sql: SAVEPOINT foo) try db.execute(sql: UPDATE ...) // 延迟 try db.execute(sql: UPDATE ...) // 延迟 try db.execute(sql: RELEASE SAVEPOINT foo) // 2. didChange, 3. didChange try db.execute(sql: SAVEPOINT bar) try db.execute(sql: UPDATE ...) // 不通知 try db.execute(sql: ROLLBACK TO SAVEPOINT bar) try db.execute(sql: RELEASE SAVEPOINT bar) return .commit // 4. willCommit, 5. didCommit }databaseWillCommit抛错会把错误暴露给应用代码do { try dbQueue.inTransaction { db in ... return .commit // 1. willCommit抛错, 2. didRollback } } catch { // 3. 事务观察者抛出的错误 }两条硬性约束所有回调都在写入分派队列中调用与所有数据库更新串行化databaseDidChange与databaseWillCommit回调不得以任何方式访问被观察的写入连接该限制不适用于databaseDidCommit/databaseDidRollback它们可以使用传入的Database参数。事件过滤observes(eventsOfKind:)默认的事件过滤机制是observes(eventsOfKind:)——它在数据库查询执行前只调用一次即可完全禁用变更跟踪是最高效、被推荐的过滤方式// 调用 observes(eventsOfKind:) 一次。 // 之后要么为每一行更新调用 databaseDidChange(with:)要么完全不调用。 try db.execute(sql: UPDATE player SET score score 1)DatabaseEventKind参数能区分插入/删除/更新并告知将要被修改的列。例如只关注player表class PlayerObserver: TransactionObserver { func observes(eventsOfKind eventKind: DatabaseEventKind) - Bool { // 只观察 player 表上的变化 eventKind.tableName player } func databaseDidChange(with event: DatabaseEvent) { // 此方法只会在 player 表变化时被调用 } }即使对所有事件类型都返回false观察者仍然会收到事务通知。该过滤机制也意味着观察者感知不到非 GRDB 编译执行的语句造成的变化——可通过实现databaseEventObservationStrategy解除此限制。观察范围Extent观察者能活多久可通过remove(transactionObserver:)随时显式停止也可用add(transactionObserver:extent:)的 extent 参数指定生命周期三种取值定义见 TransactionObserver.swiftlet observer MyObserver() // 数据库队列或池上 dbQueue.add(transactionObserver: observer) // 默认 extent dbQueue.add(transactionObserver: observer, extent: .observerLifetime) dbQueue.add(transactionObserver: observer, extent: .nextTransaction) dbQueue.add(transactionObserver: observer, extent: .databaseLifetime) // 数据库连接上 dbQueue.inDatabase { db in db.add(transactionObserver: ...) }.observerLifetime默认数据库持有弱引用观察者被释放即自动结束期间通知所有变化与事务.nextTransaction激活到当前或下一个事务完成数据库对其保持强引用直到databaseDidCommit或databaseDidRollback被调用后不再通知.databaseLifetime数据库保留并通知观察者直到连接关闭。此外观察者还可以在事务中途退场调用stopObservingDatabaseChangesUntilNextTransaction()后databaseDidChange在当前事务结束前不再被调用——典型用于只关心事务内是否出现过某类变化class PlayerObserver: TransactionObserver { var playerTableWasModified false func observes(eventsOfKind eventKind: DatabaseEventKind) - Bool { eventKind.tableName player } func databaseDidChange(with event: DatabaseEvent) { playerTableWasModified true // 没必要继续跟踪后续变化 stopObservingDatabaseChangesUntilNextTransaction() } }进阶SQLite Pre-Update Hooks当 SQLite 以SQLITE_ENABLE_PREUPDATE_HOOK选项编译时TransactionObserver会获得一个额外回调databaseWillChange(with:)可在变更发生前观察被修改行的各列初始值/最终值protocol TransactionObserver: AnyObject { #if SQLITE_ENABLE_PREUPDATE_HOOK /// 在数据库变更插入、更新或删除之前附带变更信息 /// 该行各列的初始值 / 最终值进行通知。 /// /// 事件仅在本次方法调用期间有效如需保存请复制event.copy()。 func databaseWillChange(with event: DatabasePreUpdateEvent) #endif }启用该能力的两种途径详见 Extension/TransactionObserver.mdCocoaPods 自定义编译选项在 Podfile 的post_install中为 GRDB.swift target 追加 Swift 与 C 编译定义pod GRDB.swift post_install do |installer| installer.pods_project.targets.select { |target| target.name GRDB.swift }.each do |target| target.build_configurations.each do |config| # 启用额外的 GRDB API config.build_settings[OTHER_SWIFT_FLAGS] $(inherited) -D SQLITE_ENABLE_PREUPDATE_HOOK # 启用额外的 SQLite API config.build_settings[GCC_PREPROCESSOR_DEFINITIONS] $(inherited) GRDB_SQLITE_ENABLE_PREUPDATE_HOOK1 end end end警告务必使用匹配的平台版本在较低版本设备上会触发运行时错误。 注意GCC_PREPROCESSOR_DEFINITIONS中的GRDB_SQLITE_ENABLE_PREUPDATE_HOOK1用于补齐系统sqlite3.h缺失的 C 函数原型一旦 Xcode SDK 自带完整头文件而出现重复定义编译错误只需移除该项。自定义 SQLite 构建参考 CustomSQLiteBuilds.md在编译 SQLite 时激活SQLITE_ENABLE_PREUPDATE_HOOK选项。底层观察者的盲区清单TransactionObserver无法自动通知的变化与事务包括只读事务外部数据库连接执行的变化与事务非 GRDB 编译执行的DELETE/INSERT/UPDATE之外的语句可通过实现databaseEventObservationStrategy解除数据库 schema 变更、sqlite_master等内部系统表变更WITHOUT ROWID表上的变更由ON CONFLICT REPLACE触发的重复行删除此例外未来 SQLite 版本可能改变。与高层 API 相同的补救手段是显式调用Database/notifyChanges(in:)此时databaseDidChange()回调会被触发try dbQueue.write { db in // 通知观察者数据库发生了某些变化 try db.notifyChanges(in: .fullDatabase) // 通知观察者player 表发生了变化 try db.notifyChanges(in: Player.all()) // 等价写法 try db.notifyChanges(in: Table(player)) }若要通知 schema 变化可通知sqlite_master表try dbQueue.write { db in // 通知所有观察者sqlite_master 表变化 try db.notifyChanges(in: Table(sqlite_master)) }选型总结四种观察方式怎么选面对观察需求可按下述路径决策对应 DatabaseObservation.md 的 Topics 分类要新鲜值、要刷新 UI→ValueObservation或共享版SharedValueObservation、异步版AsyncValueObservation配合Database/registerAccess(to:)进行访问注册要在每次影响指定区域的事务提交后、其他写入前被回调且能容忍自己手动取数 →DatabaseRegionObservation只想在某次事务落盘后执行一次副作用→Database/afterNextTransaction(onCommit:onRollback:)需要逐条变更、提交前介入、精细过滤或列级 pre-update 信息→ 直接实现TransactionObserver协议并通过Database/add(transactionObserver:extent:)/DatabaseWriter/add(transactionObserver:extent:)注册用Database/remove(transactionObserver:)/DatabaseWriter/remove(transactionObserver:)注销按需选择Database/TransactionObservationExtent控制生命周期。所有区域相关的类型DatabaseRegion、DatabaseRegionConvertible都可直接用于DatabaseRegionObservation与ValueObservation的区域跟踪。从 ValueObservation.swift 的源码结构可以看到ValueObservation通过可配置的ValueReducer与trackingMode组合出值变化感知 区域跟踪的完整能力而这一切最终都落在TransactionObserver提供的提交/回滚生命周期之上。掌握这四层机制你便拥有了从数据库变化到界面刷新之间的完整、可控的响应链路。【免费下载链接】GRDB.swiftA toolkit for SQLite databases, with a focus on application development项目地址: https://gitcode.com/GitHub_Trending/gr/GRDB.swift创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考