[译] 如何用 Room 处理一对一,一对多,多对多关系?

简介: [译] 如何用 Room 处理一对一,一对多,多对多关系?

将数据拆分为相关联的表,并以有意义的方式将数据组合在一起 是设计关系型数据库的重要部分。从 Room 2.2 (现已稳定)开始,通过 @Relation 注解,我们支持了表之间所有可能的关系:一对一一对多多对多


一对一


image.png

假如我们生活在一个(悲伤的)世界,每个人只能拥有一条狗,并且每条狗也只能有一个主人。这就是一对一关系。为了在关系型数据库中 表示这一关系,我们创建了两张表,DogOwnerDog 表持有 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 完成此次查询,我们需要:

  1. 进行两次查询,先查询出所有的主人,然后在根据主人的 owner id 查询出所有的狗
  2. 处理对象映射
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 注解以保证原子性。


一对多


image.png

假设一个主人可以拥有多条狗狗 (Yeah !) ,OwnerDog 之间是一对多的关系。之前定义的数据库结构不需要发生任何变化,我们仍然使用之前的表,因为相关联的键已经在表中了。

现在,为了展示主人和他的狗狗们,我们需要创建一个新的数据类:

data class OwnerWithDogs(
    val owner: Owner,
    val dogs: List<Dog>
)
复制代码


为了避免两次查询,我们给 List<Dog> 添加 @Relation 注解来定义 DogOwner 之间的一对多关系。

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>
复制代码


多对多


image.png

现在假设我们生活在一个完美的世界,每个主人可以拥有多条狗,每条狗也可以有多个主人。要对此关系进行建模,仅仅通过 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 根据被注解的属性类型来推断使用哪个实体类。例如,到目前为止,我们给 DogList<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>
)
复制代码


如果你想在 dogOwnerIdownerId 之间定义更加严格的关系,独立于你所创建的任何关系,可以在这些字段之间添加 ForeignKey 约束。请记住,SQLite 外键定义索引,并且可以具有级联触发器来更新或删除表中的条目。因此,请根据是否希望在数据库中使用这种功能来决定是否要使用外键。


无论你需要一对一,一对多,还是多对多的支持,Room 都可以通过 @Relation 注释满足你。



相关文章
|
缓存 Java 数据库连接
使用Mybatis与直觉用jdbc相比,哪个更有优势?
Mybatis如果仅仅是要【会用】,入门是非常快的,感觉两三天就可以搞好了。
279 0
|
开发者 容器
ArkUI框架,Flex布局,基础组件及封装,父子组件的双向绑定
Flex组件用于创建弹性布局,开发者可以通过Flex的接口创建容器组件,进而对容器内的其他元素进行弹性布局,例如:使三个元素在容器内水平居中,垂直等间隔分散。 例如:如下的布局代码分别表示:(垂直排列,水平居中,垂直居中)
670 0
ArkUI框架,Flex布局,基础组件及封装,父子组件的双向绑定
|
Java 开发者 C++
Java多线程同步大揭秘:synchronized与Lock的终极对决!
【6月更文挑战第20天】在Java多线程编程中,`synchronized`和`Lock`是两种关键的同步机制。`synchronized`作为内置关键字提供基础同步,简单但可能不够灵活;而`Lock`接口自Java 5引入,提供更复杂的控制和优化性能的选项。在低竞争场景下,`synchronized`性能可能更好,但在高并发或需要精细控制时,`Lock`(如`ReentrantLock`)更具优势。选择哪种取决于具体需求和场景,理解两者机制至关重要。
256 1
|
Android开发 开发者 索引
Android实战经验之如何使用DiffUtil提升RecyclerView的刷新性能
本文介绍如何使用 `DiffUtil` 实现 `RecyclerView` 数据集的高效更新,避免不必要的全局刷新,尤其适用于处理大量数据场景。通过定义 `DiffUtil.Callback`、计算差异并应用到适配器,可以显著提升性能。同时,文章还列举了常见错误及原因,帮助开发者避免陷阱。
1038 9
|
编译器 Android开发 开发者
Android经典实战之Kotlin 2.0 迁移指南:全方位优化与新特性解析
本文首发于公众号“AntDream”。Kotlin 2.0 已经到来,带来了 K2 编译器、多平台项目支持、智能转换等重大改进。本文提供全面迁移指南,涵盖编译器升级、多平台配置、Jetpack Compose 整合、性能优化等多个方面,帮助开发者顺利过渡到 Kotlin 2.0,开启高效开发新时代。
573 0
|
JSON 关系型数据库 MySQL
MySQL 5.x和8.0区别
性能:8.0的速度要比5.7快2倍,8.0在以下方面带来了更好的性能:读/写负载、IO密集型工作负载、高竞争("hot spot"热点竞争问题)工作负载。
482 3
|
存储 监控 供应链
账单系统-架构设计思路(对外版)
阿里商旅背景阿里商旅作为飞猪旅行旗下面向企业客户的数字化差旅解决方案产品,依托飞猪旅行机票、酒店供应链,为企业客户提供一站式的机票、酒店、火车票、用车等预订管控及结算票据服务。阿里商旅不仅是集团欢行的供应商,而且近几年在商业化差旅市场上崭露头角,服务了2万+中大型客户,43万+小微企业。FY22财年商旅技术团队重点规划在酒店供应链、预订管控服务、B+C客户服务、渠道及商旅基础建设等核心方向进行建设
5292 2
账单系统-架构设计思路(对外版)
|
算法 Serverless 数据安全/隐私保护
2024蓝桥杯RSA-Theorem
2024蓝桥杯RSA-Theorem
|
监控 网络协议 关系型数据库
Keepalived集群软件高级使用(工作原理和状态通知)
Keepalived集群软件高级使用(工作原理和状态通知)