设计一个关系型数据库很重要的一部分是将数据拆分红具备相关关系的数据表,而后将数据以符合这种关系的逻辑方式整合到一块儿。从 Room 2.2 的稳定版开始,咱们可利用一个 @Relation 注解来支持表之间全部可能出现的关系: 一对1、一对多和多对多。html
假设咱们生活在一个每一个人只能拥有一只狗,且每只狗只能有一个主人的 “悲惨世界” 中,这就是一对一关系。若是要以关系型数据库的方式来反应它的话,咱们能够建立两张表: Dog 表和 Owner 表,其中 Dog 表经过 owner id 来引用 Owner 表中的数据,或者 Owner 表经过 dog id 来引用 Dog 表中的数据。android
@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 类:sql
data class DogAndOwner( val owner: Owner, val dog: Dog )
为了在 SQLite 中进行查询,咱们须要 1) 运行两个查询: 一个获取全部的主人数据,一个获取全部的狗狗数据,2) 根据 owner id 来进行数据的关系映射。数据库
SELECT * FROM Owner SELECT * FROM Dog WHERE dogOwnerId IN (ownerId1, ownerId2, …)
要在 Room 中获取一个 List<DogAndOwner>
,咱们不须要本身去实现上面说的查询和映射,只须要使用 @Relation
注解。google
在咱们的示例中,因为 Dog
有了 owner 的信息,咱们给 dog 变量增长 @Relation
注解,指定父级 (这里对应 Owner
) 上的 ownerId 列对应 dogOwnerId
:spa
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 注解来确保这个行为是原子性的。3d
再假设,一个主人能够养多只狗狗,如今上面的关系就变成了一对多关系。咱们以前定义的数据库 schema 并不须要改变,仍然使用一样的表结构,由于在 “多” 这一方的表中已经有了关联键。code
如今,要展现狗和主人的列表,咱们须要建立一个新的类来进行建模:sqlite
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 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) 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。