使用 Room DAO 存取資料

透過集合功能整理內容 你可以依據偏好儲存及分類內容。

使用 Room 永久性程式庫來儲存應用程式資料時,開發人員可以透過定義資料存取物件 (DAO),進而與儲存的資料互動。每個 DAO 都提供許多方法,以抽象存取應用程式資料庫。在編譯期間,Room 會自動以開發人員定義的 DAO 進行實作。

透過 DAO 存取應用程式資料庫,而非查詢建構工具或直接查詢,開發人員就能維持關注點分離 (SoC)原則,這是非常重要的架構原則。DAO 也可讓測試應用程式時,更輕鬆地模擬存取資料庫。

DAO 剖析

每個 DAO 都可以定義為介面或抽象類別。如果是基本功能,通常會定義為介面。無論採取何種定義,一律必須使用 @Dao 註解 DAO。DAO 本身沒有屬性,但在與應用程式資料庫互動方面,提供了一種或多種方法。

如要瞭解如何在 Room 資料庫中定義插入、刪除及選取 User 物件的方法,請見以下簡單的 DAO 程式碼範例:

Kotlin

@Dao
interface UserDao {
    @Insert
    fun insertAll(vararg users: User)

    @Delete
    fun delete(user: User)

    @Query("SELECT * FROM user")
    fun getAll(): List<User>
}

Java

@Dao
public interface UserDao {
    @Insert
    void insertAll(User... users);

    @Delete
    void delete(User user);

    @Query("SELECT * FROM user")
    List<User> getAll();
}

DAO 有有兩種方法用來定義資料庫互動:

  • 可以不用寫入任何 SQL 程式碼,即能輕鬆插入、更新及刪除資料庫中的資料列的「便利方法」。
  • 開發人員撰寫自己的 SQL 查詢來與資料庫互動的「查詢方法」。

接下來說明如何使用這兩種 DAO 方法,以定義應用程式所需的資料庫互動。

便利方法

Room 的註解很方便,適合定義執行插入、更新和刪除的簡易方法,而且無須編寫 SQL 陳述式。

如果開發人員需要定義更複雜的方法以進行插入、更新或刪除,或是需要在資料庫中查詢資料,請改用「查詢方法」

插入

開發人員可使用 @Insert 註解來定義方法,將參數插入資料庫中適合的資料表。要瞭解如何使用有效的 @Insert 方法,將一或多個 User 物件插入到資料庫中,請參閱以下程式碼範例:

Kotlin

@Dao
interface UserDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertUsers(vararg users: User)

    @Insert
    fun insertBothUsers(user1: User, user2: User)

    @Insert
    fun insertUsersAndFriends(user: User, friends: List<User>)
}

Java

@Dao
public interface UserDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    public void insertUsers(User... users);

    @Insert
    public void insertBothUsers(User user1, User user2);

    @Insert
    public void insertUsersAndFriends(User user, List<User> friends);
}

@Insert 方法的每項參數都必須是 Room 資料實體類別的執行個體 (須註解 @Entity),或是資料實體執行個體的集合。呼叫 @Insert 方法時,Room 會將所有傳遞的實體執行個體,插入對應的資料庫資料表。

@Insert 方法收到單一參數,就會傳回 long 值,即插入項目的新 rowId。如果參數是陣列或集合,則方法也應傳回陣列,或是以 long 為值的集合,而每個值都是插入項目的 rowId。如要進一步瞭解如何傳回 rowId 值,請參閱 @Insert 註解的參考說明文件,以及針對 rowId 資料表的 SQLite 說明文件

更新

@Update 註解可定義從資料庫資料表中,更新特定資料列的方法。與 @Insert 方法類似,@Update 方法使用資料實體執行個體做為參數。如要使用 @Update 方法,以更新資料庫中的一或多個 User 物件,請參閱以下程式碼範例:

Kotlin

@Dao
interface UserDao {
    @Update
    fun updateUsers(vararg users: User)
}

Java

@Dao
public interface UserDao {
    @Update
    public void updateUsers(User... users);
}

Room 會使用主鍵,以比對傳送的實體執行個體和資料庫中的資料列。如果資料列沒有相同主鍵,Room 就不會進行任何變更。

如有需要,可使用 @Update 方法傳回 int 值,以得知成功更新的資料列數量。

刪除

@Delete 註解可定義從資料庫資料表中,刪除特定資料列的方法。與 @Insert 方法類似,@Delete 方法使用資料實體執行個體做為參數。如要使用 @Delete 方法,從資料庫中刪除一或多個 User 物件,請參閱以下程式碼範例:

