架构设计
- 使用
core/database模块集中管理实体与 Dao。 - 结合 Repository 模式提供领域接口。
- 借助 Flow/Channel 实现实时更新。
数据库配置
@Database(
entities = [ArticleEntity::class, AuthorEntity::class],
version = 4,
exportSchema = true
)
abstract class AppDatabase : RoomDatabase() {
abstract fun articleDao(): ArticleDao
}
fun provideDatabase(context: Context): AppDatabase = Room.databaseBuilder(
context,
AppDatabase::class.java,
"app.db"
).fallbackToDestructiveMigrationOnDowngrade()
.addMigrations(MIGRATION_3_4)
.build()
实体与关联
@Entity(tableName = "article")
data class ArticleEntity(
@PrimaryKey val id: Long,
val title: String,
val authorId: Long,
val publishTime: Instant
)
@Entity(tableName = "author")
data class AuthorEntity(
@PrimaryKey val id: Long,
val name: String
)
data class ArticleWithAuthor(
@Embedded val article: ArticleEntity,
@Relation(parentColumn = "authorId", entityColumn = "id")
val author: AuthorEntity
)
Dao
@Dao
interface ArticleDao {
@Transaction
@Query("SELECT * FROM article ORDER BY publishTime DESC")
fun observeArticles(): Flow<List<ArticleWithAuthor>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertArticles(articles: List<ArticleEntity>)
}
迁移与测试
val MIGRATION_3_4 = object : Migration(3, 4) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE article ADD COLUMN isPinned INTEGER NOT NULL DEFAULT 0")
}
}
@RunWith(AndroidJUnit4::class)
class MigrationTest {
@get:Rule
val helper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java.canonicalName,
FrameworkSQLiteOpenHelperFactory()
)
}
多模块协同
core/model定义 Domain Model。- 在 DAO 层返回 Domain Model,或在 Repository 层映射。
- 对 Feature 模块暴露接口而非实体。
interface ArticleRepository {
fun observeArticles(): Flow<List<Article>>
suspend fun sync()
}
class ArticleRepositoryImpl @Inject constructor(
private val dao: ArticleDao,
private val api: ArticleApi
) : ArticleRepository {
override fun observeArticles(): Flow<List<Article>> =
dao.observeArticles().map { list -> list.map { it.toDomain() } }
override suspend fun sync() {
val remote = api.fetchArticles()
dao.insertArticles(remote.map { it.toEntity() })
}
}
性能优化
- 开启
RoomDatabase.Builder.setQueryCallback监控 SQL。 - 使用
EXPLAIN QUERY PLAN分析索引需求。 - 大量插入时使用
@Transaction+insertAll。
@Transaction
suspend fun replaceAll(articles: List<ArticleEntity>) {
deleteAll()
insertArticles(articles)
}
协程与事务
- Room
suspend函数默认在事务中执行。 - 自定义事务:
@Transaction
suspend fun ArticleDao.upsert(article: ArticleEntity) {
val old = getById(article.id)
if (old == null) insert(article) else update(article)
}
常见问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| UI 卡顿 | 在主线程执行数据库操作 | 确保使用 suspend/Flow + Dispatchers.IO |
| 迁移失败 | schema_version 不匹配 | 更新 exportSchema,编写准确迁移脚本 |
| 多模块冲突 | DAO 重复定义 | 在核心模块统一管理 DAO |
实战案例:Room 是”有编译器撑腰的 SQLite”
Dart/Flutter 侧对等的库是 drift(以前叫 moor)——同样是编译期生成代码、同样用 DAO 模式。我在 flutter_brightness 里用 DatabaseService 存光照校准数据,Flutter 侧一般直接 sqflite 裸写 SQL,类型安全全靠自觉。
Room 强的地方恰恰是”不靠自觉”:@Query("SELECT * FROM user WHERE age > :minAge") 写错表名,编译期直接报错;Dart 的 sqflite 要等到运行时才炸。KSP(见本系列 KSP 那篇)让 Room 的注解处理比老 KAPT 快很多,Room 2.6+ 官方默认就是 KSP。
但 Room 也有 Dart 开发者不习惯的地方:
- Migration 必须手写。数据库 version 1→2,加个字段就要写
Migration(1, 2)的 ALTER 语句。fallbackToDestructiveMigration()一时爽,上线就删库——测试环境用可以,生产千万别。 - 主线程查库直接崩。Room 默认禁止主线程 IO(
allowMainThreadQueries()只能调试用),逼你用协程/Flow。Dart 的 sqflite 没这个限制,新人很容易在 UI 线程查大数据然后卡死。
一句话:Room 把”我容易犯的错”都变成了”编译不通过”,这是它比裸 SQL 值钱的地方。
参考:Android 官方文档《Room》;《Migrate Room databases》
总结
- Room 通过 Flow、协程与 Compose 无缝衔接,适合构建实时数据体验。
- 设计清晰的实体、迁移与 Repository,可提升可维护性。
- 对性能、迁移、测试持续投入,使数据库层稳定可靠,为离线与同步能力打下基础。
