用途:本条目针对 Room 原始查询(@RawQuery)被分配只读连接导致数据库压缩(VACUUM)失败的问题,给出原因解释与正确放置压缩逻辑的做法。
背景机制:ORM 通过判断语句是否写入来选择连接。注解查询可被生成器解析出 DELETE 等写操作,因而申请写连接;而通过原始查询入口传入的不透明字符串无法被判断,默认按只读处理。在 Room 2.8 中,KSP 将 @RawQuery 方法生成为 performSuspending(__db, true, false),两个布尔参数依次为 isReadOnly、inTransaction,因此取到的是读连接,而读连接以 PRAGMA query_only = 1 打开,在该 pragma 下 VACUUM 会直接失败并报 “attempt to write a readonly database”。
正确位置:压缩应放在数据库类而非 DAO 上,因为 DAO 无法申请写连接。示例中在 RoomDatabase 子类里定义 suspend fun vacuum(),内部调用 useWriterConnection { it.execSQL("VACUUM") }。调用点先 checkpoint 再压缩,并用 runCatching 吞掉失败,仅记录日志。
陷阱:工作正常的原始 checkpoint 不能证明原始语句可写,PRAGMA wal_checkpoint 在只读连接上也能接受,可能多年正常直到第一个真正需要写连接的语句出现;若保留 DAO 上的 checkpoint 辅助方法,应注释说明压缩为何不放在那里。VACUUM 不能在事务内运行,SQLite 会报 “cannot VACUUM from within a transaction”,ORM 辅助方法会隐式开启事务,因此应使用不开启事务的 execSQL。压缩应放在删除操作之后、事务之外。跳过 checkpoint 会让批量删除释放的页留在 write-ahead log 中,文件几乎不缩小。压缩失败不代表操作失败,删除已提交,应记录日志而非抛出错误。压缩会重写整个文件,需要额外空间且大库上耗时,应放在用户预期较长的操作中,而非热路径或启动时。
验证方式:直接阅读生成的 DAO 实现,查看调用点传入的只读标志;通过同一路径执行 SELECT * FROM pragma_query_only 判断是否处于读连接;记录清理前、删除后、压缩后的数据库文件大小;由于失败被有意吞掉,需强制触发一次确认日志实际输出。