Skip to content

定义对象之间的关系

由于 SQLite 是关系型数据库,因此您可以定义各个实体之间的关系。 虽然大多数对象关系映射库都允许实体对象相互引用,但 Room 明确禁止这样做。 如需了解此决策背后的技术原因,请参阅了解 Room 为何不允许对象引用

两种可能的方法

在 Room 中,您可以通过两种方式定义和查询实体之间的关系:使用具有嵌入式对象的中间数据类,或使用具有多重映射返回值类型的关系型查询方法。

中间数据类

在中间数据类方法中,您可以定义数据类,以便在 Room 实体之间建立关系。此数据类保存一个实体的实例与另一个实体的实例之间的配对(作为嵌入式对象)。然后,查询方法可以返回此数据类的实例,以供您的应用使用。

例如,您可以定义 UserBook 数据类来表示已借阅特定图书的图书馆用户,并定义一个查询方法用于从数据库中检索 UserBook 实例的列表:

kotlin
@Dao
interface UserBookDao {
    @Query(
        "SELECT user.name AS userName, book.name AS bookName " +
                "FROM user, book " +
                "WHERE user.id = book.user_id"
    )
    fun loadUserAndBookNames(): LiveData<List<UserBook>>
}

data class UserBook(val userName: String?, val bookName: String?)

多重映射返回值类型

⭐ 注意

Room 在 2.4 及更高版本中仅支持多重映射返回值类型。

在多重映射返回值类型方法中,您无需定义任何其他数据类,而是根据所需的映射结构为您的方法定义多重映射 返回值类型,并直接在 SQL 查询中定义实体之间的关系。

例如,以下查询方法会返回 UserBook 实例的映射,用于表示已借阅特定图书的图书馆用户:

kotlin
@Query(
    "SELECT * FROM user" +
            "JOIN book ON user.id = book.user_id"
)
fun loadUserAndBookNames(): Map<User, List<Book>>

选择一种方法

oom 支持这两种方法,因此您可以使用最适合您的应用的任意一种方法。本部分讨论了您可能会倾向于使用其中一种的原因。

中间数据类方法可让您避免编写复杂的 SQL 查询,但由于需要额外的数据类,它还可能会导致代码复杂性增加。 简而言之,多重映射返回值类型方法需要 SQL 查询完成更多工作;而中间数据类方法需要代码完成更多工作。

如果您没有使用中间数据类的特定原因,我们建议您使用多重映射返回值类型方法。如需详细了解此方法,请参阅返回多重映射

本指南的其余部分演示了如何使用中间数据类方法定义关系。

创建嵌套对象

有时,您可能希望在数据库逻辑中将某个实体或数据对象表示为一个紧密的整体,即使该对象包含多个字段也是如此。 在这些情况下,您可以使用 @Embedded 注释表示要分解为表格中的子字段的对象。 然后,您可以像查询其他各个列一样查询嵌套字段。

例如,您的 User 类可以包含一个 Address 类型的字段,它表示名为 streetcitystatepostCode 的字段的组合。 若要在表中单独存储组合列,请在带有 @Embedded 注解的 User 类中添加 Address 字段,如以下代码段所示:

kotlin
data class Address(
    val street: String?,
    val state: String?,
    val city: String?,
    @ColumnInfo(name = "post_code") val postCode: Int
)

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

然后,表示 User 对象的表将包含具有以下名称的列:idfirstNamestreetstatecitypost_code

⭐ 注意

嵌套字段还可以包含其他嵌套字段。

如果某个实体具有相同类型的多个嵌套字段,您可以通过设置 prefix 属性确保每个列的唯一性。然后,Room 会将提供的值添加到嵌套对象中每个列名称的开头。

定义一对一关系

两个实体之间的一对一关系是指这样一种关系:父实体的每个实例恰好对应于子实体的 1 个实例,反之亦然。

例如,假设有一个音乐在线播放应用,用户在该应用中具有一个属于自己的歌曲库。每个用户只有一个库,而且每个库恰好对应于一个用户。因此,User 实体和 Library 实体之间是一对一的关系。

如需定义一对一关系,请先为两个实体分别创建一个类。其中一个实体必须包含一个变量,且该变量是对另一个实体的主键的引用。

kotlin
@Entity
data class User(
    @PrimaryKey val userId: Long,
    val name: String,
    val age: Int
)

@Entity
data class Library(
    @PrimaryKey val libraryId: Long,
    val userOwnerId: Long
)

如需查询用户列表和对应的库,您必须先在两个实体之间建立一对一关系。 为此,请创建一个新的数据类,其中每个实例都包含父实体的一个实例和与之对应的子实体实例。 将 @Relation 注释添加到子实体的实例,同时将 parentColumn 设置为父实体主键列的名称,并将 entityColumn 设置为引用父实体主键的子实体列的名称。

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

最后,向 DAO 类添加一个方法,用于返回将父实体与子实体配对的数据类的所有实例。 该方法需要 Room 运行两次查询,因此应向该方法添加 @Transaction 注释,以确保整个操作以原子方式执行。

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

定义一对多关系

两个实体之间的一对多关系是指这样一种关系:父实体的每个实例对应于子实体的零个或多个实例,但子实体的每个实例只能恰好对应于父实体的一个实例。

在音乐在线播放应用示例中,假设用户可以将其歌曲整理到播放列表中。每个用户可以创建任意数量的播放列表,但每个播放列表只能由一个用户创建。因此,User 实体和 Playlist 实体之间存在一对多关系。

如需定义一对多关系,请先为这两个实体分别创建一个类。与一对一关系一样,子实体必须包含一个变量,且该变量是对父实体的主键的引用。

