实体(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 标注单个字段,也可以用多个字段组合主键。
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)的列
但别滥用。每加一个索引,写入和更新都会变慢一点。嗯,这是一笔权衡。
实体定义的核心逻辑
我把实体定义这件事梳理成了一张图,帮助理清各要素之间的关系。
这张图把实体定义的四个维度串了起来:注解声明、主键定义、字段映射、索引优化。缺一个都可能出问题。
实战避坑清单
说了这么多,最后列几条我踩过的坑,你们遇到类似的可以少走弯路。
- 忘记写
@PrimaryKey:编译不通过,检查好几遍才发现。 - 修改了字段名但没更新
@ColumnInfo:Room 默认用变量名,你改了 Kotlin 变量名但忘了改注解,数据库还是老列名——数据就丢了。 - 复合主键字段用了可空类型:Room 不允许。复合主键的每个字段必须非空。
- 给
@Ignore字段也加了索引:索引对不存数据库的字段无意义,还以为自己优化了,其实没作用。 - 实体里嵌套了带主键的
@Embedded:报错说主键冲突。解决方法:要么去掉内嵌类的主键,要么用prefix加前缀。