Truy cập vào dữ liệu bằng các đối tượng truy cập dữ liệu (DAO) của Room

Khi sử dụng thư viện Room về dữ liệu cố định để lưu trữ dữ liệu của ứng dụng, bạn sẽ tương tác với dữ liệu được lưu trữ bằng cách xác định các đối tượng truy cập dữ liệu hay còn gọi là DAO. Mỗi DAO có các phương thức cung cấp quyền truy cập trừu tượng vào cơ sở dữ liệu của ứng dụng. Vào thời gian biên dịch, Room sẽ tự động tạo hoạt động triển khai các DAO mà bạn xác định.

Bằng cách sử dụng DAO để truy cập cơ sở dữ liệu của ứng dụng thay vì các trình tạo truy vấn hoặc truy vấn trực tiếp, bạn có thể duy trì sự tách biệt các mối quan ngại – một nguyên tắc kiến trúc quan trọng. Các DAO cũng giúp bạn dễ dàng mô phỏng quyền truy cập cơ sở dữ liệu khi kiểm thử ứng dụng của bạn.

Phân tích thành phần của DAO

Bạn có thể xác định mỗi DAO là giao diện hoặc lớp trừu tượng. Đối với các trường hợp sử dụng cơ bản, bạn thường sử dụng giao diện. Trong cả hai trường hợp, bạn phải luôn chú giải các DAO của mình bằng @Dao. Các DAO không có thuộc tính nhưng lại xác định một hoặc nhiều phương thức tương tác với dữ liệu trong cơ sở dữ liệu của ứng dụng.

Mã sau đây là ví dụ về một DAO đơn giản xác định các phương thức chèn, xoá và chọn đối tượng User trong cơ sở dữ liệu Room:

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();
}

Có hai loại phương thức DAO để xác định các hoạt động tương tác với cơ sở dữ liệu:

  • Các phương thức thuận tiện cho phép bạn chèn, cập nhật và xoá hàng trong cơ sở dữ liệu mà không cần ghi bất kỳ mã SQL nào.
  • Các phương thức truy vấn cho phép bạn ghi truy vấn SQL của riêng bạn để tương tác với cơ sở dữ liệu.

Các mục sau đây minh hoạ cách sử dụng cả hai loại phương thức DAO để xác định các hoạt động tương tác với cơ sở dữ liệu mà ứng dụng của bạn cần.

Phương thức thuận tiện

Room cung cấp các chú giải thuận tiện để xác định các phương thức thực hiện thao tác chèn, cập nhật và xoá đơn giản mà không yêu cầu bạn ghi câu lệnh SQL.

Nếu bạn cần xác định các thao tác chèn, cập nhật hoặc xoá phức tạp hơn hoặc cần truy vấn dữ liệu trong cơ sở dữ liệu, hãy sử dụng phương thức truy vấn.

Chèn

Chú giải @Insert cho phép bạn xác định các phương thức có thể chèn tham số của chúng vào bảng thích hợp trong cơ sở dữ liệu. Đoạn mã sau đây cho thấy ví dụ về các phương thức @Insert hợp lệ có tác dụng chèn một hoặc nhiều đối tượng User vào cơ sở dữ liệu:

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);
}

Mỗi tham số cho một phương thức @Insert phải là một phiên bản của lớp thực thể dữ liệu Room được chú giải bằng @Entity hoặc một tập hợp các phiên bản lớp thực thể dữ liệu, mỗi phiên bản trỏ đến một cơ sở dữ liệu. Khi một phương thức @Insert được gọi, Room sẽ chèn từng phiên bản thực thể đã truyền vào bảng cơ sở dữ liệu tương ứng.

Nếu nhận được một tham số duy nhất, thì phương thức @Insert có thể trả về một giá trị long (đây là giá trị rowId mới cho mục được chèn). Nếu tham số là một mảng hoặc một tập hợp, thì phương thức sẽ trả về một mảng hoặc một tập hợp các giá trị long thay thế, với mỗi giá trị là rowId cho một trong các mục được chèn. Để tìm hiểu thêm về việc trả về các giá trị rowId, hãy xem tài liệu tham khảo cho chú giải @InsertTài liệu SQLite cho bảng rowid.

