第3章:数据模型设计与Room实体
3.1 离线优先架构中的原理与角色
在离线优先Android应用中,数据模型是贯穿本地存储、网络同步与UI渲染的核心骨架。Room持久化库在SQLite之上提供了编译时验证的抽象层,强制开发者以结构化方式定义数据模型与操作。
- Entity(实体):映射数据库表,定义数据字段与约束。
- DAO(数据访问对象):封装SQL操作,暴露响应式Flow/LiveData。
- Database(数据库类):纽带,提供版本管理、连接池与迁移入口。
设计原则:离线优先要求数据模型先于UI定义。先画ER图,再写Entity,最后围绕Entity设计DAO接口,能有效避免上层逻辑对数据层的侵入。
3.2 定义实体(Entity)
Entity是数据模型的最小单元,一个Entity实例对应数据库中的一行记录。通过注解声明表名、主键、字段约束与索引。
实现步骤:
- 使用
@Entity(tableName = "your_table")标记数据类。 - 指定主键
@PrimaryKey(autoGenerate = true)。 - 用
@ColumnInfo(name = "column_name")显式定义字段名。 - 对于非持久化字段,使用
@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 调试与常见错误
- 编译时错误(架构处理器):检查
@QuerySQL语法、@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 时间戳,使增量同步算法无需全表扫描即可定位变更记录。