在使用資料庫時,我們查詢的資料可能會關聯到多個資料表。Android Room 允許我們定義物件間的關聯。在查詢資料時,Android Room 會自動連同關聯的資料一起讀取出來。本文章將介紹如何定義這些關聯。
Room
本文章中,我們不會介紹如何使用 Room。如果你還不熟悉 Room 的話,可先參考以下文章。
內嵌物件
假設我們有一個資料表 books 如下。它包含了書本和作者的資料。

Room 提供一個物件內嵌的方法。如下程式碼中,Book 包含書本的資料,而將作者的資料放在 Author 中。然後將 Author 內嵌到 Book 中。這樣可以使程式碼更加地結構化。使用方式就是在 Book::author 前面加上 @Embedded。
值得注意的是,Author 並不是一個 entity,它只是一個包含 books 中的一些資料的物件。所以不要在 Author 上加上 @Entity。
@Entity("books")
data class Book(
@PrimaryKey val bookId: String,
val bookName: String,
@Embedded val author: Author,
)
data class Author(
val authorName: String,
val authorEmail: String,
)@Dao
interface BookDao {
@Upsert
suspend fun upsert(entity: Book)
@Query("SELECT * FROM books")
fun findAll(): Flow>
}
val dao = BookDatabase.getInstance(context).bookDao()
dao.upsert(
Book(
"b1",
"How to read a book",
Author("Charles", "charles@gmail.com"),
)
)
dao.upsert(
Book(
"b2",
"Atomic Habits",
Author("James", "james@gmail.com"),
)
)
dao.findAll()
.collect {
Log.d("MainViewModel", "books=$it")
}
}另外,我們可以在 @Embedded 中加上 prefix 字串來改善 Author 的 fields 的命名。

@Entity("books")
data class Book(
@PrimaryKey @ColumnInfo("book_id") val bookId: String,
val name: String,
@Embedded(prefix = "author_") val author: Author,
)
data class Author(
val name: String,
val email: String,
)一對一關係
接下來,我們想要將作者的資料從 books 中分離到另一個資料表 authors,如下圖。和之前將書本和作者資料放在同一個資料表相比,此種方式在實作上是比較常見的。

Room 可以在查詢 books 時,也會將在 authors 中相關的資料一起讀取出來。在以下的程式碼中,我們要將 Book 和 Author 加上 @Entity,因為他們都是資料表。
宣告 BookAndAuthor 物件,在裡面宣告一個 Book 和一個 Author。因為我們主要是查詢 books 資料表,然後希望可以一起讀取在 authors 中相關的資料,所以用 @Embedded 將 Book 內嵌到 BookAndAuthor 裡面。然後,用 @Relation 來定義 Book 和 Author 之間的關係。用 parentColumn 指定在 Book 中要被關聯的 field,也就是 Book::id。然後,用 entityColumn 指定在 Author 中要關聯到 parentColumn 的 field,也就是 Author::bookId。
@Entity("books")
data class Book(
@PrimaryKey val id: String,
val name: String,
)
@Entity("authors")
data class Author(
@PrimaryKey val id: String,
val name: String,
val email: String,
val bookId: String,
)
data class BookAndAuthor(
@Embedded val book: Book,
@Relation(
parentColumn = "id",
entityColumn = "bookId",
)
val author: Author,
)在 BookAndAuthor 中定義好 Book 與 Author 間的關聯。然後,在 BookDao 中的 findAll(),我們只需要查詢 books 即可,Room 就會依照定義好的關聯,將在 authors 中相關的資料一起讀取出來。
另外,我們還要在 BookDao::findAll() 上加上 @Transaction。因為 Room 實際上會執行兩個 queries,為了確保整個查詢動作是 atomically,我們必須要加上 @Transaction。
@Dao
interface BookDao {
@Upsert
suspend fun upsert(entity: Book)
@Transaction
@Query("SELECT * FROM books")
fun findAll(): Flow>
}
@Dao
interface AuthorDao {
@Upsert
suspend fun upsert(entity: Author)
}
val db = BookDatabase.getInstance(context)
db.bookDao().apply {
upsert(Book("b1", "How to read a book"))
upsert(Book("b2", "Atomic Habits"))
}
db.authorDao().apply {
upsert(Author("a1", "Charles", "charles@gmail.com", "b1"))
upsert(Author("a2", "James", "james@gmail.com", "b2"))
}
db.bookDao().findAll()
.collect {
Log.d("WAYNESTALK", "books=$it")
}一對多關係
至目前為止,books 與 authors 是一對一的關係。也就是說,一本書只能有一位作者。現在我們想調整成,一本書可以有多位作者。這也就是一對多的關係,如下圖。