Kotlin

@Dao
interface UserDao {
    @Delete
    fun deleteUsers(vararg users: User)
}

Java

@Dao
public interface UserDao {
    @Delete
    public void deleteUsers(User... users);
}

Room 會使用主鍵,以比對傳送的實體執行個體和資料庫中的資料列。如果資料列沒有相同主鍵,Room 就不會進行任何變更。

如有需要,可使用 @Delete 方法傳回 int 值,以得知成功刪除的資料列數量。

查詢方法

@Query 註解可將開發人員編寫的 SQL 陳述式,顯示為 DAO 方法。開發人員可以使用這些查詢方法,查詢應用程式資料庫中的資料,或執行較複雜的資料插入、更新和刪除。

在編譯期間,Room 會驗證 SQL 查詢。也就是說,如果查詢有問題,就會發生編譯錯誤,而非執行階段失敗。

簡易查詢

如要使用簡易 SELECT 查詢,以傳回資料庫所有 User 物件,詳情請參閱以下程式碼範例:

Kotlin

@Query("SELECT * FROM user")
fun loadAllUsers(): Array<User>

Java

@Query("SELECT * FROM user")
public User[] loadAllUsers();

如要針對一般用途修改此範例,請見以下說明。

回傳部分資料欄

大多數情況下,開發人員只需查詢的資料表傳回一小部分資料欄。例如,您的使用者介面只顯示使用者的名字和姓氏,不需要該使用者的所有資料。為節省資源並簡化查詢,您只需要查詢所需欄位即可。

只要開發人員能夠將結果資料欄集合對應至傳回的物件,Room 可讓任何查詢傳回簡易物件。比方說,開發人員可以定義以下物件,來儲存使用者的名字和姓氏,請參閱下列程式碼範例:

Kotlin

data class NameTuple(
    @ColumnInfo(name = "first_name") val firstName: String?,
    @ColumnInfo(name = "last_name") val lastName: String?
)

Java

public class NameTuple {
    @ColumnInfo(name = "first_name")
    public String firstName;

    @ColumnInfo(name = "last_name")
    @NonNull
    public String lastName;
}

然後,開發人員可以用查詢方法傳回該簡易物件:

Kotlin

@Query("SELECT first_name, last_name FROM user")
fun loadFullName(): List<NameTuple>

Java

@Query("SELECT first_name, last_name FROM user")
public List<NameTuple> loadFullName();

Room 清楚查詢會傳回 first_namelast_name 資料欄的值,而且這些值可以對應 NameTuple 類別中的欄位。要是查詢傳回的資料欄無法對應傳回物件中的欄位,Room 就會顯示警告。

將簡易參數傳送至查詢

大多數情況下,DAO 方法必須使用參數,才能執行篩選作業。Room 可以讓開發人員在查詢中,使用方法參數做為繫結參數。

以傳回超過特定年齡的所有使用者為例,請參閱以下程式碼範例說明:

Kotlin

@Query("SELECT * FROM user WHERE age > :minAge")
fun loadAllUsersOlderThan(minAge: Int): Array<User>

Java

@Query("SELECT * FROM user WHERE age > :minAge")
public User[] loadAllUsersOlderThan(int minAge);

開發人員也可以傳遞多個參數,或在單次查詢中多次參照同一個參數,詳情請參閱以下程式碼範例:

Kotlin

@Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
fun loadAllUsersBetweenAges(minAge: Int, maxAge: Int): Array<User>

@Query("SELECT * FROM user WHERE first_name LIKE :search " +
       "OR last_name LIKE :search")
fun findUserWithName(search: String): List<User>

Java

@Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
public User[] loadAllUsersBetweenAges(int minAge, int maxAge);

@Query("SELECT * FROM user WHERE first_name LIKE :search " +
       "OR last_name LIKE :search")
public List<User> findUserWithName(String search);

傳送一系列參數至查詢

開發人員有部分 DAO 方法可能需要傳遞大量參數,然而卻無法在執行前確認具體數量。Room 瞭解一個參數代表一個集合,所以在執行階段時,它會根據提供的參數數量自動擴展。

如要傳回特定區域所有使用者的資訊,請參閱以下程式碼範例所定義的方法:

Kotlin

@Query("SELECT * FROM user WHERE region IN (:regions)")
fun loadUsersFromRegions(regions: List<String>): List<User>

Java