Cập nhật

Chú giải @Update cho phép bạn xác định các phương thức cập nhật những hàng cụ thể trong bảng cơ sở dữ liệu. Tương tự như phương thức @Insert, phương thức @Update chấp nhận các phiên bản thực thể dữ liệu dưới dạng tham số. Đoạn mã sau đây cho thấy ví dụ về phương thức @Update cố gắng cập nhật một hoặc nhiều đối tượng User trong cơ sở dữ liệu:

Kotlin

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

Java

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

Room sử dụng khoá chính để so khớp các phiên bản thực thể đã truyền với các hàng trong cơ sở dữ liệu. Nếu không có hàng nào có cùng một khoá chính, Room sẽ không thực hiện thay đổi.

Phương thức @Update có thể tuỳ ý trả về một giá trị int cho biết số lượng hàng đã được cập nhật thành công.

Xoá

Chú giải @Delete cho phép bạn xác định các phương thức xoá những hàng cụ thể khỏi bảng cơ sở dữ liệu. Tương tự như phương thức @Insert, phương thức @Delete chấp nhận các phiên bản thực thể dữ liệu dưới dạng tham số. Đoạn mã sau đây cho thấy ví dụ về phương thức @Delete cố gắng xoá một hoặc nhiều đối tượng User khỏi cơ sở dữ liệu:

Kotlin

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

Java

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

Room sử dụng khoá chính để so khớp các phiên bản thực thể đã truyền với các hàng trong cơ sở dữ liệu. Nếu không có hàng nào có cùng một khoá chính, Room sẽ không thực hiện thay đổi.

Phương thức @Delete có thể tuỳ ý trả về một giá trị int cho biết số lượng hàng đã được xoá thành công.

Phương thức truy vấn

Chú giải @Query cho phép bạn ghi các câu lệnh SQL và hiển thị chúng dưới dạng các phương thức DAO. Hãy sử dụng các phương thức truy vấn này để truy vấn dữ liệu từ cơ sở dữ liệu của ứng dụng hoặc khi bạn cần thực hiện các thao tác chèn, cập nhật và xoá phức tạp hơn.

Room xác thực các truy vấn SQL vào thời gian biên dịch. Nói cách khác, nếu có vấn đề xảy ra với truy vấn của bạn, sẽ có lỗi biên dịch xảy ra thay vì lỗi thời gian chạy.

Truy vấn đơn giản

Mã sau đây xác định phương thức sử dụng một truy vấn SELECT đơn giản để trả về tất cả các đối tượng User trong cơ sở dữ liệu:

Kotlin

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

Java

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

Các mục sau đây minh hoạ cách sửa đổi ví dụ này cho các trường hợp sử dụng thông thường.

Trả về một tập hợp nhỏ các cột của bảng

Trong hầu hết trường hợp, bạn chỉ cần trả về một tập hợp nhỏ các cột từ bảng mà bạn đang truy vấn. Ví dụ: Giao diện người dùng có thể chỉ hiển thị họ và tên của người dùng thay vì mọi thông tin chi tiết về người dùng đó. Để tiết kiệm tài nguyên và đơn giản hoá quá trình thực thi truy vấn, hãy chỉ truy vấn các trường mà bạn cần.

Room cho phép bạn trả về một đối tượng đơn giản từ bất kỳ truy vấn nào, miễn là bạn có thể liên kết tập hợp các cột kết quả đến đối tượng được trả về. Ví dụ: Bạn có thể xác định đối tượng sau để giữ họ và tên của người dùng:

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

Sau đó, bạn có thể trả về đối tượng đơn giản đó từ phương thức truy vấn:

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 hiểu rằng truy vấn trả về các giá trị cho cột first_namelast_name. Các giá trị này có thể được liên kết đến các trường trong lớp NameTuple. Nếu truy vấn trả về một cột không liên kết đến trường trong đối tượng được trả về, Room sẽ hiển thị cảnh báo.

Truyền các tham số đơn giản tới truy vấn

Trong hầu hết các trường hợp, các phương thức DAO của bạn cần chấp nhận các tham số để có thể thực hiện các thao tác lọc. Room hỗ trợ sử dụng các tham số phương thức làm tham số liên kết trong các truy vấn của bạn.

