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通过把内嵌对象的属性解包到被宿主中,建立了实体的连接。此外还可以通过@Relation
和 foreignkeys
来描述实体之间更加复杂的关系。
我们至少可以描述三种实体关系
- 一对一
- 一对多或多对一
- 多对多
一对一
主表(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>