3. Entity定义:表、主键、列注解、忽略字段与默认值
这一节我们聊 Room 的实体定义。说白了,就是告诉 Room 你的数据长什么样,该存到哪张表里,哪些字段要存、哪些跳过,主键怎么设,默认值怎么给。
我刚开始用 Room 时,总觉得这些注解不过是摆设。直到有一次线上用户反馈数据错乱,查了半天发现是主键配错了——一个表里没有唯一标记的房间,Room 自己都不知道该覆盖哪条记录。嗯,从那以后我每次写 Entity 都会先把注解列清楚。
3.1 @Entity —— 告诉 Room "这是我的表"
每个需要持久化的数据类,必须用 @Entity 标记。最简单的写法就是直接加在类上,表名默认是类名(首字母小写)。比如:
@Entity
data class User(
@PrimaryKey val id: Int,
val name: String
)
这个 User 类就会映射到 user 表,两个字段 id 和 name。如果你想自定义表名,用 tableName 参数:
@Entity(tableName = "users")
data class User(...)
几个常用属性:
| 属性 | 说明 | 我在项目中怎么用 |
|---|---|---|
tableName | 自定义表名 | 和数据库命名规范对齐,比如用复数形式 |
indices | 创建索引 | 加速查询,但别加太多,写操作会变慢 |
foreignKeys | 外键约束 | 我一般不用,除非业务强关联;否则容易锁表 |
primaryKeys | 复合主键 | 联合主键时用,后面会讲 |
3.2 @PrimaryKey —— 标记"身份证"
每个 Entity 必须有一个主键。Room 要求至少有一个字段用 @PrimaryKey 标注。最简单的:
@PrimaryKey val id: Int
如果你想要自增主键,加上 autoGenerate = true:
@PrimaryKey(autoGenerate = true) val id: Long = 0
Int 或 Long,且 Kotlin 里要设一个默认值(比如 = 0),否则 Room 不知道新插入时用啥。
复合主键呢? 如果一张表需要两个字段联合唯一,比如“用户ID + 日期”才能确定一条记录,这时候用 primaryKeys 参数:
@Entity(primaryKeys = ["userId", "date"])
data class UserDailyRecord(
val userId: String,
val date: String,
val score: Int
)
注意,复合主键时不能在字段上单独写 @PrimaryKey,只能在 @Entity 的 primaryKeys 里声明。我曾经误写过双重标注,Room 直接编译报错,还好 IDE 提示得及时。
3.3 @ColumnInfo —— 列级别的精细控制
默认情况下,Room 会把属性名直接当作列名。但有时候数据库字段命名规范不同(比如下划线命名,而 Kotlin 用驼峰),或者你想指定列的类型、索引、是否允许为空。这时就用 @ColumnInfo:
@Entity
data class Product(
@PrimaryKey val id: Int,
@ColumnInfo(name = "product_name") val name: String,
@ColumnInfo(name = "price_cents", typeAffinity = ColumnInfo.INTEGER) val price: Int
)
常用参数:
| 参数 | 作用 |
|---|---|
name | 自定义列名,例如 Kotlin 的 createTime 映射到 create_time |
typeAffinity | 指定 SQLite 列类型(TEXT, INTEGER, BLOB 等),一般 Room 自动推断,但某些场合需要手动指定 |
defaultValue | 设置列的默认值(Room 1.4.0+ 支持) |
index | 是否为此列创建单列索引 |
defaultValue 只在建表时生效,如果你用 Room 的 @Insert 插入新行时不传该字段,则会用默认值。我之前有个需求是“新增记录时时间戳自动填入当前时间”,就在 @ColumnInfo(defaultValue = "CURRENT_TIMESTAMP") 上设了,效果完美。但注意 defaultValue 是 SQL 字符串,不是你代码里的常量。
3.4 @Ignore —— 跳过不需要存的字段
你的类里可能有些字段是临时计算用的,不该进数据库。比如购物车类中,一个 itemCount 是运行时动态算的,不想持久化。用 @Ignore 标记即可:
@Entity
data class CartItem(
@PrimaryKey val id: Int,
val productName: String,
@Ignore val tempCount: Int = 0 // 这个不会建列
)
也可以把 @Ignore 加在构造函数上,跳过整个构造方法(不常用)。
@Ignore,Room 会在编译时报错“Cannot find a method to get field XXX”,提示你数据库里没有这个列。那时再加上就行。
我个人习惯把所有非持久化的辅助字段集中放在类的末尾,并且用 @Ignore 显式标注,方便后续维护者一眼看出哪些是要存库的。
3.5 默认值——两种玩法
Room 里给字段设默认值主要有两种方式:
- Kotlin 默认参数:在代码层面给属性一个默认值,比如
val status: Int = 0。但这个默认值只在 新建对象且不传该参数 时生效,不改变数据库层面的列默认值。换句话说,如果你用@Insert插入一个不含该字段的对象,Room 会用对象的默认值写入。 - SQL 默认值:通过
@ColumnInfo(defaultValue = "xxx")告诉 Room 建表时加入DEFAULT xxx。这会影响所有未指定该列的 INSERT 语句(包括直接用 raw sql 插入的)。
举个例子:
@Entity
data class Task(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val title: String,
@ColumnInfo(defaultValue = "0") val completed: Int = 0
)
这里 completed 既有 Kotlin 默认值 0,又有 SQL 默认值 0。如果你用 Room 插入 Task(title = "学习"),Kotlin 默认值会起作用,插入 0;如果你用 @Query("INSERT INTO task(title) VALUES(:title)") 手动插入,SQL 默认值会起作用,也是 0。但如果你故意设置 completed = 1,那两者都不生效,按你实际传的存。
@ColumnInfo(defaultValue) 能自动填充插入时的空值,结果发现它只在 没有提供该列值的 INSERT 中生效。如果插入时显式写了 completed = null,那默认值没用,会存 null。所以建议后端插入时也别传 null,或者用 COALESCE 处理。
3.6 完整示例与结构图
下面是一个稍微完整的 Entity 示例,包含了我们讲的所有元素:
@Entity(
tableName = "notes",
indices = [Index(value = ["created_at"])],
primaryKeys = ["id"]
)
data class Note(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
@ColumnInfo(name = "title_text") val title: String,
@ColumnInfo(name = "content") val content: String?,
@ColumnInfo(name = "created_at", defaultValue = "CURRENT_TIMESTAMP") val createdAt: Long = System.currentTimeMillis(),
@Ignore val tempFormat: String? = null
)
这个 Entity 映射到 notes 表,包含了自增主键、自定义列名、默认时间戳、一个忽略字段,并且对 created_at 建了索引。
3.7 小结
Entity 定义是整个 Room 缓存设计的基础。记住几个关键点:
- 表名:默认类名,用
tableName改; - 主键:必选,可以是单个或复合;
- 列:用
@ColumnInfo微调名字、类型、默认值; - 忽略:临时数据加
@Ignore; - 默认值:Kotlin 默认 + SQL 默认配合使用,别混淆。