@Query("SELECT * FROM user WHERE region IN (:regions)")
public List<User> loadUsersFromRegions(List<String> regions);

查詢多份資料表

有部分查詢是需要存取多份資料表,才能取得結果。開發人員可以在 SQL 查詢中,使用 JOIN 子句來參照多份資料表。

以下程式碼說明如何結合三份資料表,以便將目前出租中的書籍資料傳回給特定使用者:

Kotlin

@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"
)
fun findBooksBorrowedByNameSync(userName: String): List<Book>

Java

@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")
public List<Book> findBooksBorrowedByNameSync(String userName);

開發人員也可以定義簡易物件,從多份彙整資料表傳回部分資料欄,請參閱「傳回資料表的部分資料欄」一節的說明。以下程式碼範例說明如何使用方法定義 DAO,以傳回使用者姓名和他們借閱的書籍名稱:

Kotlin

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>>

    // You can also define this class in a separate file.
    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();

   // You can also define this class in a separate file, as long as you add the
   // "public" access modifier.
   static class UserBook {
       public String userName;
       public String bookName;
   }
}

回傳多重映射

在 Room 2.4 及以上的版本中,開發人員無須額外定義資料類別,就可以查詢多份資料表的資料欄,只要編寫能傳回多重映射的查詢方法即可。

詳情請參閱「查詢多份資料表」一節中的範例。開發人員可以使用自己的查詢方法,直接傳回對應的 UserBook,而無須傳回含有 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();

當您的查詢方法傳回多重映射時,可以使用 GROUP BY 子句編寫查詢,以便利用 SQL 功能進行進階計算和篩選。比方說,開發人員可以修改 loadUserAndBookNames() 方法,只傳回還書有三本以上的使用者:

Kotlin

@Query(
    "SELECT * FROM user" +
    "JOIN book ON user.id = book.user_id" +
    "GROUP BY user.name WHERE COUNT(book.id) >= 3"
)
fun loadUserAndBookNames(): Map<User, List<Book>>

Java

@Query(
    "SELECT * FROM user" +
    "JOIN book ON user.id = book.user_id" +
    "GROUP BY user.name WHERE COUNT(book.id) >= 3"
)
public Map<User, List<Book>> loadUserAndBookNames();

如果開發人員不需要對應全部物件,就可以進一步設定查詢中特定資料欄之間的對應,只要在查詢方法中的 @MapInfo 註解設定 keyColumnvalueColumn 屬性即可:

Kotlin

@MapInfo(keyColumn = "userName", valueColumn = "bookName")
@Query(
    "SELECT user.name AS username, book.name AS bookname FROM user" +
    "JOIN book ON user.id = book.user_id"
)
fun loadUserAndBookNames(): Map<String, List<String>>

Java

@MapInfo(keyColumn = "userName", valueColumn = "bookName")
@Query(
    "SELECT user.name AS username, book.name AS bookname FROM user" +
    "JOIN book ON user.id = book.user_id"
)
public Map<String, List<String>> loadUserAndBookNames();

特殊回傳類型

Room 提供特殊回傳類型,能與其他 API 程式庫整合。

使用分頁程式庫查詢分頁

Room 透過整合分頁程式庫來提供分頁查詢功能。在 Room 2.3.0-alpha01 及更高版本中,DAO 會傳回 PagingSource 物件,以便用於 Paging 3

Kotlin

@Dao
interface UserDao {
  @Query("SELECT * FROM users WHERE label LIKE :query")
  fun pagingSource(query: String): PagingSource<Int, User>
}

Java

@Dao
interface UserDao {
  @Query("SELECT * FROM users WHERE label LIKE :query")
  PagingSource<Integer, User> pagingSource(String query);
}

如要進一步瞭解如何為 PagingSource 選擇類型參數,請參閱「選取鍵值類型」一節。

直接定位游標權限

如果應用程式的邏輯需要直接存取傳回資料列,開發人員可以編寫 DAO 方法,以傳回 Cursor 物件,詳情請參閱以下範例:

Kotlin

@Dao
interface UserDao {
    @Query("SELECT * FROM user WHERE age > :minAge LIMIT 5")
    fun loadRawUsersOlderThan(minAge: Int): Cursor
}

Java

@Dao
public interface UserDao {
    @Query("SELECT * FROM user WHERE age > :minAge LIMIT 5")
    public Cursor loadRawUsersOlderThan(int minAge);
}

其他資源

如要進一步瞭解如何使用 Room DAO 存取資料,請參閱以下資源:

範例

程式碼研究室