设计一个关系型数据库很重要的一部分是将数据拆分成具备相干关系的数据表,而后将数据以合乎这种关系的逻辑形式整合到一起。从 Room 2.2 的稳定版开始,咱们可利用一个 @Relation 注解来反对表之间所有可能呈现的关系: 一对一、一对多和多对多。
一对一关系
假如咱们生存在一个每个人只能领有一只狗,且每只狗只能有一个客人的 “悲惨世界” 中,这就是一对一关系。如果要以关系型数据库的形式来反馈它的话,咱们能够创立两张表: Dog 表和 Owner 表,其中 Dog 表通过 owner id 来援用 Owner 表中的数据,或者 Owner 表通过 dog id 来援用 Dog 表中的数据。
@Entitydata class Dog( @PrimaryKey val dogId: Long, val dogOwnerId: Long, val name: String, val cuteness: Int, val barkVolume: Int, val breed: String)@Entitydata class Owner(@PrimaryKey val ownerId: Long, val name: String)
假如咱们想在一个列表中展现所有的狗和它们的客人,咱们须要创立一个 DogAndOwner 类:
data class DogAndOwner( val owner: Owner, val dog: Dog)
为了在 SQLite 中进行查问,咱们须要 1) 运行两个查问: 一个获取所有的客人数据,一个获取所有的狗狗数据,2) 依据 owner id 来进行数据的关系映射。
SELECT * FROM OwnerSELECT * FROM Dog WHERE dogOwnerId IN (ownerId1, ownerId2, …)
要在 Room 中获取一个 List<DogAndOwner>
,咱们不须要本人去实现下面说的查问和映射,只须要应用 @Relation
注解。
在咱们的示例中,因为 Dog
有了 owner 的信息,咱们给 dog 变量减少 @Relation
注解,指定父级 (这里对应 Owner
) 上的 ownerId 列对应 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 注解来确保这个行为是原子性的。
一对多关系
再假如,一个客人能够养多只狗狗,当初下面的关系就变成了一对多关系。咱们之前定义的数据库 schema 并不需要扭转,依然应用同样的表构造,因为在 “多” 这一方的表中曾经有了关联键。
当初,要展现狗和客人的列表,咱们须要创立一个新的类来进行建模:
data class OwnerWithDogs( val owner: Owner, val dogs: List<Dog>)
为了防止运行两个独立的查问,咱们能够在 Dog 和 Owner 中定义一对多的关系,同样,还是在 List<Dog> 前减少 @Relation
注解。
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 表是不够的。因为一只狗狗能够有多个客人,咱们须要在同一个 dog id 上可能匹配多个不同的 owner id。因为 dogId 是 Dog 表的主键,咱们不能间接在 Dog 表中增加同样 id 的多条数据。为了解决这个问题,咱们须要创立一个 associative 表 (也被称为连贯表),这个表来存储 (dogId, ownerId) 的数据对。
@Entity(primaryKeys = ["dogId", "ownerId"])data class DogOwnerCrossRef( val dogId: Long, val ownerId: Long)
如果当初咱们想要获取到所有的狗狗和客人的数据,也就是 List<OwnerWithDogs>
,仅须要编写两个 SQLite 查问,一个获取到所有的客人数据,另一个获取 Dog 和 DogOwnerCrossRef 表的连贯数据。
SELECT * FROM OwnerSELECT Dog.dogId AS dogId, Dog.dogOwnerId AS dogOwnerId, Dog.name AS name, _junction.ownerIdFROM DogOwnerCrossRef AS _junctionINNER JOIN Dog ON (_junction.dogId = Dog.dogId)WHERE _junction.ownerId IN (ownerId1, ownerId2, …)
要通过 Room 来实现这个性能,咱们须要更新 OwnerWithDogs 数据类,并通知 Room 要应用 DogOwnerCrossRef 这个连贯表来获取 Dogs 数据。咱们通过应用 Junction 援用这张表。
data class OwnerWithDogs( @Embedded val owner: Owner, @Relation( parentColumn = "ownerId", entityColumn = "dogId", associateBy = Junction(DogOwnerCrossRef::class) ) val dogs: List<Dog>)
在咱们的 Dao 中,咱们须要从 Owners 中抉择并返回正确的数据类:
@Transaction@Query("SELECT * FROM Owner")fun getOwnersWithDogs(): List<OwnerWithDogs>
更高阶的数据库关系用例
当应用 @Relation 注解时,Room 会默认从所润饰的属性类型推断出要应用的数据库实体。例如,到目前为止咱们用 @Relation 润饰了 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 注解来解决问题。您能够在咱们的 Android Dev Summit ’19 的一个 演讲 中理解无关 Room 2.2 的更多新性能。
点击这里 进一步理解 Room。