程式碼與一對一關係幾乎相同,差別在於我們將 BookAuthor 重新命名為 BookAuthors。然後,將 BookAuthor::product 改為 BookAuthors::products,且將型態改為 List
@Entity("books")
data class Book(
@PrimaryKey val id: String,
val name: String,
)
@Entity("authors")
data class Author(
@PrimaryKey val id: String,
val name: String,
val email: String,
val bookId: String,
)
data class BookAndAuthors(
@Embedded val book: Book,
@Relation(
parentColumn = "id",
entityColumn = "bookId",
)
val authors: List,
) @Dao
interface BookDao {
@Upsert
suspend fun upsert(entity: Book)
@Transaction
@Query("SELECT * FROM books")
fun findAll(): Flow>
}
@Dao
interface AuthorDao {
@Upsert
suspend fun upsert(entity: Author)
}
val db = BookDatabase.getInstance(context)
db.bookDao().apply {
upsert(Book("b1", "How to read a book"))
upsert(Book("b2", "Atomic Habits"))
}
db.authorDao().apply {
upsert(Author("a11", "Charles", "charles@gmail.com", "b1"))
upsert(Author("a12", "Mortimer", "mortimer@gmail.com", "b1"))
upsert(Author("a2", "James", "james@gmail.com", "b2"))
}
db.bookDao().findAll()
.collect {
Log.d("WAYNESTALK", "books=$it")
}多對多關係
最後一種關係是多對多的關係。也就是說,一本書可以有多位作者,而一位作者也可以有多本書,如下圖。在使用多對多關係時,我們需要一個額外的 cross-reference table,如圖中的 book_author_cross_ref。它記錄著 books 和 authors 的對應關係。

在以下的程式碼中,我們新增 BookAuthorCrossRef 作為 books 和 authors 之間的 cross-reference table。BookAuthorCrossRef 中的 bookId 和 authorId 必須要設定為 primary keys。
如果你想要查詢書本資料,並且連同其關聯的作者資料一起讀取起來的話,就使用 BookAndAuthors。在 BookAndAuthors 中,用 @Relation 來定義 Book 和 Author 之間的關係。用 parentColumn 指定在 Book 中要被關聯的 field,也就是 Book::id。然後,用 entityColumn 指定在 Author 中要關聯到 parentColumn 的 field,也就是 Author::bookId。此外,再用 associateBy 指定 cross-reference table。
反之,如果你想要查詢作者資料,並且連同其關聯的書本資料一起讀取起來的話,就使用 AuthorAndBooks。
@Entity("books")
data class Book(
@PrimaryKey val bookId: String,
val name: String,
)
@Entity("authors")
data class Author(
@PrimaryKey val authorId: String,
val name: String,
val email: String,
)
@Entity(
tableName = "book_author_cross_ref",
primaryKeys = ["bookId", "authorId"],
)
data class BookAuthorCrossRef(
val bookId: String,
val authorId: String,
)
data class BookAndAuthors(
@Embedded val book: Book,
@Relation(
parentColumn = "bookId",
entityColumn = "authorId",
associateBy = Junction(BookAuthorCrossRef::class),
)
val authors: List,
)
data class AuthorAndBooks(
@Embedded val author: Author,
@Relation(
parentColumn = "authorId",
entityColumn = "bookId",
associateBy = Junction(BookAuthorCrossRef::class),
)
val books: List,
) @Dao
interface BookDao {
@Upsert
suspend fun upsert(entity: Book)
@Transaction
@Query("SELECT * FROM books")
fun findAll(): List
}
@Dao
interface AuthorDao {
@Upsert
suspend fun upsert(entity: Author)
@Transaction
@Query("SELECT * FROM authors")
fun findAll(): List
}
@Dao
interface BookAuthorCrossRefDao {
@Upsert
suspend fun upsert(entity: BookAuthorCrossRef)
} val db = BookDatabase.getInstance(context)
db.bookDao().apply {
upsert(Book("b1", "How to read a book"))
upsert(Book("b2", "Atomic Habits"))
}
db.authorDao().apply {
upsert(Author("a11", "Charles", "charles@gmail.com"))
upsert(Author("a12", "Mortimer", "mortimer@gmail.com"))
upsert(Author("a2", "James", "james@gmail.com"))
}
db.bookAuthorCrossRefDao().apply {
upsert(BookAuthorCrossRef("b1", "a11"))
upsert(BookAuthorCrossRef("b1", "a12"))
upsert(BookAuthorCrossRef("b2", "a2"))
}
val books = db.bookDao().findAll()
Log.d("WAYNESTALK", "books=$books")
val authors = db.authorDao().findAll()
Log.d("WAYNESTALK", "authors=$authors")Foreign Keys
Room 還允許我們定義 foreign key。下圖中,books 和 authors 是一對多的關係,其中 authors 的 bookId 為 foreign key。

