定义 Entity 实体类:@Entity、主键、列名、索引

Room 的 Entity 就是你数据库里那张表的“映射”。说白了,你写一个 Kotlin 数据类,加上 @Entity 注解,Room 就知道:“哦,这玩意儿要变成一张表。”

我刚学 Room 那会儿,觉得 Entity 不就是个普通类吗?加个注解就完事了。结果项目跑起来,一堆诡异错误……嗯,后来才明白,注解玩不转,数据库里全是雷。今天咱们就把 @Entity、主键、列名、索引这几个核心概念掰开揉碎。

1. @Entity:声明表的起点

@Entity 标注在类上,告诉 Room 这个类对应一张表。你可以设置 tableName 来指定表名,默认用类名(下划线风格)。

重点:每个 Entity 类都必须有至少一个 @PrimaryKey 字段,否则编译就报错。
@Entity(tableName = "users")
data class User(
    @PrimaryKey val id: Int,
    val name: String?,
    val age: Int
)

这个例子很简单,但有坑。比如 id 我用 Int 类型,如果数据库里自增主键是 Long,插入时就会炸。我曾在某个老项目里吃过这个亏——字段类型一定要和 SQLite 的实际类型匹配,Room 虽然做了隐式转换,但坑还是不少。

2. 主键:@PrimaryKey

主键是每张表的灵魂。Room 里的 @PrimaryKey 可以加在单个字段上,也可以加在多个字段上形成复合主键。

  • 自动生成主键:@PrimaryKey(autoGenerate = true),适合整型字段。
  • 复合主键:在 @Entity 注解中用 primaryKeys 参数指定多个字段名。

我个人习惯:只要业务主键不是自然键(比如用户ID可以传过来),我都用 autoGenerate 的自增主键。省心。

// 单字段自增主键
@PrimaryKey(autoGenerate = true) val id: Long = 0

// 复合主键
@Entity(primaryKeys = ["firstName", "lastName"])
data class NameEntity(
    val firstName: String,
    val lastName: String,
    val age: Int
)
⚠️ 避坑指南:我曾经写过复合主键,但两个字段都允许为空,结果插入时 Room 报错“null values in primary key”。核心主键字段不能为 null,否则 Room 会直接抛异常。

3. 列名:@ColumnInfo

默认情况下,Room 会把 Kotlin 字段名直接当做列名(驼峰转下划线?实际上 Room 并不会自动转换!)。我第一次用 Room 时,字段是 userName,数据库里也存成 userName,后来同事说“应该用下划线命名规范”。我就加了 @ColumnInfo(name = "user_name")。

@ColumnInfo 还可以设置 typeAffinity(列类型亲和性)、defaultValue(默认值)、index(是否创建索引)等参数。不过我个人用得最多的是 name 和 defaultValue。

@Entity(tableName = "users")
data class User(
    @PrimaryKey val id: Int,
    @ColumnInfo(name = "user_name", defaultValue = "unknown") val name: String
)

这里有个小细节:defaultValue 必须是 SQL 合法的默认值表达式,比如 "unknown" 或者 "0",Room 不会帮你加引号。我自己曾写 defaultValue = "unknown" 没加引号,结果建表时 SQL 报错,因为 unknown 被当成了列名……

4. 索引:@Index

索引是提升查询性能的秘密武器,但也会拖慢写入速度。Room 通过 @Entity 的 indices 参数来创建索引。

参数 说明 示例
value 索引包含的列名(数组) ["name", "age"]
unique 是否唯一索引 true
name 自定义索引名(生成的 SQL 会用到) "idx_user_name"
@Entity(
    tableName = "users",
    indices = [
        Index(value = ["name"], name = "idx_user_name"),
        Index(value = ["name", "age"], unique = true)
    ]
)
data class User(
    @PrimaryKey val id: Int,
    @ColumnInfo(name = "user_name") val name: String,
    val age: Int
)

你想想看,如果你的 App 经常按 name 和 age 组合查询,给这两个字段建一个复合索引就很划算。但别滥用——每多一个索引,插入数据就多一次写索引的操作。我曾在线上环境看到一位同事给三个字段各建一个索引,数据量一上来,插入性能直接腰斩。

💡 实战经验:在 Room 中,通过 @ColumnInfo(index = true) 也可以给单个字段快速加索引,但它不能设置唯一约束。如果你想唯一 + 索引,还是用 @Entity(indices) 配合 unique = true。

知识脉络一览

注解/参数 用途 常见坑点
@Entity(tableName) 声明表,自定义表名 表名不用特殊符号,Room 不支持自动转换驼峰
@PrimaryKey 设置主键(单字段/复合) 复合主键字段不可为 null,autoGenerate 需用 Long
@ColumnInfo(name, defaultValue) 自定义列名和默认值 defaultValue 字符串要自己加引号
@Index(unique, name) 创建索引(可唯一) 索引过多影响写入性能
Entity 实体类结构关系图 @Entity tableName, primaryKeys... @PrimaryKey autoGenerate / 复合主键 @ColumnInfo name / defaultValue @Index unique / 复合索引 默认列名 = 字段名 不自动转换驼峰,手动指定 唯一索引 unique = true 防重复数据

一句话总结

写 Entity 时先想清楚:表名、主键策略、列名是否要改、需要哪些索引。这四个点确定好了,你的数据库模型就稳了一半。剩下的另一半,靠的是运行时行为和迁移策略——那将是后面章节的内容。