使用 Room 持續性程式庫儲存應用程式資料時,開發人員可以透過定義「資料存取物件」(或稱 DAO)與儲存的資料互動。每個 DAO 都包含許多函式,可用來提供應用程式資料庫的抽象存取權。在編譯期間,Room 會自動產生您定義的 DAO 實作成果。
透過 DAO 存取應用程式資料庫,而不使用查詢建立工具或直接查詢,可讓您維持關注點分離這項重要的架構原則。DAO 也可讓您在測試應用程式時,模擬資料庫存取程序。
DAO 剖析
您可以將每個 DAO 定義為介面或抽象類別。如果是基本用途,通常應定義為介面。不論是何種用途,您都必須使用 @Dao 為 DAO 加註。DAO 沒有屬性,但會定義一或多項函式,用來與您應用程式資料庫的資料互動。
以下程式碼為 DAO 示例,用於在 Room 資料庫中插入、刪除及選取 User 物件。
@Dao interface UserDao { @Insert suspend fun insertAll(vararg users: User) @Delete suspend fun delete(user: User) @Query("SELECT * FROM user") suspend fun getAll(): List<User> }
有兩種 DAO 函式類型可定義資料庫互動:
- 便利函式:不必編寫任何 SQL 程式碼,即可在資料庫中插入、更新及刪除資料列。
- 查詢函式:可讓您編寫自己的 SQL 查詢,以與資料庫互動。
接下來的章節會說明如何使用這兩種 DAO 函式,定義應用程式需要的資料庫互動。
便利函式
Room 提供的便利註解可用來定義函式,讓您不必編寫 SQL 陳述式即可執行插入、更新和刪除作業。
如果您需要定義更複雜的插入、更新或刪除作業,或是需要查詢資料庫中的資料,請改用查詢函式。
插入
@Insert 註解可定義將參數插入資料庫中適合資料表的方法。以下程式碼顯示有效的 @Insert 函式示例,可將一或多個 User 物件插入資料庫:
@Dao interface UserDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertUsers(vararg users: User) @Insert suspend fun insertBothUsers(user1: User, user2: User) @Insert suspend fun insertUsersAndFriends(user: User, friends: List<User>) }
@Insert 函式的每項參數都必須是含有 @Entity 註解的 Room 資料實體類別例項,或是資料實體類別例項的集合。呼叫 @Insert 函式時,Room 會將每個傳遞的實體例項插入對應的資料庫資料表。
如果 @Insert 函式接收單一參數,會傳回 Long 值,即插入項目的新 rowId。如果參數是陣列或集合,則應改為傳回 Long 值的陣列或集合,且每個值皆做為插入項目的 rowId。如要進一步瞭解如何傳回 rowId 值,請參閱 @Insert 註解的說明文件,以及 rowId 資料表的 SQLite 說明文件。
更新
@Update 註解可定義在資料庫資料表中更新特定資料列的函式。與 @Insert 函式類似,@Update 函式接受以資料實體例項做為參數。以下程式碼顯示 @Update 函式示例,嘗試更新資料庫中的一或多個 User 物件:
@Dao interface UserDao { @Update suspend fun updateUsers(vararg users: User) }
Room 會使用主鍵,將引數中的實體例項與資料庫中的資料列相互比對。如果資料列沒有相同主鍵,Room 就不會進行任何變更。
@Update 函式可以選擇性傳回 Int 值,指出已成功更新的資料列數量。
刪除
@Delete 註解可定義從資料庫資料表中刪除特定資料列的函式。與 @Insert 函式類似,@Delete 函式接受以資料實體例項做為參數。以下程式碼顯示 @Delete 函式示例,嘗試從資料庫中刪除一或多個 User 物件:
@Dao interface UserDao { @Delete suspend fun deleteUsers(vararg users: User) }
Room 會使用主鍵,將引數中的實體例項與資料庫中的資料列相互比對。如果資料列沒有相同主鍵,Room 就不會進行任何變更。
@Delete 函式可選擇性傳回 Int 值,指出已成功刪除的資料列數量。
新增或更新
@Upsert 註解可讓您定義函式,在沒有相符資料列時插入實體例項,或在已有相同主鍵的資料列時更新實體例項。
與 @Insert 和 @Update 函式類似,@Upsert 函式接受以資料實體例項做為參數。以下程式碼顯示 @Upsert 函式示例,嘗試在資料庫中 upsert 一或多個 User 物件:
@Dao interface UserDao { @Upsert suspend fun upsertUsers(vararg users: User) }
如果 @Upsert 函式接收單一參數,會傳回 Long 值。如果因此插入新資料列,則會傳回新插入資料列的 rowId。如果導致現有資料列更新,則會傳回 -1。如果參數是陣列或集合,則應改為傳回 Long 值的陣列或集合。
查詢函式
@Query 註解可讓您編寫 SQL 陳述式,並將其公開為 DAO 函式。您可以使用這些查詢函式查詢應用程式資料庫的資料,或視需求執行較複雜的插入、更新及刪除作業。
Room 會在編譯期間驗證 SQL 查詢。這表示若查詢有問題,會發生編譯錯誤,而非執行階段失敗。
簡易查詢
以下程式碼定義的函式會使用 SELECT 查詢,傳回資料庫中所有的 User 物件:
@Query("SELECT * FROM user") suspend fun loadAllUsers(): List<User>
以下段落示範如何針對常見用途修改此示例。
回傳表格欄位的子集
在多數情況下,您只需傳回要查詢的資料表中的部分資料欄。例如,您的 UI 可能只顯示使用者的姓名,而非該使用者的所有詳細資料。為節省資源並簡化查詢執行作業,建議您只查詢所需屬性。
只要您能夠將結果資料欄的組合對應至傳回的物件,即可使用 Room 從任何查詢傳回資料物件。例如,您可以定義下列物件來保留使用者的姓名:
data class NameTuple( @ColumnInfo(name = "first_name") val firstName: String, @ColumnInfo(name = "last_name") val lastName: String )
接著,您可以使用查詢函式傳回該資料物件:
@Query("SELECT first_name, last_name FROM user") suspend fun loadFullName(): List<NameTuple>
由於查詢會傳回 first_name 和 last_name 資料欄的值,因此 Room 會將這些值對應至 NameTuple 類別中的屬性。如果查詢傳回的資料欄無法對應傳回物件中的屬性,Room 會顯示警示。
雖然上一個範例使用自訂資料類別擷取部分資料欄,但如果查詢傳回的資料欄正好是兩或三欄,Room 也支援傳回 kotlin.Pair 和 kotlin.Triple,方便您使用。使用這些型別時,系統會依據查詢陳述式中定義的順序對應資料欄,因此 SELECT 陳述式中的資料欄順序必須與 Pair 或 Triple 中的型別順序相符。
將簡易參數傳送至查詢
大多數情況下,DAO 函式必須接受參數,以便執行篩選作業。Room 可讓您在查詢中使用函式參數做為繫結參數。
舉例來說,以下程式碼定義的函式會傳回特定年齡以上的所有使用者:
@Query("SELECT * FROM user WHERE age > :minAge") suspend fun loadAllUsersOlderThan(minAge: Int): Array<User>
您也可以在同一個查詢中傳遞多個參數,或是多次參照相同的參數,如以下程式碼所示:
@Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge") suspend fun loadAllUsersBetweenAges(minAge: Int, maxAge: Int): Array<User> @Query( """ SELECT * FROM user WHERE first_name LIKE :search OR last_name LIKE :search """ ) suspend fun findUserWithName(search: String): List<User>
將一系列參數傳送至查詢
部分 DAO 函式可能會要求您傳入數量不定的參數,確切數量要到執行階段才知道。如果參數代表集合,系統會在執行階段依據值的數量自動擴展。
舉例來說,以下程式碼定義的函式會傳回部分地區所有使用者的相關資訊:
@Query("SELECT * FROM user WHERE region IN (:regions)") suspend fun loadUsersFromRegions(regions: List<String>): List<User>
查詢多份表格
有些查詢可能需要存取多份資料表才能計算結果。您可以在 SQL 查詢中使用 JOIN 子句來參照多份資料表。
以下程式碼定義的函式會結合三份資料表,將目前外借中的書籍資訊傳回給特定使用者:
@Query( """ SELECT * FROM book INNER JOIN loan ON loan.book_id = book.id INNER JOIN user ON user.id = loan.user_id WHERE user.name LIKE :userName """ ) suspend fun findBooksBorrowedByName(userName: String): List<Book>
您也可以定義資料物件,從多個彙整的資料表中傳回資料欄子集。詳情請參閱傳回資料表中的部分資料欄。 以下程式碼定義了 DAO,其函式會傳回使用者的名稱,以及他們借閱的書籍名稱:
interface UserBookDao { @Query( """ SELECT user.name AS userName, book.name AS bookName FROM user, book WHERE user.id = book.user_id """ ) fun loadUserAndBookNames(): Flow<List<UserBook>> } data class UserBook(val userName: String, val bookName: String)
回傳多重映射
如要執行聯結作業,您也可以編寫傳回多重對應的查詢函式,查詢多個資料表的資料欄,不必定義其他資料類別。
請參閱「查詢多份資料表」一節中的範例。您可以直接透過查詢函式傳回 User 和 Book 的對應內容,不必傳回保留 User 和 Book 例項配對的自訂資料類別的例項清單:
@Query( """ SELECT * FROM user JOIN book ON user.id = book.user_id """ ) suspend fun loadUserAndBookNames(): Map<User, List<Book>>
當查詢函式傳回多重對應時,您可以編寫使用 GROUP BY 子句的查詢,以便利用 SQL 功能進行進階計算和篩選。舉例來說,您可以修改 loadUserAndBookNames 函式,只傳回借閱三本以上書籍的使用者:
@Query( """ SELECT * FROM user JOIN book ON user.id = book.user_id GROUP BY user.name HAVING COUNT(book.id) >= 3 """ ) suspend fun loadUserAndBookNamesGrouped(): Map<User, List<Book>>
如果您不需要對應所有物件,也可以使用回傳型別泛型參數的 @MapColumn 註解,傳回查詢中特定資料欄間的對應。
@Query( """ SELECT user.name AS username, book.name AS bookname FROM user JOIN book ON user.id = book.user_id """ ) suspend fun loadUserAndBookNamesColumns(): Map< @MapColumn(columnName = "username") String, List<@MapColumn(columnName = "bookname") String> >
特殊回傳類型
Room 提供許多特殊傳回類型,用來與其他 API 程式庫整合。
使用分頁程式庫進行分頁查詢
Room 藉由與 Paging 程式庫整合來支援分頁查詢功能。如要使用 Paging 3 傳回類型,您必須在資料庫或 DAO 中註冊 Paging 傳回類型轉換器:
- 在建構設定中加入
androidx.room3:room3-paging構件。 - 使用
@DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class)為@Database或@Dao宣告加上註解。
註冊後,您的 DAO 即可傳回 PagingSource 物件,以便與 Paging 3 搭配使用:
@Dao @DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class) interface UserDao { @Query("SELECT * FROM users WHERE label LIKE :query") fun pagingSource(query: String): PagingSource<Int, User> }
如要進一步瞭解如何為 PagingSource 選擇類型參數,請參閱「選取鍵和值類型」。
直接連線至資料庫
如果應用程式的邏輯需要直接存取資料庫連線的低層級資訊,可以改用 Room 的連線 API。您可以透過 RoomDatabase 執行個體上的唯讀作業使用 useReaderConnection 取得連線,或透過寫入作業使用 useWriterConnection 取得連線,並使用 usePrepared 執行陳述式:
val result: List<Pair<Long, String>> = roomDatabase.useReaderConnection { connection -> connection.usePrepared( "SELECT * FROM user WHERE age > :minAge LIMIT 5" ) { stmt -> // Bind arguments if needed stmt.bindLong(1, minAge.toLong()) buildList { // Step through the results while (stmt.step()) { add(stmt.getLong(0) to stmt.getText(1)) } } } }
如需直接在連線上執行低階資料庫交易,可以在 useWriterConnection 區塊內的 Transactor 例項上使用 immediateTransaction、deferredTransaction 或 exclusiveTransaction 輔助函式:
roomDatabase.useWriterConnection { transactor -> transactor.immediateTransaction { // Perform transactional database operations using transactor } }
或者,如果您只需要在交易中執行高階 DAO 作業,請在 RoomDatabase 執行個體上使用 withReadTransaction 或 withWriteTransaction 輔助擴充功能函式:
// Perform transactional read operations (DEFERRED transaction) val userCount = roomDatabase.withReadTransaction { userDao.countUsers() } // Perform transactional write operations (IMMEDIATE transaction) roomDatabase.withWriteTransaction { userDao.insert(newUser) userDao.update(existingUser) }