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 表时,会自动生成四列:id、name、street、city、zipCode。你看,地址对象被“扁平化”了。
核心思想:对象嵌套,表结构扁平。Room 帮你做了拆箱工作。
14.2 前缀 prefix:解决字段名冲突
嗯,这里有个坑。如果两个嵌套对象有同名字段怎么办?比如用户有家庭地址和公司地址,都包含 street 和 city。
我曾经在项目中遇到过这种情况,直接报错说列名重复。当时我还纳闷,明明是两个不同的对象啊?后来才明白,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
)
这样生成的列名就是:street、city、work_street、work_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 的核心逻辑:
14.6 实战建议
最后,我总结几条实战中的经验:
- 嵌套层级不要太深。我见过有人嵌套三层以上,结果表结构混乱,查询也麻烦。一般两层就够了。
- 注意字段默认值。如果嵌套对象里的字段可能为 null,建议给它们设置默认值,避免整个对象变成 null。
- 用 prefix 要统一命名规范。团队里最好约定好前缀风格,比如都用下划线结尾,避免混乱。
- 不要滥用 @Embedded。它适合“值对象”场景,不适合“实体”场景。如果嵌套对象有自己的生命周期,请单独建表。
个人习惯:我通常把 @Embedded 用于地址、坐标、配置项这类“附属信息”。它们没有独立存在的意义,只是父对象的一部分。这样代码更清晰,表结构也更合理。
好了,关于嵌套对象就聊这么多。记住一句话:对象嵌套是给开发者看的,扁平存储是给数据库用的。Room 的 @Embedded 就是这两者之间的桥梁。