定义对象之间的关系

由于 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?)

Java

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

public class UserBook {
    public String userName;
    public String bookName;
}

多图返回值类型

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

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

Kotlin

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

Java

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

选择一种方法

Room 支持这两种方法,因此您可以使用最适合您应用的方法。本节讨论了您可能更喜欢其中一种方法的一些原因。

中间数据类方法允许您避免编写复杂的 SQL 查询,但它也可能导致代码复杂性增加,因为它需要额外的类。简而言之,多图返回值类型方法要求您的 SQL 查询做更多工作,而中间数据类方法要求您的代码做更多工作。

如果您没有使用中间数据类的特定原因,我们建议您使用多图返回值类型方法。要了解有关此方法的更多信息,请参阅 返回多图

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

创建嵌入对象

有时,您希望在数据库逻辑中将实体或数据对象表示为一个完整的整体,即使该对象包含多个字段。在这些情况下,您可以使用 @Embedded 注解来表示您希望在表中分解为子字段的对象。然后,您可以像对其他单个列一样查询嵌入字段。

例如,您的 User 类可以包含一个类型为 Address 的字段,该字段表示名为 streetcitystatepostCode 的字段的组合。要将组合列分别存储在表中,请在 User 类中包含一个使用 @Embedded 注解的 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?
)

Java

public class Address {
    public String street;
    public String state;
    public String city;

    @ColumnInfo(name = "post_code") public int postCode;
}

@Entity
public class User {
    @PrimaryKey public int id;

    public String firstName;

    @Embedded public Address address;
}

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

如果一个实体有多个相同类型的嵌入式字段,可以通过设置prefix 属性来保持每个列的唯一性。然后,Room 会将提供的 value 添加到嵌入对象中每个列名的开头。

定义一对一关系

两个实体之间的一对一关系是指父实体的每个实例都对应于子实体的一个实例,反之亦然。

例如,考虑一个音乐流媒体应用程序,其中用户拥有一个他们自己的歌曲库。每个用户只有一个库,每个库都对应于一个用户。因此,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
)

Java

@Entity
public class User {
    @PrimaryKey public long userId;
    public String name;
    public int age;
}

@Entity
public class Library {
    @PrimaryKey public long libraryId;
    public long userOwnerId;
}

要查询用户和对应库的列表,必须首先对这两个实体之间的一对一关系进行建模。为此,创建一个新的数据类,其中每个实例都包含父实体的一个实例和子实体的对应实例。将 @Relation 注解添加到子实体的实例中,并将 parentColumn 设置为父实体主键列的名称,并将 entityColumn 设置为子实体中引用父实体主键的列的名称。

Kotlin

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

Java

public class UserAndLibrary {
    @Embedded public User user;
    @Relation(
         parentColumn = "userId",
         entityColumn = "userOwnerId"
    )
    public Library library;
}

最后,向 DAO 类添加一个方法,该方法返回配对父实体和子实体的数据类的所有实例。此方法需要 Room 运行两个查询,因此为此方法添加 @Transaction 注解,以便整个操作以原子方式执行。

Kotlin

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

Java

@Transaction
@Query("SELECT * FROM User")
public List<UserAndLibrary> getUsersAndLibraries();

定义一对多关系

两个实体之间的一对多关系是指父实体的每个实例都对应于零个或多个子实体的实例,但子实体的每个实例只能对应于父实体的一个实例。

在音乐流媒体应用程序示例中,假设用户能够将其歌曲组织成播放列表。每个用户可以创建任意数量的播放列表,但每个播放列表都由一个用户创建。因此,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
)

Java

@Entity
public class User {
    @PrimaryKey public long userId;
    public String name;
    public int age;
}

@Entity
public class Playlist {
    @PrimaryKey public long playlistId;
    public long userCreatorId;
    public String playlistName;
}

要查询用户和对应播放列表的列表,必须首先对这两个实体之间的一对多关系进行建模。为此,创建一个新的数据类,其中每个实例都包含父实体的一个实例以及所有对应子实体实例的列表。将 @Relation 注解添加到子实体的实例中,并将 parentColumn 设置为父实体主键列的名称,并将 entityColumn 设置为子实体中引用父实体主键的列的名称。

Kotlin

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

Java

public class UserWithPlaylists {
    @Embedded public User user;
    @Relation(
         parentColumn = "userId",
         entityColumn = "userCreatorId"
    )
    public List<Playlist> playlists;
}