在以下程式碼中,我們使用 @ForeignKey 定義 foreign key。在 entity 中指定 foreign key 對應到 Book,而在 parentColumns 中指定 foreign key 對應到 Book 的 bookId。在 childColumns 中指定 foreign key 的 field,也就是 Author::bookId。
onDelete 是指當 Author::bookId 對應的 Book 被刪除時,SQLite 要執行的動作。這裡我們指定為 ForiegnKey.CASCADE。當某本書被刪除時,擁有相同 bookId 的 Author 也都會被刪除。
@Entity("books")
data class Book(
@PrimaryKey val id: String,
val name: String,
)
@Entity(
tableName = "authors",
foreignKeys = [
ForeignKey(
entity = Book::class,
parentColumns = ["id"],
childColumns = ["bookId"],
onDelete = ForeignKey.CASCADE,
)
]
)
data class Author(
@PrimaryKey val authorId: String,
val name: String,
val email: String,
val bookId: String,
)
data class BookAndAuthors(
@Embedded val book: Book,
@Relation(
parentColumn = "bookId",
entityColumn = "bookId",
)
val authors: List,
) @Dao
interface BookDao {
@Upsert
suspend fun upsert(entity: Book)
@Transaction
@Query("SELECT * FROM books")
fun findAll(): List
@Delete
suspend fun delete(entity: Book)
}
@Dao
interface AuthorDao {
@Upsert
suspend fun upsert(entity: Author)
} val db = BookDatabase.getInstance(context)
db.bookDao().apply {
upsert(Book("b1", "How to read a book"))
upsert(Book("b2", "Atomic Habits"))
}
db.authorDao().apply {
upsert(Author("a11", "Charles", "charles@gmail.com", "b1"))
upsert(Author("a12", "Mortimer", "mortimer@gmail.com", "b1"))
upsert(Author("a2", "James", "james@gmail.com", "b2"))
}
var books = db.bookDao().findAll()
Log.d("WAYNESTALK", "books=$books")
db.bookDao().delete(Book("b1", "How to read a book"))
books = db.bookDao().findAll()
Log.d("WAYNESTALK", "books=$books")
結語
Android Room 允許我們定義物件間的關係,尤其當資料表間的關係複雜時,是非常有用的工具。它會自動連同關聯的資料一起讀取出來,而不需要我們手動寫程式在多個資料表間一一讀取。這增加了開發的速度,也降低了錯誤的發生。









