14、嵌套对象:@Embedded注解、前缀prefix、嵌套查询、扁平化存储

好,咱们今天聊一个很实在的话题——嵌套对象。

你想想看,在实际开发中,一个数据实体往往不是孤立的。比如一个用户,他可能有地址信息;一篇订单,它可能包含收货人信息。如果每个字段都平铺在同一个表里,那表结构会变得又宽又乱。我个人习惯是把这些逻辑上相关的字段,封装成一个独立的数据类。

但问题来了:Room 是关系型数据库,它只认扁平的列。你传一个嵌套对象进去,它不认识啊。怎么办?

这时候 @Embedded 注解就派上用场了。

14.1 什么是 @Embedded

@Embedded 是 Room 提供的一个注解,用来把一个对象“拆开”,把它的字段平铺到父表中。说白了,就是把嵌套对象的属性,变成当前表的列。

举个例子:

data class Address(
    val street: String,
    val city: String,
    val zipCode: String
)

@Entity
data class User(
    @PrimaryKey val id: Int,
    val name: String,
    @Embedded val address: Address
)

这样 Room 在创建 User 表时,会自动生成四列:idnamestreetcityzipCode。你看,地址对象被“扁平化”了。

核心思想:对象嵌套,表结构扁平。Room 帮你做了拆箱工作。

14.2 前缀 prefix:解决字段名冲突

嗯,这里有个坑。如果两个嵌套对象有同名字段怎么办?比如用户有家庭地址和公司地址,都包含 streetcity

我曾经在项目中遇到过这种情况,直接报错说列名重复。当时我还纳闷,明明是两个不同的对象啊?后来才明白,Room 在扁平化时,字段名是直接拼接的,不会自动加前缀。

解决办法就是 prefix 参数:

data class Address(
    val street: String,
    val city: String
)

@Entity
data class User(
    @PrimaryKey val id: Int,
    val name: String,
    @Embedded val homeAddress: Address,
    @Embedded(prefix = "work_") val workAddress: Address
)

这样生成的列名就是:streetcitywork_streetwork_city。完美避开冲突。

小技巧:前缀建议用下划线结尾,这样生成的列名更自然。比如 prefix = "work_"

14.3 嵌套查询:怎么查嵌套对象?

你可能会问:存进去的时候扁平化了,查出来的时候还能还原成嵌套对象吗?

答案是肯定的。Room 会自动把查询结果映射回嵌套结构。你只需要在 DAO 里写普通的查询语句就行:

@Dao
interface UserDao {
    @Query("SELECT * FROM user WHERE id = :userId")
    suspend fun getUser(userId: Int): User
}

返回的 User 对象里,address 字段会自动填充。Room 会根据列名前缀(如果有)来匹配。

但注意一点:如果你用了 prefix,查询时列名是带前缀的。比如 work_street,Room 会自动把它映射到 workAddress.street。你不需要手动处理。

避坑指南:我曾经遇到一个 bug,嵌套对象里有一个字段是 null,结果整个嵌套对象都变成了 null。后来发现是 Room 的默认行为——如果嵌套对象的所有字段都是 null,Room 会把整个对象置为 null。解决办法是给字段设置默认值,或者在查询时用 COALESCE 处理。

14.4 扁平化存储:为什么这么做?

说白了,扁平化存储是为了性能。关系型数据库对宽表的查询效率,远高于多表关联。你把地址信息直接放在用户表里,一次查询就能拿到所有数据,不需要 JOIN。

但代价是什么?数据冗余。如果多个用户共享同一个地址,那地址信息会被重复存储。所以 @Embedded 适合那种“一对一”且“强依赖”的关系。比如订单和收货地址,地址只属于这个订单,不会复用。

我个人的经验是:

  • 如果嵌套对象和父对象生命周期一致,用 @Embedded
  • 如果嵌套对象可能被多个父对象引用,那就单独建表,用外键关联

14.5 知识体系图

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

@Embedded 核心逻辑 User 实体 id: Int name: String @Embedded address: Address Address 对象 street: String city: String zipCode: String @Embedded 扁平化 user 表(扁平化存储) id | name | street | city | zipCode 1 | 张三 | 长安街 | 北京 | 100000 2 | 李四 | 南京路 | 上海 | 200000 自动拆箱存储 查询自动还原

14.6 实战建议

最后,我总结几条实战中的经验:

  1. 嵌套层级不要太深。我见过有人嵌套三层以上,结果表结构混乱,查询也麻烦。一般两层就够了。
  2. 注意字段默认值。如果嵌套对象里的字段可能为 null,建议给它们设置默认值,避免整个对象变成 null。
  3. 用 prefix 要统一命名规范。团队里最好约定好前缀风格,比如都用下划线结尾,避免混乱。
  4. 不要滥用 @Embedded。它适合“值对象”场景,不适合“实体”场景。如果嵌套对象有自己的生命周期,请单独建表。

个人习惯:我通常把 @Embedded 用于地址、坐标、配置项这类“附属信息”。它们没有独立存在的意义,只是父对象的一部分。这样代码更清晰,表结构也更合理。

好了,关于嵌套对象就聊这么多。记住一句话:对象嵌套是给开发者看的,扁平存储是给数据库用的。Room 的 @Embedded 就是这两者之间的桥梁。

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