kotlin
@Entity
data class User(
    @PrimaryKey val userId: Long,
    val name: String,
    val age: Int
)

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

为了查询用户列表和对应的播放列表,您必须先在两个实体之间建立一对多关系。为此,请创建一个新的数据类,其中每个实例都包含父实体的一个实例和与之对应的所有子实体实例的列表。 将 @Relation 注释添加到子实体的实例,同时将 parentColumn 设置为父实体主键列的名称,并将 entityColumn 设置为引用父实体主键的子实体列的名称。

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

最后,向 DAO 类添加一个方法,用于返回将父实体与子实体配对的数据类的所有实例。 该方法需要 Room 运行两次查询,因此应向该方法添加 @Transaction 注释,以确保整个操作以原子方式执行。

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

定义多对多关系

两个实体之间的多对多关系是指这样一种关系:父实体的每个实例对应于子实体的零个或多个实例,反之亦然。

在音乐在线播放应用示例中,考虑用户定义的播放列表中的歌曲。每个播放列表都可以包含多首歌曲,每首歌曲都可以包含在多个不同的播放列表中。因此,Playlist 实体和 Song 实体之间存在多对多关系。

如需定义多对多关系,请先为两个实体分别创建一个类。多对多关系与其他关系类型均不同的一点在于,子实体中通常不存在对父实体的引用。 因此,需要创建第三个类来表示两个实体之间的关联实体(即交叉引用表)。 交叉引用表中必须包含表中表示的多对多关系中每个实体的主键列。在本例中,交叉引用表中的每一行都对应于 Playlist 实例和 Song 实例的配对,其中引用的歌曲包含在引用的播放列表中。

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

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

@Entity(primaryKeys = ["playlistId", "songId"])
data class PlaylistSongCrossRef(
    val playlistId: Long,
    val songId: Long
)

下一步取决于您想如何查询这些相关实体。

  • 如果您想查询播放列表和每个播放列表所含歌曲的列表,则应创建一个新的数据类,其中包含单个 Playlist 对象,以及该播放列表所包含的所有 Song 对象的列表。
  • 如果您想查询歌曲和每首歌曲所在播放列表的列表,则应创建一个新的数据类,其中包含单个 Song 对象,以及包含该歌曲的所有 Playlist 对象的列表。

在这两种情况下,都可以通过以下方法在实体之间建立关系:在上述每个类中的 @Relation 注释中使用 associateBy 属性来确定提供 Playlist 实体与 Song 实体之间关系的交叉引用实体。

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

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

最后,向 DAO 类添加一个方法,用于提供您的应用所需的查询功能。

  • getPlaylistsWithSongs:该方法会查询数据库并返回查询到的所有 PlaylistWithSongs 对象。
  • getSongsWithPlaylists:该方法会查询数据库并返回查询到的所有 SongWithPlaylists 对象。

这两个方法都需要 Room 运行两次查询,因此应为这两个方法添加 @Transaction 注释,以确保整个操作以原子方式执行。

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

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

⭐ 注意

如果 @Relation 注释不适用于您的特定用例,您可能需要在 SQL 查询中使用 JOIN 关键字来手动定义适当的关系。 如需详细了解如何手动查询多个表,请参阅使用 Room DAO 访问数据

定义嵌套关系

有时,您可能需要查询包含三个或更多表格的集合,这些表格之间互相关联。在这种情况下,您需要定义各个表之间的嵌套关系。

⚠️ 注意

使用嵌套关系查询数据需要 Room 处理大量数据,可能会影响性能。因此,请在查询中尽量少用嵌套关系。

在音乐在线播放应用示例中,假设您想要查询所有用户、每个用户的所有播放列表以及每个用户的各个播放列表中包含的所有歌曲。用户与播放列表之间存在一对多关系,而播放列表与歌曲之间存在多对多关系。 以下代码示例显示了代表这些实体以及播放列表与歌曲之间多对多关系的交叉引用表的类:

kotlin
@Entity
data class User(
    @PrimaryKey val userId: Long,
    val name: String,
    val age: Int
)

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

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

@Entity(primaryKeys = ["playlistId", "songId"])
data class PlaylistSongCrossRef(
    val playlistId: Long,
    val songId: Long
)

首先,按照常规方法使用数据类和 @Relation 注释在集合中的两个表格之间建立关系。 以下示例展示了一个 PlaylistWithSongs 类,该类可在 Playlist 实体类和 Song 实体类之间建立多对多关系:

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

定义表示此关系的数据类后,请创建另一个数据类,用于在集合中的另一个表与第一个关系类之间建立关系,并将现有关系嵌套到新关系中。 以下示例展示了一个 UserWithPlaylistsAndSongs 类,该类可在 User 实体类和 PlaylistWithSongs 关系类之间建立一对多关系:

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

UserWithPlaylistsAndSongs 类间接地在以下三个实体类之间建立了关系:UserPlaylistSong。 图 1 展示了该关系。

图 1. 音乐在线播放应用示例中关系类的示意图。

如果集合中还有其他表,则可以创建类在剩余的每个表和关系类(此类可在之前的所有表之间建立关系)之间建立关系。这样会在您要查询的所有表之间创建嵌套关系链。

最后,向 DAO 类添加一个方法,用于提供您的应用所需的查询功能。该方法需要 Room 运行多次查询,因此应添加 @Transaction 注释,以确保整个操作以原子方式执行。

kotlin
@Transaction
@Query("SELECT * FROM User")
fun getUsersWithPlaylistsAndSongs(): List<UserWithPlaylistsAndSongs>