实体(Entity)定义:注解、主键、字段、索引

做 Room 的时候,我第一件事就是先把实体定义搞清楚。说白了,实体就是一张数据库表的映射。你写一个 Kotlin 类,加上几个注解,Room 就能帮你生成对应的表结构。这事听起来简单,但坑不少,我一个个说。

注解:告诉 Room 这是个实体

要定义一个实体,你得在类上面加 @Entity 注解。就这一行,Room 就知道:哦,这个类要变成一张表。

@Entity(tableName = "users")
data class User(
    @PrimaryKey val uid: Int,
    @ColumnInfo(name = "first_name") val firstName: String?,
    val lastName: String?
)

看到没?tableName 是可选的。我习惯显式指定表名,不然 Room 会用类名作为默认表名。万一以后类名重构了,表名也跟着变,那就尴尬了。

@ColumnInfo 可以定义字段名、类型、是否可为空等。我这里用 name 把 Kotlin 的驼峰命名转成数据库下划线命名。我个人觉得这种映射比较清晰。

主键:每个实体必须有

Room 要求每个实体至少有一个主键。没有主键?编译就报错。你可以用 @PrimaryKey 标注单个字段,也可以用多个字段组合主键。

经验之谈:尽量用自增整数主键。我曾在一个项目里图省事用了 UUID 字符串做主键,结果查询性能很差。Room 虽然支持,但字符串索引效率低。建议用 autoGenerate = true。

单个主键

@Entity
data class Book(
    @PrimaryKey(autoGenerate = true) val id: Int = 0,
    val title: String,
    val author: String
)

复合主键

@Entity(primaryKeys = ["studentId", "courseId"])
data class Enrollment(
    val studentId: Int,
    val courseId: Int,
    val grade: String
)

复合主键我用的不多。说实话,大多数场景下单独一个自增 ID 就够了。如果你真的需要,记得复合主键的字段都不允许为空。

字段:控制列的行为

字段就是类属性。Room 会把它们自动映射成表列。但有些细节你得知道。

注解用途示例
@ColumnInfo自定义列名、类型、是否索引@ColumnInfo(name = "created_at")
@NonNull标记非空字段@NonNull val email: String
@Ignore忽略该字段,不存入数据库@Ignore val tempData: String?
@Embedded嵌入另一个实体(扁平化)@Embedded val address: Address

我最常用的是 @Ignore。比如在同步策略里,经常需要一个临时标记,用来指示这条记录是否被修改过。但我不想存到数据库里——用 @Ignore 就解决了。

注意:被 @Embedded 的字段不能包含主键。我曾经把一个带主键的地址对象直接嵌入到用户实体里,结果 Room 报错说主键冲突。解决方法是在地址类里去掉主键,或者用 prefix 参数重命名列。

索引:提升查询性能的关键

不加索引的查询,就像在大图书馆里一本一本翻书。Room 支持在 @Entity 注解里声明索引。

语法

@Entity(
    tableName = "tasks",
    indices = [
        Index(value = ["priority", "dueDate"], unique = false),
        Index(value = ["userId"], unique = false)
    ]
)
data class Task(
    @PrimaryKey(autoGenerate = true) val id: Int = 0,
    val userId: Int,
    val priority: Int,
    val dueDate: Long,
    val title: String
)

索引可以单列或多列组合。unique = true 的话,就是唯一索引——能保证数据不重复。

我遇到过一个坑:给字符串列加了索引,但字段内容特别长(比如 JSON 字符串)。结果索引建得很大,插入变慢不少。后来我只对短字符串或者枚举值加索引,长文本就放弃了。

何时加索引?

  • 经常出现在 WHERE 子句中的列
  • 经常用于 JOIN 的列
  • 需要排序(ORDER BY)的列

但别滥用。每加一个索引,写入和更新都会变慢一点。嗯,这是一笔权衡。

实体定义的核心逻辑

我把实体定义这件事梳理成了一张图,帮助理清各要素之间的关系。

实体定义核心逻辑 @Entity 实体类 注解 (@Entity / @ColumnInfo) 主键 @PrimaryKey 字段 (属性 + @Ignore) 索引 @Index autoGenerate / 复合 单列 / 多列 / 唯一 tableName / 嵌套等 @Embedded / @NonNull / @ColumnInfo 要点 实体 = 表结构,每个属性默认映射为列。 主键必备,索引按需加,不要滥用。 注解驱动 -> 自动生成创建表 SQL -> 编译期校验 -> 运行时高效

这张图把实体定义的四个维度串了起来:注解声明、主键定义、字段映射、索引优化。缺一个都可能出问题。

实战避坑清单

说了这么多,最后列几条我踩过的坑,你们遇到类似的可以少走弯路。

  1. 忘记写 @PrimaryKey:编译不通过,检查好几遍才发现。
  2. 修改了字段名但没更新 @ColumnInfo:Room 默认用变量名,你改了 Kotlin 变量名但忘了改注解,数据库还是老列名——数据就丢了。
  3. 复合主键字段用了可空类型:Room 不允许。复合主键的每个字段必须非空。
  4. 给 @Ignore 字段也加了索引:索引对不存数据库的字段无意义,还以为自己优化了,其实没作用。
  5. 实体里嵌套了带主键的 @Embedded:报错说主键冲突。解决方法:要么去掉内嵌类的主键,要么用 prefix 加前缀。