跳转到正文
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 后台任务策略