原文作者:Florina Muntenescu 原文地址: https://medium.com/androiddevelopers/database-relations-with-room-544ab95e4542 译者:秉心说 译者说:最近在做一款 Rss 阅读器,使用 Room 存储订阅源以及其中的文章,这就是一个典型的 一对多 关系。正好通过此文详细了解
@Relation
注解的使用。
将数据拆分为相关联的表,并以有意义的方式将数据组合在一起 是设计关系型数据库的重要部分。从 Room 2.2 (现已稳定)开始,通过 @Relation
注解,我们支持了表之间所有可能的关系:一对一,一对多,多对多 。
假如我们生活在一个(悲伤的)世界,每个人只能拥有一条狗,并且每条狗也只能有一个主人。这就是一对一关系。为了在关系型数据库中 表示这一关系,我们创建了两张表,Dog
和 Owner
。Dog
表持有 owner id 的引用,Owner
表持有 dog id 的引用。在 Room 中,我们创建这样两个实体类:
@Entity
data class Dog(
@PrimaryKey val dogId: Long,
val dogOwnerId: Long,
val name: String,
val cuteness: Int,
val barkVolume: Int,
val breed: String
)
@Entity
data class Owner(@PrimaryKey val ownerId: Long, val name: String)
我们要在页面上显示所有的狗狗和它们的主人,为此,我们创建了一个数据类 DogAndOwner
:
data class DogAndOwner(
val owner: Owner,
val dog: Dog
)
要通过 Sqlite 完成此次查询,我们需要:
SELECT * FROM Owner
SELECT * FROM Dog
WHERE dogOwnerId IN (ownerId1, ownerId2, …)
通过 Room 来得到 List<DogAndOwner>
,我们不需要自己实现两次查询和对象映射,仅仅通过 @Relation
注解即可。
在上面的例子中,由于 Dog
拥有主人的信息,所有在 dog 变量上添加 @Relation
注解:指定 owner 表中的 ownerId
和 dog 表中的 dogOwnerId
是相对应的。
data class DogAndOwner(
@Embedded val owner: Owner,
@Relation(
parentColumn = "ownerId",
entityColumn = "dogOwnerId"
)
val dog: Dog
)
Dao 可以简化如下:
@Transaction
@Query("SELECT * FROM Owner")
fun getDogsAndOwners(): List<DogAndOwner>
注意:由于 Room 会在后台自动为我们执行这两次查询,所以要添加 @Transaction
注解以保证原子性。
假设一个主人可以拥有多条狗狗 (Yeah !) ,Owner
和 Dog
之间是一对多的关系。之前定义的数据库结构不需要发生任何变化,我们仍然使用之前的表,因为相关联的键已经在表中了。
现在,为了展示主人和他的狗狗们,我们需要创建一个新的数据类:
data class OwnerWithDogs(
val owner: Owner,
val dogs: List<Dog>
)
为了避免两次查询,我们给 List<Dog>
添加 @Relation
注解来定义 Dog
和 Owner
之间的一对多关系。
data class OwnerWithDogs(
@Embedded val owner: Owner,
@Relation(
parentColumn = "ownerId",
entityColumn = "dogOwnerId"
)
val dogs: List<Dog>
)
Dao 是这样的。
@Transaction
@Query("SELECT * FROM Owner")
fun getDogsAndOwners(): List<OwnerWithDogs>
现在假设我们生活在一个完美的世界,每个主人可以拥有多条狗,每条狗也可以有多个主人。要对此关系进行建模,仅仅通过 Dog
表和 Owner
表是不够的。由于一条狗可能有多个主人,所以同一个 dogId
可能需要多条数据,以匹配不同的主人。但是在 Dog
表中,dogId
是主键,我们不能插入多个 id 相同,主人不同的狗狗。为了解决这一问题,我们需要额外创建一个存储 (dogId,ownerId)
的 关联表 (也称为交叉引用表) 。
@Entity(primaryKeys = ["dogId", "ownerId"])
data class DogOwnerCrossRef(
val dogId: Long,
val ownerId: Long
)
假设现在仅仅只通过 Sqlite 来所有的主人和他们的狗:List<OwnerWithDogs>
,我们需要两次查询:获取所有的主人,联表查询 Dog
表和 DogOwnerCrossRef
表。
SELECT * FROM Owner
SELECT
Dog.dogId AS dogId,
Dog.dogOwnerId AS dogOwnerId,
Dog.name AS name,
_junction.ownerId
FROM
DogOwnerCrossRef AS _junction
INNER JOIN Dog ON (_junction.dogId = Dog.dogId)
用 Room 实现的话,我们需要更新 OwnerWithDogs
实例类,告诉 Room 要为了获取对应的所有狗狗,要关联表 DogOwnerCrossRef
。通过 Junction
来引用表。
在 Dao 中,通过查询 Owner
来返回正确的数据类。
@Transactionhttps://youtu.be/_aJsh6P00c0
@Query("SELECT * FROM Owner")
fun getOwnersWithDogs(): List<OwnerWithDogs>
当使用 @Relation
注解时,Room 根据被注解的属性类型来推断使用哪个实体类。例如,到目前为止,我们给 Dog
或 List<Dog>
添加了注解,这就告诉了 Room 要使用哪个类,要查询哪些字段。
如果我们想返回一个其他对象,例如 Pup
,它不是一个实体但是包含了一些字段。我们可以通过 @Relation
注解指定要使用的实体。
data class Pup(
val name: String,
val cuteness: Int = 11
)
data class OwnerWithPups(
@Embedded val owner: Owner,
@Relation(
parentColumn = "ownerId",
entity = Dog::class,
entityColumn = "dogOwnerId"
)
val dogs: List<Pup>
)
如果我们指向返回实体类的指定字段,就需要通过 @Relation
注解的 projection
属性来指定。例如,我们指向获取 OwnerWithDogs
中所有狗狗的名字,因此我们需要返回的是 List<String>
。而 Room 无法推断这些字符串代表的是名字还是品种,所有需要我们通过 projection
指定。
data class OwnerWithDogs(
@Embedded val owner: Owner,
@Relation(
parentColumn = "ownerId",
entity = Dog::class,
entityColumn = "dogOwnerId",
projection = ["name"]
)
val dogNames: List<String>
)
如果你想在 dogOwnerId
和 ownerId
之间定义更加严格的关系,独立于你所创建的任何关系,可以在这些字段之间添加 ForeignKey 约束。请记住,SQLite 外键定义索引,并且可以具有级联触发器来更新或删除表中的条目。因此,请根据是否希望在数据库中使用这种功能来决定是否要使用外键。
无论你需要一对一,一对多,还是多对多的支持,Room 都可以通过 @Relation
注释满足你。
文章首发微信公众号:
秉心说TM
, 专注 Kotlin、 Java 、 Android 原创知识分享,AOSP 源码解析。 更多最新原创文章,扫码关注我吧!