定义对象之间的关系

由于 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 会将提供的值添加到嵌入对象中每个列名的开头。

定义一对一关系

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

例如,考虑一个音乐流媒体应用程序,其中用户拥有一个他们拥有的歌曲库。每个用户只有一个库,并且每个库都对应于一个用户。因此,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 中定义实体之间关系的更多信息,请参阅以下其他资源。

示例

视频

博客