13、关系型数据:@ForeignKey外键、@Relation注解、嵌套关系、一对一、一对多、多对多
说实话,Room 如果只能存单表,那跟 SharedPreferences 也没啥区别了。真正让它强大的,是处理表与表之间的关系。这一章,我们就来啃这块硬骨头。
我记得刚接触 Room 时,最困惑的就是「外键到底要不要加?」、「加了会不会影响性能?」。后来踩过坑才明白——外键是数据库层面的约束,而 @Relation 是代码层面的便利工具。两者各司其职,配合起来才顺手。
- @ForeignKey:在数据库层面建立约束,保证数据完整性
- @Relation:在代码层面简化嵌套对象的查询
- 一对一、一对多、多对多:三种常见关系的建模方式
13.1 为什么需要关系型数据?
你想想看,一个用户有多条订单,一条订单包含多个商品。如果全塞在一张表里,字段会爆炸,数据冗余到怀疑人生。关系型数据库就是用来解决这个问题的——把数据拆成多张表,通过「关系」把它们连起来。
Room 作为 ORM 框架,提供了两种手段来管理关系:
- 数据库层:用 @ForeignKey 声明外键,让 SQLite 帮你检查数据一致性
- 代码层:用 @Relation 注解,把关联查询的结果自动映射成嵌套对象
我个人习惯是:关键业务数据必加外键,非核心数据(比如日志)可以不加,省点性能开销。
13.2 @ForeignKey:数据库层面的约束
先看一个最简单的例子——用户和订单的一对多关系。
@Entity
data class User(
@PrimaryKey val userId: Long,
val name: String
)
@Entity(
foreignKeys = [
ForeignKey(
entity = User::class,
parentColumns = ["userId"],
childColumns = ["userId"],
onDelete = ForeignKey.CASCADE,
onUpdate = ForeignKey.CASCADE
)
]
)
data class Order(
@PrimaryKey val orderId: Long,
val userId: Long, // 外键字段
val amount: Double
)
这里有几个关键点:
- parentColumns:父表的主键(或唯一键)
- childColumns:子表的外键字段
- onDelete / onUpdate:级联操作策略
CASCADE:删除用户时,自动删除他的所有订单。适合强关联场景。SET_NULL:删除用户时,订单的 userId 置为 null。适合弱关联。RESTRICT:如果用户还有订单,禁止删除。适合需要手动清理的场景。NO_ACTION:不自动处理,由开发者自己维护。
我曾经在一个项目中用了 RESTRICT,结果上线后发现用户注销时一直报外键冲突。后来改成 CASCADE 才解决。嗯,这里要注意——级联策略一定要跟业务逻辑匹配,别想当然。
13.3 @Relation:代码层面的便利工具
加了外键之后,怎么查询用户和他的所有订单?传统做法是写两个查询,然后在代码里手动组装。Room 提供了 @Relation 注解,可以自动完成这个组装过程。
首先,定义一个包含关系的 POJO 类:
data class UserWithOrders(
@Embedded val user: User,
@Relation(
parentColumn = "userId",
entityColumn = "userId"
)
val orders: List<Order>
)
然后,在 DAO 中写一个查询:
@Transaction
@Query("SELECT * FROM User")
suspend fun getUsersWithOrders(): List<UserWithOrders>
注意这里加了 @Transaction 注解。为什么?因为 Room 需要执行两条 SQL(先查 User,再查 Order),为了保证数据一致性,必须放在一个事务里。
- @Relation 只能用在 POJO 类中,不能直接用在 Entity 上
- 查询方法必须加 @Transaction,否则会报错
- parentColumn 和 entityColumn 的名称必须跟数据库字段一致
13.4 一对一关系
一对一关系比较少见,但确实存在。比如一个用户对应一个身份证信息。
@Entity
data class IdCard(
@PrimaryKey val idCardId: Long,
val userId: Long, // 外键
val cardNumber: String
)
data class UserWithIdCard(
@Embedded val user: User,
@Relation(
parentColumn = "userId",
entityColumn = "userId"
)
val idCard: IdCard? // 注意这里是单个对象,不是 List
)
查询方式跟一对多几乎一样,只是返回的字段类型从 List 变成了单个对象(或可空对象)。
13.5 一对多关系
这就是我们前面一直在讲的——一个用户有多条订单。这是最常用的关系类型。
我再补充一个实际场景:分类和商品。一个分类下有多个商品,这是典型的「一对多」。
data class CategoryWithProducts(
@Embedded val category: Category,
@Relation(
parentColumn = "categoryId",
entityColumn = "categoryId"
)
val products: List<Product>
)
我个人习惯在查询时加上排序:
@Transaction
@Query("SELECT * FROM Category ORDER BY sortOrder ASC")
suspend fun getCategoriesWithProducts(): List<CategoryWithProducts>
这样拿到的数据直接就能展示,不用再在 UI 层排序。
13.6 多对多关系
多对多关系需要一张中间表。比如「订单和商品」——一个订单包含多个商品,一个商品也出现在多个订单中。
先定义中间表:
@Entity(
primaryKeys = ["orderId", "productId"],
foreignKeys = [
ForeignKey(entity = Order::class, parentColumns = ["orderId"], childColumns = ["orderId"]),
ForeignKey(entity = Product::class, parentColumns = ["productId"], childColumns = ["productId"])
]
)
data class OrderProductCrossRef(
val orderId: Long,
val productId: Long,
val quantity: Int // 中间表可以带额外字段
)
然后定义关系 POJO:
data class OrderWithProducts(
@Embedded val order: Order,
@Relation(
parentColumn = "orderId",
entityColumn = "productId",
associateBy = Junction(
value = OrderProductCrossRef::class,
parentColumn = "orderId",
entityColumn = "productId"
)
)
val products: List<Product>
)
这里的关键是 associateBy 参数,它告诉 Room 通过哪张中间表来关联。
@Transaction
@Query("SELECT * FROM `Order`")
suspend fun getOrdersWithProducts(): List<OrderWithProducts>
13.7 嵌套关系:三层甚至更多
有时候我们需要嵌套多层关系。比如「用户 -> 订单 -> 商品」。Room 支持这种嵌套,但需要逐层定义 POJO。
data class OrderWithProducts(
@Embedded val order: Order,
@Relation(
parentColumn = "orderId",
entityColumn = "orderId",
associateBy = Junction(
value = OrderProductCrossRef::class,
parentColumn = "orderId",
entityColumn = "productId"
)
)
val products: List<Product>
)
data class UserWithOrdersAndProducts(
@Embedded val user: User,
@Relation(
parentColumn = "userId",
entityColumn = "userId"
)
val orders: List<OrderWithProducts> // 嵌套使用
)
查询时只需要查最外层:
@Transaction
@Query("SELECT * FROM User")
suspend fun getUsersWithOrdersAndProducts(): List<UserWithOrdersAndProducts>
Room 会自动递归地组装所有嵌套关系。不过要注意——嵌套层数越多,查询性能越差。我建议最多嵌套三层,超过三层就考虑用其他方式(比如手动查询组装)。
13.8 避坑指南
最后,分享几个我踩过的坑:
- 外键字段必须建立索引:否则关联查询会全表扫描。用
@ColumnInfo(index = true)即可。 - @Relation 不支持懒加载:所有关联数据会一次性查出来。如果数据量大,考虑分页。
- 中间表的主键:多对多的中间表,主键通常是两个外键的组合。用
primaryKeys声明。 - @Transaction 不是可选的:忘了加会直接 crash,别问我怎么知道的。
- @ForeignKey 是数据库层面的约束,保证数据完整性
- @Relation 是代码层面的便利,简化嵌套查询
- 一对一用单个对象,一对多用 List,多对多用中间表 + Junction
- 嵌套关系最多三层,多了性能扛不住
好了,这一章的内容就到这里。关系型数据是 Room 的核心能力,掌握了它,你就能应对绝大多数业务场景。下一章我们会聊聊数据库迁移和版本管理——嗯,那又是一个容易踩坑的地方。