跳转到正文
YGJoon 技术博客

Android Kotlin Room 数据库进阶实践

发布于:
·917 字 · 约 2 分钟·作者:
Room 进阶:实体关联设计、Migration 迁移测试、Flow 响应式查询,多模块下数据库的归属划分与事务边界。
Android Kotlin Room 数据库进阶实践文章封面
目录

架构设计

数据库配置

@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()
    )
}

多模块协同

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() })
    }
}

性能优化

@Transaction
suspend fun replaceAll(articles: List<ArticleEntity>) {
    deleteAll()
    insertArticles(articles)
}

协程与事务

@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 开发者不习惯的地方:

  1. Migration 必须手写。数据库 version 1→2,加个字段就要写 Migration(1, 2) 的 ALTER 语句。fallbackToDestructiveMigration() 一时爽,上线就删库——测试环境用可以,生产千万别。
  2. 主线程查库直接崩。Room 默认禁止主线程 IO(allowMainThreadQueries() 只能调试用),逼你用协程/Flow。Dart 的 sqflite 没这个限制,新人很容易在 UI 线程查大数据然后卡死。

一句话:Room 把”我容易犯的错”都变成了”编译不通过”,这是它比裸 SQL 值钱的地方。

参考:Android 官方文档《Room》;《Migrate Room databases》

总结

  1. Room 通过 Flow、协程与 Compose 无缝衔接,适合构建实时数据体验。
  2. 设计清晰的实体、迁移与 Repository,可提升可维护性。
  3. 对性能、迁移、测试持续投入,使数据库层稳定可靠,为离线与同步能力打下基础。

分享这篇文章



相关文章

上一篇
Android Kotlin Jetpack Compose 实战指南
下一篇
Android Kotlin WorkManager 后台任务策略
YGJoon 的头像

YGJoon

记录 Flutter、Web 与原生移动端工程实践。更多背景见关于页,源码与项目见GitHub。