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 里给字段设默认值主要有两种方式:

  1. Kotlin 默认参数:在代码层面给属性一个默认值,比如 val status: Int = 0。但这个默认值只在 新建对象且不传该参数 时生效,不改变数据库层面的列默认值。换句话说,如果你用 @Insert 插入一个不含该字段的对象,Room 会用对象的默认值写入。
  2. 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 建了索引。

Room Entity 定义核心组成 @Entity (表映射) @Entity 参数配置 tableName indices / foreignKeys @PrimaryKey autoGenerate 复合主键 (primaryKeys) @ColumnInfo name / typeAffinity defaultValue / index @Ignore 跳过不需要持久化的字段 默认值设置 Kotlin 默认值 / SQL defaultValue 综合 Entity 所有元素组合使用 见代码示例 要点总结: • 每个 Entity 必须至少有一个主键(单字段或复合) • 用 @Ignore 标记非持久化字段,避免多余列 • @ColumnInfo(defaultValue) 用于建表时定义 SQL 默认值,不影响 Kotlin 代码默认参数

3.7 小结

Entity 定义是整个 Room 缓存设计的基础。记住几个关键点:

  • 表名:默认类名,用 tableName 改;
  • 主键:必选,可以是单个或复合;
  • 列:用 @ColumnInfo 微调名字、类型、默认值;
  • 忽略:临时数据加 @Ignore;
  • 默认值:Kotlin 默认 + SQL 默认配合使用,别混淆。