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 hàm 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. DAO cũng cho phép bạn 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 hàm để 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 xác định các hàm để chèn, xoá và chọn các đối tượng User trong cơ sở dữ liệu Room:
@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> }
Có hai loại hàm DAO xác định các hoạt động tương tác với cơ sở dữ liệu:
- Các hàm 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 hàm truy vấn cho phép bạn viết truy vấn SQL của riêng mình để 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 hàm 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.
Hàm tiện ích
Room cung cấp các chú giải thuận tiện để xác định các hàm thực hiện thao tác chèn, cập nhật và xoá mà không yêu cầu bạn viết 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 hàm truy vấn.
Insert
Chú giải @Insert cho phép bạn xác định các hàm 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 hàm @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:
@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>) }
Mỗi tham số cho một hàm @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. Khi một hàm @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ì hàm @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 này 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 @Insert và Tà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 hàm cập nhật những hàng cụ thể trong bảng cơ sở dữ liệu. Tương tự như các hàm @Insert, các hàm @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ề một hàm @Update cố gắng cập nhật một hoặc nhiều đối tượng User trong cơ sở dữ liệu:
@Dao interface UserDao { @Update suspend fun updateUsers(vararg users: User) }
Room sử dụng khoá chính để so khớp các thực thể trong đối số với các hàng trong cơ sở dữ liệu. Nếu không có hàng nào có cùng khoá chính, Room sẽ không thực hiện thay đổi.
Hàm @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.
Xóa
Chú giải @Delete cho phép bạn xác định các hàm xoá những hàng cụ thể khỏi bảng cơ sở dữ liệu. Tương tự như hàm @Insert, hàm @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ề một hàm @Delete cố gắng xoá một hoặc nhiều đối tượng User khỏi cơ sở dữ liệu:
@Dao interface UserDao { @Delete suspend fun deleteUsers(vararg users: User) }
Room sử dụng khoá chính để so khớp các thực thể trong đối số với các hàng trong cơ sở dữ liệu. Nếu không có hàng nào có cùng khoá chính, Room sẽ không thực hiện thay đổi.
Hàm @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.
Upsert
Chú giải @Upsert cho phép bạn xác định các hàm chèn thực thể khi không có hàng nào khớp hoặc cập nhật các hàm đó nếu đã có một hàng có cùng khoá chính.
Tương tự như hàm @Insert và @Update, hàm @Upsert 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ề một hàm @Upsert cố gắng chèn hoặc cập nhật một hoặc nhiều đối tượng User trong cơ sở dữ liệu:
@Dao interface UserDao { @Upsert suspend fun upsertUsers(vararg users: User) }
Nếu nhận được một tham số duy nhất, thì hàm @Upsert có thể trả về một giá trị Long. Nếu thao tác này dẫn đến việc chèn một hàng mới, thì hàm sẽ trả về rowId của hàng mới được chèn. Nếu thao tác này dẫn đến việc một hàng hiện có được cập nhật, thì hàm sẽ trả về -1. Nếu tham số là một mảng hoặc một tập hợp, thì phương thức này sẽ trả về một mảng hoặc một tập hợp các giá trị Long thay thế.
Hàm 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 hàm DAO. Hãy sử dụng các hàm 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
Đoạn mã sau đây xác định một hàm sử dụng truy vấn SELECT để trả về tất cả các đối tượng User trong cơ sở dữ liệu:
@Query("SELECT * FROM user") suspend fun loadAllUsers(): List<User>
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 những thuộc tính mà bạn cần.
Room cho phép bạn trả về một đối tượng dữ liệu 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:
data class NameTuple( @ColumnInfo(name = "first_name") val firstName: String, @ColumnInfo(name = "last_name") val lastName: String )
Sau đó, bạn có thể trả về đối tượng dữ liệu đó từ hàm truy vấn:
@Query("SELECT first_name, last_name FROM user") suspend fun loadFullName(): List<NameTuple>
Vì truy vấn trả về các giá trị cho cột first_name và last_name, nên Room sẽ liên kết các giá trị này với các thuộc tính trong lớp NameTuple. Nếu truy vấn trả về một cột không liên kết đến một thuộc tính trong đối tượng được trả về, Room sẽ hiển thị cảnh báo.
Mặc dù ví dụ trước sử dụng một lớp dữ liệu tuỳ chỉnh để truy xuất một nhóm nhỏ các cột, nhưng Room cũng hỗ trợ việc trả về kotlin.Pair và kotlin.Triple để thuận tiện khi một truy vấn trả về chính xác 2 hoặc 3 cột. Khi sử dụng các loại này, các cột sẽ được liên kết theo thứ tự mà chúng được xác định trong câu lệnh truy vấn, vì vậy, thứ tự của các cột trong câu lệnh SELECT phải khớp với thứ tự của các loại trong Pair hoặc Triple.
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 hàm 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ố hàm 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 hàm trả về tất cả người dùng trên một độ tuổi nhất định:
@Query("SELECT * FROM user WHERE age > :minAge") suspend fun loadAllUsersOlderThan(minAge: Int): Array<User>
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:
@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>
Truyền một tập hợp các tham số tới truy vấn
Một số hàm 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. Nếu một tham số đại diện cho một tập hợp, thì tham số đó sẽ tự động mở rộng trong thời gian chạy dựa trên số lượng giá trị.
Ví dụ: Mã sau đây xác định một hàm trả về thông tin về tất cả người dùng từ một tập hợp nhỏ các khu vực:
@Query("SELECT * FROM user WHERE region IN (:regions)") suspend fun loadUsersFromRegions(regions: List<String>): List<User>
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 hàm kết hợp 3 bảng với nhau để trả về các sách hiện đang cho một người dùng cụ thể mượn:
@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>
Bạn cũng có thể xác định các đối tượng dữ liệu để trả về một tập hợp nhỏ các cột từ nhiều bảng đã kết hợp. Để biết thêm thông tin, hãy xem phần Trả về một tập hợp nhỏ các cột của bảng. Mã sau đây xác định một DAO bằng một hàm trả về tên người dùng và tên sách mà họ đã mượn:
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)
Trả về kiểu cấu trúc đa ánh xạ (multimap)
Đối với các thao tác kết hợp, 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 viết các hàm truy vấn trả về đa ánh xạ.
Hãy 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ác cặp phiên bản User và Book, bạn có thể trực tiếp trả về một bản ánh xạ của User và Book từ hàm truy vấn của mình:
@Query( """ SELECT * FROM user JOIN book ON user.id = book.user_id """ ) suspend fun loadUserAndBookNames(): Map<User, List<Book>>
Khi hàm truy vấn của bạn trả về một đa ánh xạ, bạn có thể viết 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 hàm loadUserAndBookNames để chỉ trả về những người dùng đã mượn từ ba cuốn sách trở lên:
@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>>
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 sử dụng chú giải @MapColumn trên các tham số chung của kiểu dữ liệu trả về.
@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> >
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. Để sử dụng các kiểu dữ liệu trả về Paging 3, bạn phải đăng ký trình chuyển đổi kiểu dữ liệu trả về Paging trong cơ sở dữ liệu hoặc DAO:
- Đưa cấu phần phần mềm
androidx.room3:room3-pagingvào cấu hình bản dựng. - Chú giải khai báo
@Databasehoặc@Daobằng@DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class).
Sau khi đăng ký, các DAO của bạn có thể trả về các đối tượng PagingSource để sử dụng với Paging 3:
@Dao @DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class) interface UserDao { @Query("SELECT * FROM users WHERE label LIKE :query") fun pagingSource(query: String): PagingSource<Int, User> }
Để biết thêm thông tin về cách chọn tham số loại cho PagingSource, hãy xem phần Chọn loại khoá và giá trị.
Quyền truy cập trực tiếp vào kết nối cơ sở dữ liệu
Nếu logic của ứng dụng yêu cầu quyền truy cập trực tiếp, cấp thấp vào kết nối cơ sở dữ liệu, bạn có thể sử dụng API kết nối của Room. Bạn có thể lấy một kết nối bằng cách sử dụng useReaderConnection cho các thao tác chỉ đọc hoặc useWriterConnection cho các thao tác ghi trên phiên bản RoomDatabase và sử dụng usePrepared để thực thi các câu lệnh:
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)) } } } }
Nếu cần thực hiện các giao dịch cơ sở dữ liệu cấp thấp ngay trên kết nối, bạn có thể sử dụng các hàm trợ giúp immediateTransaction, deferredTransaction hoặc exclusiveTransaction trên một thực thể Transactor bên trong khối useWriterConnection:
roomDatabase.useWriterConnection { transactor -> transactor.immediateTransaction { // Perform transactional database operations using transactor } }
Ngoài ra, nếu bạn chỉ cần thực thi các thao tác DAO cấp cao trong một giao dịch, hãy sử dụng các hàm tiện ích mở rộng withReadTransaction hoặc withWriteTransaction trên thực thể RoomDatabase của bạn:
// 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) }