Ví dụ: Mã sau đây xác định một phương thức trả về tất cả người dùng trên một độ tuổi nhất định:

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

Bạn cũng có thể truyền nhiều tham số hoặc tham chiếu cùng một tham số nhiều lần trong một truy vấn, như được minh hoạ trong mã sau:

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

Truyền một tập hợp các tham số tới truy vấn

Một số phương thức DAO của bạn có thể yêu cầu bạn truyền số lượng tham số khác nhau chưa xác định cho đến thời gian chạy. Room hiểu khi một tham số đại diện cho một tập hợp và tự động mở rộng tập hợp đó trong thời gian chạy dựa trên số lượng tham số được cung cấp.

Ví dụ: Mã sau đây xác định một phương thức trả về thông tin liên quan đến tất cả người dùng từ một tập con các khu vực:

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

Truy vấn nhiều bảng

Một số truy vấn của bạn có thể yêu cầu quyền truy cập vào nhiều bảng để tính toán kết quả. Bạn có thể sử dụng mệnh đề JOIN trong các truy vấn SQL để tham chiếu nhiều bảng.

Mã sau đây xác định một phương thức kết hợp ba bảng với nhau để trả về kết quả các sách hiện đang cho một người dùng cụ thể mượn:

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

Bạn cũng có thể xác định các đối tượng đơn giản để trả về một tập con các cột từ nhiều bảng đã kết hợp như được thảo luận trong phần Trả về một tập con các cột của bảng. Mã sau đây xác định một DAO bằng phương thức trả về tên người dùng và tên sách mà họ đã mượn:

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

Trả về kiểu cấu trúc đa ánh xạ (multimap)

Trong Room 2.4 trở lên, bạn cũng có thể truy vấn các cột từ nhiều bảng mà không cần xác định thêm lớp dữ liệu bằng cách ghi các phương thức truy vấn trả về đa ánh xạ.

Xem ví dụ trong phần Truy vấn nhiều bảng. Thay vì trả về danh sách các phiên bản của một lớp dữ liệu tuỳ chỉnh chứa cặp phiên bản UserBook, bạn có thể trực tiếp trả về một bản ánh xạ của UserBook từ phương thức truy vấn của mình:

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();

Khi phương thức truy vấn của bạn trả về đa ánh xạ, bạn có thể ghi các truy vấn sử dụng mệnh đề GROUP BY, cho phép bạn tận dụng các khả năng của SQL để tính toán và lọc nâng cao. Ví dụ: Bạn có thể sửa đổi phương thức loadUserAndBookNames() để chỉ trả về những người dùng đã mượn từ ba cuốn sách trở lên:

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();

Nếu không cần ánh xạ toàn bộ các đối tượng, bạn cũng có thể trả về các bản ánh xạ giữa các cột cụ thể trong truy vấn bằng cách đặt thuộc tính keyColumnvalueColumn trong chú giải @MapInfo ở phương thức truy vấn của bạn:

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();

Loại dữ liệu trả về đặc biệt

Room cung cấp một số loại dữ liệu trả về đặc biệt để tích hợp với các thư viện API khác.

Truy vấn được phân trang bằng thư viện Paging

Room hỗ trợ các truy vấn phân trang thông qua việc tích hợp với thư viện Paging. Trong Room 2.3.0-alpha01 trở lên, DAO có thể trả về các đối tượng PagingSource để sử dụng cùng với 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);
}

Để biết thêm thông tin về cách chọn tham số loại cho PagingSource, xin xem phần Chọn loại khoá và giá trị.

Quyền truy cập con trỏ trực tiếp

Nếu logic của ứng dụng yêu cầu quyền truy cập trực tiếp vào các hàng trả về, bạn có thể ghi các phương thức DAO để trả về đối tượng Cursor như minh hoạ trong ví dụ sau đây:

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);
}

Tài nguyên khác

Để tìm hiểu thêm về cách truy cập dữ liệu bằng DAO của Room, xem các tài nguyên bổ sung sau:

Mẫu

Lớp học lập trình