Jetpack Room入门系列:(三)实体/数据表关系

在这里插入图片描述
Jetpack Room入门系列:(一)基本介绍
Jetpack Room入门系列:(二)使用DAO读写数据库
Jetpack Room入门系列:(三)实体/数据表关系
Jetpack Room入门系列:(四)内部实现原理
Jetpack Room入门系列:(五)数据库版本升级、数据迁移
Jetpack Room入门系列:(六)配合LiveData等三方库的使用


对于关系型数据库来说,最重要的是如何将数据拆分为有相关关系的多个数据表。SQLite作为关系型数据库,允许entits之间可以有多种关系,Room提供了多种方式表达这种关系。


@Embedded内嵌对象


@Embedded注解可以将一个Entity作为属性内嵌到另一Entity,我们可以像访问Column一样访问内嵌Entity

内嵌实体本身也可以包括其他内嵌对象

data class Address(
  val street: String?,
  val state: String?,
  val city: String?,
  val postCode: Int
)

@Entity
data class User(
  @PrimaryKey val id: Int,
  val firstName: String?,
  @Embedded val address: Address?
)

如上,等价于User表包含了 id, firstName, street, state, city, postCode等column

如果内嵌对象中存在同名字段,可以使用prefix指定前缀加以区分

@Embedded通过把内嵌对象的属性解包到被宿主中,建立了实体的连接。此外还可以通过@Relationforeignkeys来描述实体之间更加复杂的关系。

我们至少可以描述三种实体关系

  • 一对一
  • 一对多或多对一
  • 多对多

一对一


主表(Parent Entity)中的每条记录与从表(Child Entity)中的每条记录一一对应。

设想一个音乐app的场景,用户(User)和曲库(Library)有如下关系:

  • 一个User只有一个Library
  • 一个Library只属于唯一User
@Entity
data class User(
  @PrimaryKey val userId: Long,
  val name: String,
  val age: Int
)

@Entity(foreignKeys = @ForeignKey(entity = User.class,
    parentColumns = "userId",
    childColumns = "userOwnerId",
    onDelete = CASCADE))
data class Library(
  @PrimaryKey val libraryId: Long,
  val title: String,
  val userOwnerId: Long
)

data class UserAndLibrary(
  @Embedded val user: User,
  @Relation(
      parentColumn = "userId",
      entityColumn = "userOwnerId"
  )
  val library: Library
)

如上,User和Library之间属于一对一的关系。

foreignkeys

foreignkeys作为@Relation的属性用来定义外键约束。外键只能在从表上,从表需要有字段对应到主表的主键(Library的userOwnerId对应到User的userId)。

外键约束属性:当有删除或者更新操作的时候发出这个约束

通过外键约束,对主表的操作会受到从表的影响。例如当在主表(即外键的来源表)中删除对应记录时,首先检查该记录是否有对应外键,如果有则不允许删除。

@Relation

为了能够对User以及关联的Library进行查询,需要为两者之间建立一对一关系:

  • 通过UserAndLibrary定义这种关系,包含两个成员分别是主表和从表的实体
  • 为从表添加@Relation注解
  • parentColumn:主表主键
  • entityColumn:从表外键约束的字段

然后,可以通过UserAndLibrary进行查询

@Transaction
@Query("SELECT * FROM User")
fun getUsersAndLibraries(): List<UserAndLibrary>

此方法要从两个表中分别进行两次查询,所以@Transaction确保方法中的多次查询的原子性


一对多


主表中的一条记录对应从表中的零到多条记录。

在前面音乐APP的例子中,有如下一对多关系:

  • 一个User可以创建多个播放列表(Playlist)
  • 每个Playlist只能有唯一的创作者(User)
@Entity
data class User(
  @PrimaryKey val userId: Long,
  val name: String,
  val age: Int
)

@Entity(foreignKeys = @ForeignKey(entity = User.class,
    parentColumns = "userId",
    childColumns = "userCreatorId",
    onDelete = CASCADE))
  data class Playlist(
  @PrimaryKey val playlistId: Long,
  val userCreatorId: Long,
  val playlistName: String
)

data class UserWithPlaylists(
  @Embedded val user: User,
  @Relation(
      parentColumn = "userId",
      entityColumn = "userCreatorId"
  )
  val playlists: List<Playlist>
)

可以看到,一对多关系的UserWithPlaylists与一对一类似, 只是playlists需要是一个List表示从表中的记录不止一个。

查询方法如下:

@Transaction
@Query("SELECT * FROM User")
fun getUsersWithPlaylists(): List<UserWithPlaylists>

多对多


主表中的一条记录对应从表中的零活多个,反之亦然

  • 每个Playlist中可以有很多首歌曲(Song)
  • 每个Song可以归属不同的Playlist

因此,Playlist与Song之间是多对多的关系

@Entity
data class Playlist(
  @PrimaryKey val id: Long,
  val playlistName: String
)

@Entity
data class Song(
  @PrimaryKey val id: Long,
  val songName: String,
  val artist: String
)

@Entity(primaryKeys = ["playlistId", "songId"],
    foreignKeys = {
      @ForeignKey(entity = Playlist.class,
      parentColumns = "id",
      childColumns = "playlistId"),
      @ForeignKey(entity = Song.class,
      parentColumns = "id",
      childColumns = "songId")
}))

data class PlaylistSongCrossRef(
  val playlistId: Long,
  val songId: Long
)

多对多关系中,Song和Playlist之间没有明确的外键约束关系,需要定义一个 associative entity(又或者称作交叉连接表):PlaylistSongCrossRef,然后分别与Song和Playlist建立外键约束。交叉连接的结果是Song与Playlist的笛卡尔积,即两个表中所有记录的组合。

基于交叉连接表,我们可以获取一首Song与其包含它的所有Playlist,又或者一个Playlist与其包含的所有Song。

如果使用SQL获取指定Playlist与其包含的Song,需要两条查询:

# 查询playlist信息
SELECT * FROM Playlist
# 查询Song信息
SELECT
     Song.id AS songId,
     Song.name AS songName,
     _junction.playlistId
FROM
     PlaylistSongCrossRef AS _junction
INNER JOIN Song ON (_junction.songId = Song.id)
 
 
# WHERE _junction.playlistId IN (playlistId1, playlistId2, …)

如果使用Room,则需要定义PlaylistWithSongs类,并告诉其使用PlaylistSongCrossRef作为连接:

data class PlaylistWithSongs(
  @Embedded val playlist: Playlist,
  @Relation(
      parentColumn = "playlistId",
      entityColumn = "songId",
      associateBy = @Junction(PlaylistSongCrossRef::class)
  )
  val songs: List<Song>
)

同理,也可定义SongWithPlaylists

data class SongWithPlaylists(
  @Embedded val song: Song,
  @Relation(
      parentColumn = "songId",
      entityColumn = "playlistId",
      associateBy = @Junction(PlaylistSongCrossRef::class)
  )
  val playlists: List<Playlist>
)

查询与前面类似,很简单:

@Transaction
@Query("SELECT * FROM Playlist")
fun getPlaylistsWithSongs(): List<PlaylistWithSongs>

@Transaction
@Query("SELECT * FROM Song")
fun getSongsWithPlaylists(): List<SongWithPlaylists>

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

fundroid

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值