定義物件之間的關係

由於 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 所示。

UserWithplaylistAndSongs 建立了 User 和 PlaylistWithSongs 之間的關係模型,進而建立 Playlist 和 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 中定義實體之間的關係,請參閱下列其他資源。

範例

影片

網誌