3. 设计实体与主键策略:@Entity 注解、主键选择、@PrimaryKey 用法
3.1 @Entity 注解声明数据表
Room 通过 @Entity 将普通 Kotlin/Java 数据类映射为 SQLite 表。每个 @Entity 类对应一张表,字段默认映射为列名(可自定义列名)。注解可配置表名、索引、外键、忽略字段等。
基本声明
@Entity(tableName = "cache_items")
data class CacheItem(
@PrimaryKey val id: Long,
val key: String,
val value: String,
val timestamp: Long
)
tableName 不指定时默认使用类名(首字母小写)。字段命名推荐使用驼峰,Room 默认按原字段名作为列名,也可用 @ColumnInfo(name = "column_name") 单独指定。
3.2 主键选择策略
| 主键类型 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 单一主键 | 每行有天然唯一标识(如 ID、key) | 简单高效,冲突少 | 需业务保证唯一性 |
| 复合主键 | 多字段联合唯一(如 userId + cacheKey) | 无需额外唯一索引 | 查询时必须提供全部主键 |
| 自增主键 | 行无业务含义唯一标识 | 插入自动生成,无冲突 | 难以跨版本匹配 |
单一主键示例
@Entity
data class UserCache(
@PrimaryKey
val userId: String,
val data: String,
val lastUpdate: Long
)
复合主键示例
@Entity(primaryKeys = ["userId", "cacheKey"])
data class MultiKeyCache(
val userId: String,
val cacheKey: String,
val content: String,
val version: Int
)
自增主键(autoGenerate)
@Entity
data class AutoIdCache(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val payload: String,
val createTime: Long
)
注意:autoGenerate = true 只支持 Int / Long 类型主键。自动生成的主键在插入后才能获得,更新时应使用该主键匹配。
3.3 主键选择的增量更新考量
缓存增量更新(insert/replace 或 upsert)依赖主键判断冲突:
- 单一业务主键(如 key 字段)最适合增量替换,Room 的
@Insert(onConflict = OnConflictStrategy.REPLACE)可直接根据主键判断重复。 - 复合主键 能实现更细粒度冲突检测,但需注意建表时主键顺序影响查询性能。
- 自增主键 无法表达业务唯一性,如需增量更新必须额外建立唯一索引。
策略建议: 在缓存增量更新场景下,优先使用业务唯一字段(如请求 URL + 参数哈希值)作为单一主键,配合
REPLACE 策略,既避免自增主键的索引开销,又避免复合主键的查询复杂度。
3.4 主键与数据库版本升级
修改主键(增减复合主键字段、切换自增/非自增)属于 破坏性变更,必须编写迁移脚本:
// 从版本1 到版本2:复合主键增加一个字段
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("CREATE TABLE cache_new (" +
"userId TEXT NOT NULL, " +
"cacheKey TEXT NOT NULL, " +
"region TEXT NOT NULL, " + // 新增字段,也是主键一部分
"content TEXT, " +
"PRIMARY KEY(userId, cacheKey, region))");
database.execSQL("INSERT INTO cache_new (userId, cacheKey, region, content) " +
"SELECT userId, cacheKey, 'default', content FROM cache");
database.execSQL("DROP TABLE cache");
database.execSQL("ALTER TABLE cache_new RENAME TO cache");
}
};
常见错误: 使用
@PrimaryKey(autoGenerate = true) 后,插入数据时给主键赋值 0L 会被 Room 认为是“未设置”,触发自动生成,导致新行。若想手动指定 id,应传入非零值(通常大于0)。
3.5 内联 SVG:实体主键决策流程
3.6 主键选型对比总结
| 主键类型 | 增量更新兼容性 | 迁移难度 | 自增字段影响 |
|---|---|---|---|
| 单一业务主键 | ★★★★★ 直接 upsert | 低 | 需保证唯一 |
| 复合主键 | ★★★★ 联合唯一判断 | 高(需重建表) | 无 |
| 自增主键 | ★★ 需额外字段索引 | 中 | 插入后不可更改 |
实践技巧: 如果你的缓存数据需要按时间或版本进行增量更新,可以在实体中增加
version 或 updatedAt 字段,并建立唯一索引作为逻辑主键,物理主键仍可使用自增 id,这样兼顾了插入效率和业务匹配。
3.7 常见错误与排查
- 💥 复合主键顺序不一致: 写入和查询时主键字段顺序必须严格匹配
primaryKeys数组声明顺序。 - 💥
autoGenerate与非 Int/Long 类型: Room 会在编译时报错,确保只使用Int或Long。 - 💥 主键变更未迁移: 运行时会抛出
IllegalStateException,提示 schema 不匹配,必须编写 Migration 或使用fallbackToDestructiveMigration()(会丢失数据)。 - 💥
REPLACE删除整行: 当主键冲突时,OnConflictStrategy.REPLACE会删除原有行再插入新行,注意自增主键下不会触发冲突,除非另外定义了 UNIQUE 约束。