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:实体主键决策流程

Room 实体主键选择决策流程 业务唯一字段存在? 是 单一主键(业务ID) 否 需要多字段唯一? 是 复合主键 否 自增主键 @PrimaryKey 配合 REPLACE 策略 💡 增量更新推荐 优先使用业务单一主键 + onConflict=REPLACE

3.6 主键选型对比总结

主键类型增量更新兼容性迁移难度自增字段影响
单一业务主键★★★★★ 直接 upsert低需保证唯一
复合主键★★★★ 联合唯一判断高(需重建表)无
自增主键★★ 需额外字段索引中插入后不可更改
实践技巧: 如果你的缓存数据需要按时间或版本进行增量更新,可以在实体中增加 version 或 updatedAt 字段,并建立唯一索引作为逻辑主键,物理主键仍可使用自增 id,这样兼顾了插入效率和业务匹配。

3.7 常见错误与排查

  • 💥 复合主键顺序不一致: 写入和查询时主键字段顺序必须严格匹配 primaryKeys 数组声明顺序。
  • 💥 autoGenerate 与非 Int/Long 类型: Room 会在编译时报错,确保只使用 Int 或 Long。
  • 💥 主键变更未迁移: 运行时会抛出 IllegalStateException,提示 schema 不匹配,必须编写 Migration 或使用 fallbackToDestructiveMigration()(会丢失数据)。
  • 💥 REPLACE 删除整行: 当主键冲突时,OnConflictStrategy.REPLACE 会删除原有行再插入新行,注意自增主键下不会触发冲突,除非另外定义了 UNIQUE 约束。

3.8 小结