最后,向 DAO 类添加一个方法,该方法返回配对父实体和子实体的数据类的所有实例。此方法需要 Room 运行两个查询,因此为此方法添加 @Transaction 注解,以便整个操作以原子方式执行。

Kotlin

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

Java

@Transaction
@Query("SELECT * FROM User")
public List<UserWithPlaylists> getUsersWithPlaylists();

定义多对多关系

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

在音乐流媒体应用程序示例中,考虑用户定义的播放列表中的歌曲。每个播放列表可以包含许多歌曲,每首歌曲可以是许多不同播放列表的一部分。因此,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
)

Java

@Entity
public class Playlist {
    @PrimaryKey public long playlistId;
    public String playlistName;
}

@Entity
public class Song {
    @PrimaryKey public long songId;
    public String songName;
    public String artist;
}

@Entity(primaryKeys = {"playlistId", "songId"})
public class PlaylistSongCrossRef {
    public long playlistId;
    public long songId;
}

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

  • 如果要查询播放列表以及每个播放列表的对应歌曲列表,请创建一个新的数据类,其中包含单个 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>
)

Java

public class PlaylistWithSongs {
    @Embedded public Playlist playlist;
    @Relation(
         parentColumn = "playlistId",
         entityColumn = "songId",
         associateBy = @Junction(PlaylistSongCrossref.class)
    )
    public List<Song> songs;
}

public class SongWithPlaylists {
    @Embedded public Song song;
    @Relation(
         parentColumn = "songId",
         entityColumn = "playlistId",
         associateBy = @Junction(PlaylistSongCrossref.class)
    )
    public List<Playlist> playlists;
}

最后,向 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>

Java

@Transaction
@Query("SELECT * FROM Playlist")
public List<PlaylistWithSongs> getPlaylistsWithSongs();

@Transaction
@Query("SELECT * FROM Song")
public List<SongWithPlaylists> getSongsWithPlaylists();

定义嵌套关系

有时,您可能需要查询一组三个或更多相互关联的表。在这种情况下,您需要在表之间定义嵌套关系

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

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
)

Java

@Entity
public class User {
    @PrimaryKey public long userId;
    public String name;
    public int age;
}

@Entity
public class Playlist {
    @PrimaryKey public long playlistId;
    public long userCreatorId;
    public String playlistName;
}
@Entity
public class Song {
    @PrimaryKey public long songId;
    public String songName;
    public String artist;
}

@Entity(primaryKeys = {"playlistId", "songId"})
public class PlaylistSongCrossRef {
    public long playlistId;
    public long songId;
}

首先,像往常一样使用数据类和 @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>
)

Java

public class PlaylistWithSongs {
    @Embedded public Playlist playlist;
    @Relation(
         parentColumn = "playlistId",
         entityColumn = "songId",
         associateBy = Junction(PlaylistSongCrossRef.class)
    )
    public List<Song> songs;
}

定义表示此关系的数据类后,创建另一个数据类来对您集合中的另一个表与第一个关系类之间的关系进行建模,将现有关系“嵌套”在新关系中。以下示例显示了一个 UserWithPlaylistsAndSongs 类,该类对 User 实体类和 PlaylistWithSongs 关系类之间的一对多关系进行建模

Kotlin

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

Java

public class UserWithPlaylistsAndSongs {
    @Embedded public User user;
    @Relation(
        entity = Playlist.class,
        parentColumn = "userId",
        entityColumn = "userCreatorId"
    )
    public List<PlaylistWithSongs> playlists;
}

UserWithPlaylistsAndSongs 类间接地对所有三个实体类(UserPlaylistSong)之间的关系进行建模。图 1 说明了这一点。

UserWithPlaylistsAndSongs models the relationship between User and
  PlaylistWithSongs, which in turn models the relationship between Playlist
  and Song.

图 1. 音乐流媒体应用程序示例中关系类的图。

如果集合中还有其他表,请创建一个类来对每个剩余表与表示所有先前表之间关系的关系类之间的关系进行建模。这会在您想要查询的所有表之间创建嵌套关系链。

最后,向 DAO 类添加一个方法来公开应用程序需要的查询功能。此方法需要 Room 运行多个查询,因此添加 @Transaction 注解以使整个操作以原子方式执行

Kotlin

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

Java

@Transaction
@Query("SELECT * FROM User")
public List<UserWithPlaylistsAndSongs> getUsersWithPlaylistsAndSongs();

其他资源

要了解有关在 Room 中定义实体之间关系的更多信息,请参阅以下其他资源。

示例

视频

博客