第3章:数据模型设计与Room实体

3.1 离线优先架构中的原理与角色

在离线优先Android应用中,数据模型是贯穿本地存储、网络同步与UI渲染的核心骨架。Room持久化库在SQLite之上提供了编译时验证的抽象层,强制开发者以结构化方式定义数据模型与操作。

  • Entity(实体):映射数据库表,定义数据字段与约束。
  • DAO(数据访问对象):封装SQL操作,暴露响应式Flow/LiveData。
  • Database(数据库类):纽带,提供版本管理、连接池与迁移入口。
设计原则:离线优先要求数据模型先于UI定义。先画ER图,再写Entity,最后围绕Entity设计DAO接口,能有效避免上层逻辑对数据层的侵入。
Room持久化库核心架构与数据流 AppDatabase (DB版本管理) @Entity User @Entity Post @Dao UserDao @Dao PostDao SQLite Database (物理文件) Migration(1→2) ALTER TABLE ADD COLUMN Database Entity DAO Migration

3.2 定义实体(Entity)

Entity是数据模型的最小单元,一个Entity实例对应数据库中的一行记录。通过注解声明表名、主键、字段约束与索引。

实现步骤:
  1. 使用 @Entity(tableName = "your_table") 标记数据类。
  2. 指定主键 @PrimaryKey(autoGenerate = true)。
  3. 用 @ColumnInfo(name = "column_name") 显式定义字段名。
  4. 对于非持久化字段,使用 @Ignore 注解。

代码示例:User.kt

@Entity(tableName = "users")
data class User(
    @PrimaryKey(autoGenerate = true)
    val id: Int = 0,

    @ColumnInfo(name = "full_name")
    val name: String,

    val email: String,

    @ColumnInfo(defaultValue = "0")
    val age: Int = 0,

    // 不持久化到数据库,仅用于内存计算
    @Ignore
    val tempField: String? = null
)

3.3 定义数据访问对象(DAO)

DAO接口是数据层的操作契约,Room在编译时自动生成实现。推荐所有数据操作以挂起函数(suspend)或Flow形式暴露,天然适配离线优先的异步与观察模式。

代码示例:UserDao.kt

@Dao
interface UserDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertUser(user: User): Long

    @Query("SELECT * FROM users WHERE id = :userId")
    suspend fun getUser(userId: Int): User?

    @Query("SELECT * FROM users ORDER BY name ASC")
    fun getAllUsers(): Flow<List<User>>

    @Query("UPDATE users SET age = age + 1 WHERE id = :userId")
    suspend fun increaseAge(userId: Int): Int

    @Delete
    suspend fun deleteUser(user: User)
}
常见错误:DAO方法必须在后台线程调用(除非返回Flow或LiveData)。Room在编译期会检查SQL语法,强烈建议在编写@Query后立即编译项目捕获错误,避免运行时崩溃。

3.4 定义数据库类

Database类扮演整个持久化层的入口,负责创建数据库、管理连接池、触发迁移以及提供DAO实例。必须遵循单例模式以避免资源泄漏。

代码示例:AppDatabase.kt

@Database(
    entities = [User::class, Post::class],
    version = 2,
    exportSchema = true   // 迁移时建议开启,用于版本回溯
)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
    abstract fun postDao(): PostDao

    companion object {
        const val DATABASE_NAME = "my_app_db"

        @Volatile
        private var INSTANCE: AppDatabase? = null

        fun getInstance(context: Context): AppDatabase {
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    AppDatabase::class.java,
                    DATABASE_NAME
                )
                .addMigrations(MIGRATION_1_2)  // 注册迁移
                .build()
                INSTANCE = instance
                instance
            }
        }
    }
}

3.5 迁移策略

生产环境下的数据库升级必须使用 Migration 类,确保用户数据不丢失。每一个版本演进对应一个 Migration(startVersion, endVersion) 对象。

代码示例:定义迁移

val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(database: SupportSQLiteDatabase) {
        // 执行ALTER TABLE或CREATE TABLE等SQL
        database.execSQL("ALTER TABLE users ADD COLUMN age INTEGER NOT NULL DEFAULT 0")
    }
}

val MIGRATION_2_3 = object : Migration(2, 3) {
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL("CREATE TABLE IF NOT EXISTS `posts` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `user_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `content` TEXT, FOREIGN KEY(`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE)")
    }
}
迁移链与回退:
  • 逐步迁移:调用 .addMigrations(MIGRATION_1_2, MIGRATION_2_3),Room自动按版本链逐一执行。
  • 回退模式:调用 .fallbackToDestructiveMigration() —— 仅用于开发与测试环境,生产环境使用会导致用户数据清空!

3.6 数据关系与类型转换器

Room支持对象间的导航关系,但底层仍基于外键与JOIN查询。正确的设计可减少冗余存储并提升同步效率。

关系类型对比表:

关系类型 描述 Room实现方式
一对一 一个用户对应一个资料 @Embedded 将子Entity直接嵌入父Entity
一对多 一个用户拥有多篇文章 @Relation(parentColumn = "id", entityColumn = "user_id")
多对多 用户与标签、文章与标签 辅助交叉引用表 + @Junction 注解

类型转换器(TypeConverters):当数据库需要存储非基本类型(如Date、List<String>)时,必须定义转换器。

class Converters {
    @TypeConverter
    fun fromTimestamp(value: Long?): Date? {
        return value?.let { Date(it) }
    }

    @TypeConverter
    fun dateToTimestamp(date: Date?): Long? {
        return date?.time
    }
}

// 在AppDatabase上声明
@Database(entities = [...], version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() { ... }

3.7 调试与常见错误

  • 编译时错误(架构处理器):检查 @Query SQL语法、@Entity 字段类型是否正确。Project级build.gradle启用 kapt 或 ksp。
  • 运行时错误(版本不匹配):java.lang.IllegalStateException: Room cannot verify the data integrity. —— 未正确配置 addMigrations 或 fallbackToDestructiveMigration。
  • 空指针与默认值:Kotlin数据类字段建议设置默认值,避免Room从Cursor构造对象时抛异常。
避坑指南:永远不要在UI线程调用返回LiveData/Flow之外的DAO方法。开启Room的日志调试:Room.databaseBuilder(...).setQueryCallback(RoomDatabase.QueryCallback { sql, bindArgs -> Log.d("ROOM_SQL", "SQL: $sql, Args: $bindArgs") })。

3.8 性能与稳定性优化

  • 索引:为频繁出现在 WHERE 或 JOIN 条件的字段添加索引。
    @ColumnInfo(name = "email", index = true) val email: String
  • 外键约束:保证数据引用完整性,级联删除孤儿数据。
    @Entity(foreignKeys = [ForeignKey(entity = User::class, parentColumns = ["id"], childColumns = ["user_id"], onDelete = ForeignKey.CASCADE)])
  • 事务(@Transaction):当DAO方法包含多个查询逻辑(如一对多关系),使用 @Transaction 确保原子性与一致性。
离线优先优化建议:同步表设计时,增加 sync_status 字段(SYNCED/PENDING/SYNCING)和 last_updated 时间戳,使增量同步算法无需全表扫描即可定位变更记录。