在使用資料庫時,我們查詢的資料可能會關聯到多個資料表。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<List<Book>> }
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<List<BookAndAuthor>> } @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<Author>。
@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<Author>, )
@Dao interface BookDao { @Upsert suspend fun upsert(entity: Book) @Transaction @Query("SELECT * FROM books") fun findAll(): Flow<List<BookAndAuthors>> } @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<Author>, ) data class AuthorAndBooks( @Embedded val author: Author, @Relation( parentColumn = "authorId", entityColumn = "bookId", associateBy = Junction(BookAuthorCrossRef::class), ) val books: List<Book>, )
@Dao interface BookDao { @Upsert suspend fun upsert(entity: Book) @Transaction @Query("SELECT * FROM books") fun findAll(): List<BookAndAuthors> } @Dao interface AuthorDao { @Upsert suspend fun upsert(entity: Author) @Transaction @Query("SELECT * FROM authors") fun findAll(): List<AuthorAndBooks> } @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<Author>, )
@Dao interface BookDao { @Upsert suspend fun upsert(entity: Book) @Transaction @Query("SELECT * FROM books") fun findAll(): List<BookAndAuthors> @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 允許我們定義物件間的關係,尤其當資料表間的關係複雜時,是非常有用的工具。它會自動連同關聯的資料一起讀取出來,而不需要我們手動寫程式在多個資料表間一一讀取。這增加了開發的速度,也降低了錯誤的發生。