
Now in Android 的 Room 数据层架构解析从:core:database模块看单模块数据库设计的工程实践【免费下载链接】nowinandroidA fully functional Android app built entirely with Kotlin and Jetpack Compose项目地址: https://gitcode.com/GitHub_Trending/no/nowinandroid导读本文以 Now in AndroidNiA开源项目中的:core:database模块为切入点系统讲解该模块在整体分层架构中的位置、Room 数据库的实体/DAO/依赖注入实现、FTS4 全文搜索与多对多关系建模以及基于 AutoMigration 的数据库版本演进方案。读者阅读本文后可以掌握在单模块而非多模块数据库约束下如何用 Room Hilt 组织一套可测试、可迁移、面向 Compose 数据流消费的本地持久层并可直接对照仓库源码验证每一项结论。一、模块定位数据层底座与依赖方向:core:database是 Now in Android 分层架构中负责本地持久化的核心库模块其唯一的上游依赖是:core:model纯 JVM 库存放 UI/数据层共用的不可变模型。这一依赖关系可以用模块依赖图精确表达从图中可以读出两个关键工程决策数据库模块只依赖模型模块core/database/README.md中:core:database -- :core:model表明Entity 与外部模型NewsResource、Topic的互相转换asExternalModel()都以:core:model为契约避免数据库层反向依赖网络层或 UI 层。单模块承载全部持久化NiA 刻意没有把数据库按业务拆成多个 feature 级 DB而是统一收敛在:core:database一个 Android Library 中由上层feature/*与core/data共同消费。这是一种数据访问集中化的取舍——schema 演进集中管理、迁移逻辑单一可控。二、NiaDatabaseRoom 数据库的声明与装配数据库的声明集中在 NiaDatabase.kt它是整个持久层的注册中心Database( entities [ NewsResourceEntity::class, NewsResourceTopicCrossRef::class, NewsResourceFtsEntity::class, TopicEntity::class, TopicFtsEntity::class, RecentSearchQueryEntity::class, ], version 14, autoMigrations [ /* 13 组 AutoMigration见下文第五节 */ ], exportSchema true, ) TypeConverters(InstantConverter::class) internal abstract class NiaDatabase : RoomDatabase() { abstract fun topicDao(): TopicDao abstract fun newsResourceDao(): NewsResourceDao abstract fun topicFtsDao(): TopicFtsDao abstract fun newsResourceFtsDao(): NewsResourceFtsDao abstract fun recentSearchQueryDao(): RecentSearchQueryDao }逐项拆解其工程含义entities 列表6 张表全部在本模块内声明既包含业务表news_resources、topics也包含关联表news_resources_topics与全文索引表FTS4。这保证了 Room 编译期可以校验外键、关系注解与交叉引用的一致性。version 14当前 schema 已演进到第 14 版与 schemas/ 目录下1.json ~ 14.json的导出历史一一对应1 到 9 号各有一次之后依次到 14。exportSchema true开启 schema 导出配合schemas/目录中的 JSON 快照使 Room 能在编译期对比新旧 schema并在 AutoMigration 无法自动推导时报错这是多版本迁移的第一道防线。TypeConverters(InstantConverter::class)kotlinx-datetime的Instant类型无法被 SQLite 直接持久化InstantConverter.kt 负责Instant ↔ Longepoch 毫秒的双向转换被NewsResourceEntity.publishDate、RecentSearchQueryEntity.queriedDate等字段引用。实例化逻辑位于 DatabaseModule.kt通过 Hilt 以单例注入Module InstallIn(SingletonComponent::class) internal object DatabaseModule { Provides Singleton fun providesNiaDatabase(ApplicationContext context: Context): NiaDatabase Room.databaseBuilder(context, NiaDatabase::class.java, nia-database).build() }注意这里没有显式添加.fallbackToDestructiveMigration()这是有意为之NiA 依赖autoMigrations提供无损迁移破坏性回退会静默丢数据与持久化阅读数据 用户订阅主题的定位相悖。数据库文件名为nia-databaseDAO 的提供则由同目录的 DaosModule.kt 完成。三、实体模型与关系建模表结构全景模块内的实体全部位于 model/下面按关系语义分组说明。3.1 业务实体NewsResourceEntity 与 TopicEntityNewsResourceEntity.kt 定义新闻资源表news_resourcesEntity(tableName news_resources) data class NewsResourceEntity( PrimaryKey val id: String, val title: String, val content: String, val url: String, ColumnInfo(name header_image_url) val headerImageUrl: String?, ColumnInfo(name publish_date) val publishDate: Instant, val type: String, )要点主键是字符串 id而非自增 Long与后端数据源返回的 ID 保持一致天然幂等方便Upsert按主键去重。headerImageUrl可空其余字段非空publish_date使用Instant配合InstantConverter落库为整数。文件底部提供了asExternalModel()扩展函数将 Entity 映射为:core:model中的NewsResource此时topics emptyList()真正带主题的完整映射在PopulatedNewsResource中完成。TopicEntity.kt 定义主题表topics其注释明确说明与NewsResourceEntity是多对多关系Entity(tableName topics) data class TopicEntity( PrimaryKey val id: String, val name: String, val shortDescription: String, ColumnInfo(defaultValue ) val longDescription: String, ColumnInfo(defaultValue ) val url: String, ColumnInfo(defaultValue ) val imageUrl: String, )ColumnInfo(defaultValue )的写法值得借鉴对可选文本字段给定 SQL 默认值而非可空类型保证旧行/缺列数据在迁移后依然满足非空约束。3.2 多对多关联NewsResourceTopicCrossRef新闻与主题的多对多关系通过交叉引用表承载见 NewsResourceTopicCrossRef.kt对应表news_resources_topicsnews_resource_id、topic_id双列。该表由PopulatedNewsResource的Relation注解消费data class PopulatedNewsResource( Embedded val entity: NewsResourceEntity, Relation( parentColumn id, entityColumn id, associateBy Junction( value NewsResourceTopicCrossRef::class, parentColumn news_resource_id, entityColumn topic_id, ), ) val topics: ListTopicEntity, )这段 PopulatedNewsResource.kt 声明了 Room 的关系解析规则以news_resources.id为父列经news_resources_topics关联topics.id一次性取出新闻 其全部主题的聚合对象。文件还提供了asExternalModel()Entity → 领域模型topics被真实填充和asFtsEntity()Entity → FTS 索引实体两个映射函数分别服务 UI 数据流与全文搜索。3.3 全文搜索FTS4 实体搜索功能依赖 SQLite FTS4 虚拟表两个实体分别索引新闻与主题NewsResourceFtsEntity.ktEntity(tableName newsResourcesFts) Fts4字段为newsResourceId、title、contentTopicFtsEntity.ktEntity(tableName topicsFts) Fts4字段为topicId、name、shortDescription。FTS4 是内容侧索引必须由应用代码在业务表写入时同步灌入见 PopulatedNewsResource.asFtsEntity() 的转换函数并配合Fts4默认的 tokenizer 做分词匹配。查询入口在对应 DAO 中见第四节。3.4 搜索历史RecentSearchQueryEntityRecentSearchQueryEntity.kt 以query为主键、queriedDate: Instant记录查询时间表名recentSearchQueries用于支撑最近搜索UI按queriedDate倒序取前 N 条。四、DAO 层面向 Flow 的响应式查询与写入策略模块包含 5 个 DAO全部返回kotlinx.coroutines.flow.Flow实时观察写入方法以suspend暴露。以 NewsResourceDao.kt 为最核心代表Transaction Query( value SELECT * FROM news_resources WHERE CASE WHEN :useFilterNewsIds THEN id IN (:filterNewsIds) ELSE 1 END AND CASE WHEN :useFilterTopicIds THEN id IN ( SELECT news_resource_id FROM news_resources_topics WHERE topic_id IN (:filterTopicIds) ) ELSE 1 END ORDER BY publish_date DESC , ) fun getNewsResources( useFilterTopicIds: Boolean false, filterTopicIds: SetString emptySet(), useFilterNewsIds: Boolean false, filterNewsIds: SetString emptySet(), ): FlowListPopulatedNewsResource这段 SQL 的实现技巧非常典型开关参数 CASE WHENuseFilterNewsIds/useFilterTopicIds为false时对应过滤分支恒为真ELSE 1相当于不过滤为true时才启用IN (:ids)子查询。这样单个查询方法即可同时服务全部新闻流按主题过滤按收藏 ID 过滤等多种场景避免 DAO 方法爆炸。子查询关联主题过滤通过news_resources_topics子查询反向匹配新闻 ID。TransactionRelation确保PopulatedNewsResource的多表 join 读取在一个事务内完成返回完整聚合对象。ORDER BY publish_date DESC新闻流按发布时间倒序配合Flow可让 UI 在数据变化时自动重发。同文件的getNewsResourceIds复用同一过滤逻辑但只查id供收藏/去重场景写入侧则体现了混合冲突策略Upsert upsertNewsResources(...)按主键插入或更新幂等同步新闻Insert(onConflict OnConflictStrategy.IGNORE) insertOrIgnoreTopicCrossRefEntities(...)交叉引用表存在即跳过避免重复插入关联行Query DELETE ... WHERE id in (:ids) deleteNewsResources(ids)批量删除如清理已失效内容。其余 DAO 各司其职TopicDao.ktgetTopicEntity(topicId)、getTopicEntities()、按 id 集合查询、insertOrIgnoreTopicsIGNORE 策略、upsertTopics、deleteTopics(ids)并额外提供一次性非 Flow查询getOneOffTopicEntities()供不关心实时性的场景使用。NewsResourceFtsDao.ktinsertAllREPLACE 策略保证索引与源数据一致、SELECT newsResourceId FROM newsResourcesFts WHERE newsResourcesFts MATCH :query全文检索返回匹配 ID 流、getCount()。TopicFtsDao.kt与新闻 FTS DAO 对称topicsFts MATCH :query返回主题 ID 流。RecentSearchQueryDao.ktgetRecentSearchQueryEntities(limit: Int)倒序限量查询、Upsert insertOrReplaceRecentSearchQuery同词覆盖、clearRecentSearchQueries()一键清空。五、Schema 演进AutoMigration 与手写迁移规范数据库从 v1 演进到 v14全部采用AutoMigration这是本模块最具学习价值的工程实践。在 NiaDatabase.kt 中声明了 13 组自动迁移其中三组带有自定义AutoMigrationSpecautoMigrations [ AutoMigration(from 1, to 2), AutoMigration(from 2, to 3, spec DatabaseMigrations.Schema2to3::class), AutoMigration(from 3, to 4), AutoMigration(from 4, to 5), AutoMigration(from 5, to 6), AutoMigration(from 6, to 7), AutoMigration(from 7, to 8), AutoMigration(from 8, to 9), AutoMigration(from 9, to 10), AutoMigration(from 10, to 11, spec DatabaseMigrations.Schema10to11::class), AutoMigration(from 11, to 12, spec DatabaseMigrations.Schema11to12::class), AutoMigration(from 12, to 13), AutoMigration(from 13, to 14), ]DatabaseMigrations.kt 中的类注释给出了命名与使用规范当自动迁移需要额外指令如重命名列、删列、删表时按SchemaXtoY命名、实现AutoMigrationSpec其中X是迁出版本、Y是迁入版本。三组 Spec 分别演示了三类典型操作Spec注解语义Schema2to3RenameColumn(tableName topics, fromColumnName description, toColumnName shortDescription)列重命名topics.description→topics.shortDescriptionSchema10to11DeleteColumn(tableName news_resources, columnName episode_id)DeleteTable.Entries(DeleteTable(episodes_authors), DeleteTable(episodes))删除列 删除两张表移除剧集业务Schema11to12DeleteTable.Entries(DeleteTable(news_resources_authors), DeleteTable(authors))批量删表移除作者业务这里的命名规律值得在自研项目中复制每次 schema 变更都固化为SchemaXtoY类与schemas/目录中的X.json → Y.json快照一一对应让从哪来到哪去、做了什么在代码层自文档化。同时exportSchema true配合 schemas/ 的 JSON 快照保证 Room 编译器能校验每一步迁移的合法性——这是 AutoMigration 能放心大规模使用的基石。六、测试保障DAO 与迁移的自动化验证数据库层并非写完即弃模块内提供了完整的 DAO 测试与迁移验证DatabaseTest.kt验证 Room 数据库构建与迁移链路的正确性是升级 schema 时的回归防线NewsResourceDaoTest.kt覆盖getNewsResources的过滤组合主题过滤、ID 过滤、组合过滤、不过滤以及插入/删除的幂等行为TopicDaoTest.kt验证主题的插入、忽略冲突与批量删除。这些测试位于androidTest需要设备/模拟器上的真实 SQLite与schemas/快照共同构成编译期校验 运行期回归的双保险。七、从源码结构看可复用的设计要点结合整个模块可以提炼出以下可直接迁移到自有项目的模式DAO 查询方法少而全用CASE WHEN :flag THEN ... ELSE 1 END把多个过滤维度收敛到一个查询配合默认参数保持调用端简洁见 NewsResourceDao.kt。Flow 贯穿持久层所有读操作返回Flow写入为suspend让 Compose 层通过collectAsStateWithLifecycle直接订阅数据变更自动刷新 UI。Entity 与领域模型分离Entity 只描述表结构通过asExternalModel()扩展函数映射到:core:model的不可变模型避免 Room 注解污染领域层。FTS 与业务表分离FTS4 虚拟表由asFtsEntity()显式灌入与业务写入解耦搜索失败不影响主数据流。迁移策略集中管理全部 AutoMigration SchemaXtoYSpec 集中在 DatabaseMigrations.ktschema 演进可审计、可测试。需要说明的是以上结论均基于当前仓库源码core/database与 schemas/ 快照推导Room 版本能力、AutoMigration支持范围以项目 gradle/libs.versions.toml 中锁定的版本为准若在你的项目中引入同样的写法请确保 Room 版本不低于 NiA 所依赖的版本并开启exportSchema以享受编译期迁移校验。【免费下载链接】nowinandroidA fully functional Android app built entirely with Kotlin and Jetpack Compose项目地址: https://gitcode.com/GitHub_Trending/no/nowinandroid创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考