第3章:Room实例设计 — 单实例 vs 多实例方案权衡与决策拓扑
3.1 本章核心矛盾
跨模块数据同步的设计起点,是“数据库实例的共享范围”。选择错误将直接导致数据不一致、编译依赖膨胀或运行时崩溃。核心矛盾在于:
跨模块数据一致性 要求共享数据库实例,以利用 SQLite 的事务隔离与外键约束。
模块编译/运行隔离性 要求各自持有独立实例,避免模块 A 的 schema 变更破坏模块 B。
3.2 方案一:单实例(SharedDatabase)
边界定义:整个 App 进程内只存在一个 RoomDatabase 对象,通过依赖注入(如 Dagger/Hilt)统一提供。
- 模块职责:所有模块的 DAO 操作同一个物理 SQLite 文件,事务对外可见。
- 数据流/控制流:
- 数据流:ModuleA → DAO_A → SharedDatabase → 磁盘 → SharedDatabase → DAO_B → ModuleB
- 控制流:依赖注入框架实例化 SharedDatabase,各模块通过构造函数注入获取 DAO 实例。
- 信任链路:编译时信任 —— 各模块必须在同一 Gradle 模块层级,且共同持有 `@Database` 注解定义;运行时信任 —— 所有 DAO 由同一实例创建,不存在状态分裂。
- 适用场景:模块间需要强一致性(如用户画像+行为上报)、共享外键约束、跨模块事务。
3.3 方案二:多实例(SeparateDatabases)
推荐场景:各模块业务边界清晰,数据无强关联,且需要独立发布、独立演进。
- 边界定义:每个功能模块持有自己的 RoomDatabase 实例,存放在模块私有路径或专有名称下。
- 模块职责:各自管理 schema 迁移、备份、清理,完全自治。
- 数据流/控制流:
- 数据流:ModuleA → DAO_A → DB_A;ModuleB → DAO_B → DB_B。
- 跨模块数据同步依赖上层 Coordinator 或 ContentProvider 桥接(见第5章)。
- 信任链路:运行时协商 —— 每次同步需校验数据格式、版本号,无法利用数据库层的事务隔离。
- 风险点:数据冗余、同步延迟、无外键保护。
3.4 跨模块共享数据库的边界原则
| 维度 | 单实例(SharedDatabase) | 多实例(SeparateDatabases) |
|---|---|---|
| 隔离性与一致性 | 强一致,事务可见 | 最终一致,需额外同步机制 |
| 编译依赖 | 高耦合,所有模块需知完整 schema | 低耦合,模块独立编译 |
| 运行时性能 | 单点写入串行化,大模块可能阻塞 | 并发写入无锁冲突 |
| 数据迁移 | 统一 migration,一人决策 | 各自管理,易出现版本不兼容 |
| 信任边界 | 编译时契约 | 运行时协同(版本、格式校验) |
3.5 实例拓扑决策树
以下决策树概括了从业务需求到实例拓扑的完整分支路径。
3.6 风险点与落地步骤
| 风险点 | 说明 | 缓解措施 |
|---|---|---|
| 数据污染冲突 | 单实例下模块A误写脏数据影响模块B | DAO 层封装校验逻辑、schema reviewer 机制 |
| 耦合度过高 | 单实例导致所有模块必须同步编译 | 引入 API 模块暴露 DAO 接口,隐藏实现 |
| 版本分裂 | 多实例下各模块 schema 版本不一致 | 建立版本清单表、编译时检查 |
| 事务死锁 | 单实例多模块竞争同一写事务 | 使用 @Transaction 隔离、WAL 模式 |
架构决策警示:不要为了“未来模块化”盲目选择多实例;也不要为了“简单”强行共享实例。决策树的根节点必须是业务对“数据一致性”的实时性要求,而非开发者的惯性偏好。
3.7 验收标准
- 编译隔离性:模块 B 在未依赖模块 A 的情况下,可通过自己定义的 DAO 接口完成数据查询。
- 边界清晰:每个模块的
@Database注解文件只包含本模块的 entity 和 DAO,不引入无关类。 - 同步可测:对于多实例场景,可独立编写单测验证跨模块数据同步逻辑(mock 对端 DB 层)。
- 版本独占:任何模块升级 schema 不影响全局构建,除非该模块处于单实例共享路径下。