3、Entity实体定义:@Entity注解、@PrimaryKey主键、@ColumnInfo列名、@Ignore忽略字段

好,咱们今天来聊聊 Room 里最基础、也最核心的东西——Entity(实体)。

说白了,Entity 就是一张数据库表的“蓝图”。你定义好一个 Kotlin 数据类,加上几个注解,Room 就能自动帮你把表建好、把数据存进去。我刚开始用 Room 的时候,觉得这玩意儿不就是个 ORM 嘛,后来踩了几个坑才发现,细节还真不少。

核心要点:Entity 是 Room 中数据模型的载体,一个 Entity 类对应一张数据库表,一个实例对应表中的一行记录。

3.1 @Entity 注解:告诉 Room 这是个表

先看一个最简单的例子:

@Entity(tableName = "users")
data class User(
    @PrimaryKey val id: Int,
    val name: String,
    val age: Int
)

看到 @Entity 了吗?这个注解就是告诉 Room:“嘿,这个类我要存到数据库里。” 默认情况下,类名就是表名。但我个人习惯显式指定 tableName,因为有时候类名重构了,表名也跟着变,容易出问题。

这里有几个参数你可能会用到:

参数 说明 示例
tableName 指定表名,默认是类名 @Entity(tableName = "users")
indices 定义索引,提升查询速度 @Entity(indices = [Index("name")])
primaryKeys 复合主键,多个字段联合做主键 @Entity(primaryKeys = ["firstName", "lastName"])
foreignKeys 定义外键约束 @Entity(foreignKeys = [ForeignKey(...)])

小技巧:如果你不需要表名跟类名一致,建议总是显式写 tableName。这样以后改类名时,数据库不会因为表名变化而出错。

3.2 @PrimaryKey 主键:每张表都得有个“身份证”

主键就是每行数据的唯一标识。Room 要求每个 Entity 至少有一个主键。你可以用 @PrimaryKey 标记单个字段,也可以用 @Entity(primaryKeys = [...]) 标记多个字段。

最常见的写法:

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

注意 autoGenerate = true 这个参数。它表示主键由 Room 自动生成,你插入数据时不用传 id。我记得有一次项目里忘了设 autoGenerate,结果每次插入都得手动算 id,那叫一个痛苦。

如果是复合主键,写法是这样的:

@Entity(primaryKeys = ["userId", "bookId"])
data class BorrowRecord(
    val userId: Int,
    val bookId: Int,
    val borrowDate: Long
)

复合主键适合那种“联合唯一”的场景,比如一个用户不能重复借同一本书。嗯,这里要注意:复合主键的字段不能为 null,否则 Room 会报错。

避坑指南:我曾经在复合主键里用了可空类型,结果 Room 编译时直接报错。后来查文档才发现,主键字段必须是非空类型。切记!

3.3 @ColumnInfo 列名:让字段名和列名“各走各的路”

有时候,Kotlin 里的字段名跟数据库里的列名不一样。比如你 Kotlin 里用驼峰命名 userName,但数据库里习惯用下划线 user_name。这时候 @ColumnInfo 就派上用场了。

@Entity
data class Student(
    @PrimaryKey val id: Int,
    @ColumnInfo(name = "student_name") val studentName: String,
    @ColumnInfo(name = "class_id") val classId: Int
)

这样,Kotlin 代码里你写 studentName,数据库里存的却是 student_name。我个人觉得这种分离挺好的——代码保持 Kotlin 风格,数据库保持 SQL 风格,互不干扰。

@ColumnInfo 还有一些其他参数:

  • typeAffinity:指定列的类型,比如 ColumnInfo.TEXTColumnInfo.INTEGER。一般不用设,Room 会自动推断。
  • defaultValue:设置默认值,比如 @ColumnInfo(defaultValue = "0")
  • index:是否为此列创建索引,相当于 @ColumnInfo(index = true)

小技巧:如果你用 @ColumnInfo(name = "...") 改了列名,记得在 DAO 的查询语句里也要用这个列名,而不是字段名。我刚开始就搞混过,查了半天才发现是列名写错了。

3.4 @Ignore 忽略字段:有些东西不该进数据库

不是所有字段都需要存到数据库里。比如你有个计算属性,或者临时缓存的数据,这时候用 @Ignore 告诉 Room:“这个字段别管它。”

@Entity
data class Product(
    @PrimaryKey val id: Int,
    val name: String,
    val price: Double,
    @Ignore val discount: Double = 0.0,  // 临时折扣,不存数据库
    val finalPrice: Double
        get() = price * (1 - discount)  // 计算属性,也不存
)

注意,@Ignore 可以放在字段上,也可以放在构造函数参数上。如果你有多个字段要忽略,还可以用 @Ignore 标记整个构造函数:

@Entity
data class Order(
    @PrimaryKey val id: Int,
    val productName: String,
    val quantity: Int
) {
    @Ignore
    constructor(id: Int, productName: String) : this(id, productName, 0)
}

为什么要忽略?说白了,就是减少数据库的存储开销。你想想看,一个临时计算的值,每次用的时候算一下就行,何必占一个列呢?

避坑指南:我曾经把一个计算属性忘了加 @Ignore,结果 Room 编译时报错说“找不到对应的列”。因为 Room 默认会把所有属性都当成数据库字段。所以,只要不是你想存到数据库里的,统统加上 @Ignore。

3.5 知识体系总览

下面这张图帮你理清 Entity 的核心逻辑:

Room Entity 实体定义核心结构 @Entity 注解 @PrimaryKey 主键 autoGenerate = true 自动生成 @ColumnInfo 列名 name、typeAffinity、defaultValue @Ignore 忽略字段 临时数据、计算属性 复合主键 primaryKeys 多个字段联合唯一标识 关键原则 ① 每个 Entity 必须有主键 ② 非存储字段加 @Ignore ③ 列名与字段名可分离 ④ 复合主键字段不可为空 ⑤ 善用 autoGenerate 避免手动管理 id

3.6 实战中的一些经验

最后,分享几个我在项目中积累的小经验:

  • 主键用 Long 还是 Int? 我个人习惯用 Long,因为数据量大了之后 Int 可能不够用。尤其是做社交类 App,用户量一上来,Int 很容易溢出。
  • 字段命名统一风格:Kotlin 里用驼峰,数据库里用下划线。用 @ColumnInfo 做桥接,两边都舒服。
  • 不要滥用 @Ignore:虽然方便,但如果你发现某个字段经常被忽略,可能说明你的数据模型设计有问题。该拆表就拆表,别硬塞。
  • 记得写默认值:比如 @PrimaryKey(autoGenerate = true) val id: Long = 0,这样插入时不用传 id,Room 会自动处理。

嗯,Entity 的定义就讲到这里。说白了,它就是 Room 数据库的“地基”——地基打好了,后面的 DAO 和数据库操作才能稳。下一节咱们会深入 DAO 的写法,到时候你会看到 Entity 是怎么被用起来的。

总结一句话:Entity 是 Room 的“表定义”,@PrimaryKey 是“身份证”,@ColumnInfo 是“别名”,@Ignore 是“免存牌”。用好这四个注解,你的数据库模型就稳了。


公众号:蓝海资料掘金营,微信deep3321