Nếu ứng dụng của bạn xử lý lượng dữ liệu có cấu trúc với lượng không nhỏ, thì bạn có thể được hưởng nhiều lợi ích khi lưu dữ liệu đó trên máy. Trường hợp sử dụng phổ biến nhất là lưu các phần dữ liệu có liên quan vào bộ nhớ đệm. Bằng cách này, khi thiết bị không thể truy cập mạng, bạn vẫn duyệt qua được nội dung đó.
Thư viện lưu trữ dữ liệu Room cung cấp một lớp trừu tượng qua SQLite để cho phép bạn truy cập vào cơ sở dữ liệu một cách dễ dàng, đồng thời khai thác toàn bộ sức mạnh của SQLite.
Thiết lập Room 2.x
Để sử dụng Room 2.x trong ứng dụng, hãy thêm các phần phụ thuộc sau vào tệp build.gradle của ứng dụng:
dependencies {
val room_version = "2.6.1"
implementation("androidx.room:room-runtime:$room_version")
annotationProcessor("androidx.room:room-compiler:$room_version")
// To use Kotlin Symbol Processing (KSP)
// ksp("androidx.room:room-compiler:$room_version")
// optional - Kotlin Extensions and Coroutines support for Room
implementation("androidx.room:room-ktx:$room_version")
// optional - RxJava2 support for Room
implementation("androidx.room:room-rxjava2:$room_version")
// optional - Guava support for Room, including Optional and ListenableFuture
implementation("androidx.room:room-guava:$room_version")
// optional - Test helpers
testImplementation("androidx.room:room-testing:$room_version")
}
Thành phần chính
Room có 3 thành phần chính:
- Lớp cơ sở dữ liệu lưu giữ cơ sở dữ liệu và đóng vai trò là điểm truy cập chính cho đường kết nối cơ bản đến dữ liệu cố định của ứng dụng.
- Thực thể dữ liệu đại diện cho các bảng trong cơ sở dữ liệu của ứng dụng.
- Đối tượng truy cập dữ liệu (DAO) cung cấp các phương thức mà ứng dụng của bạn có thể dùng để truy vấn, cập nhật, chèn và xoá dữ liệu trong cơ sở dữ liệu.
Hình 1 minh hoạ mối quan hệ giữa các thành phần của Room.
Triển khai mẫu
// Entity
@Entity
data class User(
@PrimaryKey val uid: Int,
@ColumnInfo(name = "first_name") val firstName: String?,
@ColumnInfo(name = "last_name") val lastName: String?
)
// DAO
@Dao
interface UserDao {
@Query("SELECT * FROM user")
fun getAll(): List<User>
@Insert
fun insertAll(vararg users: User)
@Delete
fun delete(user: User)
}
// Database
@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
// Usage
val db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "database-name"
).build()
val userDao = db.userDao()
val users: List<User> = userDao.getAll()
Xác định dữ liệu bằng thực thể
Mỗi thực thể Room đại diện cho một bảng trong cơ sở dữ liệu. Bạn xác định từng thực thể dưới dạng một lớp được chú thích bằng @Entity.
@Entity(tableName = "users")
data class User (
@PrimaryKey val id: Int,
@ColumnInfo(name = "first_name") val firstName: String?,
@ColumnInfo(name = "last_name") val lastName: String?,
@Ignore val picture: Bitmap? = null
)
- Tên bảng và tên cột tuỳ chỉnh: Theo mặc định, Room sử dụng tên lớp làm tên bảng và tên thuộc tính làm tên cột. Để tuỳ chỉnh các thành phần này, hãy dùng thuộc tính
tableNametrong@Entityvà chú thích@ColumnInfo(name = "..."). - Khoá chính: Để xác định khoá chính, hãy sử dụng
@PrimaryKey. Đối với các khoá kết hợp, hãy dùng thuộc tínhprimaryKeyscủa@Entity:@Entity(primaryKeys = ["firstName", "lastName"]). - Bỏ qua các trường: Để ngăn các trường được duy trì, hãy sử dụng
@Ignore.
Trình chuyển đổi loại
Đôi khi, bạn cần lưu trữ các loại tuỳ chỉnh, chẳng hạn như Date, trong một cột duy nhất.
Cung cấp các phương thức @TypeConverter để chuyển đổi các loại tuỳ chỉnh thành và từ các loại mà Room có thể duy trì.
class Converters {
@TypeConverter
fun fromTimestamp(value: Long?): Date? = value?.let { Date(it) }
@TypeConverter
fun dateToTimestamp(date: Date?): Long? = date?.time
}
// Register in your Database class
@Database(entities = [User::class], version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() { ... }
Truy cập dữ liệu bằng DAO
DAO xác định các phương thức tương tác với cơ sở dữ liệu. Chú giải giao diện hoặc lớp trừu tượng bằng @Dao.
Phương thức thuận tiện
@Dao
interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertUsers(vararg users: User)
@Update
fun updateUsers(vararg users: User)
@Delete
fun deleteUsers(vararg users: User)
}
- Chèn: Phương thức chèn có thể trả về một
Longđại diện cho mã nhận dạng hàng được chèn hoặc mộtList<Long>chứa mã nhận dạng của tất cả các hàng được chèn. - Cập nhật hoặc Xoá: Phương thức cập nhật hoặc xoá có thể trả về một
Intđại diện cho số lượng hàng bị ảnh hưởng.
Phương thức truy vấn
Chú giải các phương thức bằng @Query để viết câu lệnh SQL. Room xác thực các truy vấn vào thời gian biên dịch.
@Dao
interface UserDao {
// Simple query
@Query("SELECT * FROM user")
fun loadAllUsers(): Array<User>
// Return a subset of columns using a POJO or tuple
@Query("SELECT first_name, last_name FROM user")
fun loadFullName(): List<NameTuple>
// Pass parameters
@Query("SELECT * FROM user WHERE age > :minAge")
fun loadAllUsersOlderThan(minAge: Int): Array<User>
// Collection of parameters
@Query("SELECT * FROM user WHERE region IN (:regions)")
fun loadUsersFromRegions(regions: List<String>): List<User>
// Join tables
@Query("SELECT * FROM book INNER JOIN user ON user.id = book.user_id WHERE user.name = :userName")
fun findBooksBorrowedByName(userName: String): List<Book>
}
Kiểu dữ liệu trả về nhiều bản đồ
Trong Room 2.4 trở lên, các phương thức truy vấn có thể trực tiếp trả về một đa ánh xạ bằng cách sử dụng kiểu Map:
@Query("SELECT * FROM user JOIN book ON user.id = book.user_id")
fun loadUserAndBookNames(): Map<User, List<Book>>
Truy vấn DAO không đồng bộ
Để tránh tình trạng giao diện người dùng bị treo, các truy vấn cơ sở dữ liệu không thể chạy trên luồng chính. Thực hiện các truy vấn không đồng bộ bằng một trong các chế độ tích hợp sau:
Coroutine và Flow trong Kotlin
Yêu cầu phần phụ thuộc room-ktx.
@Dao
interface UserDao {
// One-shot async query
@Insert
suspend fun insertUsers(vararg users: User)
// Observable query using Flow
@Query("SELECT * FROM user WHERE id = :id")
fun loadUserById(id: Int): Flow<User>
}
Java với RxJava
Bạn phải sử dụng room-rxjava2 hoặc room-rxjava3.
@Dao
interface UserDao {
@Insert
fun insertUsers(users: List<User>): Completable
@Query("SELECT * FROM user WHERE id = :id")
fun loadUserById(id: Int): Flowable<User>
}
Java với LiveData và Guava
Yêu cầu phải có room-guava cho ListenableFuture.
@Dao
interface UserDao {
// LiveData for observable queries
@Query("SELECT * FROM user WHERE id = :id")
fun loadUserById(id: Int): LiveData<User>
// Guava ListenableFuture for one-shot queries
@Insert
fun insertUsers(users: List<User>): ListenableFuture<Integer>
}
Xác định mối quan hệ trong Room 2.x
Để ngăn tải từng phần trên luồng giao diện người dùng, bạn không thể sử dụng các tham chiếu đối tượng trực tiếp giữa các thực thể. Thay vào đó, hãy xác định các mối quan hệ bằng cách sử dụng các lớp dữ liệu trung gian có @Relation.
Một với một
Mỗi người dùng chỉ có một thư viện.
@Entity
data class User(@PrimaryKey val userId: Long, val name: String)
@Entity
data class Library(@PrimaryKey val libraryId: Long, val userOwnerId: Long)
// Intermediate class
data class UserAndLibrary(
@Embedded val user: User,
@Relation(
parentColumn = "userId",
entityColumn = "userOwnerId"
)
val library: Library
)
// DAO Query
@Transaction
@Query("SELECT * FROM User")
fun getUsersAndLibraries(): List<UserAndLibrary>
Một với nhiều
Mỗi người dùng có thể có nhiều danh sách phát.
@Entity
data class Playlist(@PrimaryKey val playlistId: Long, val userCreatorId: Long)
data class UserWithPlaylists(
@Embedded val user: User,
@Relation(
parentColumn = "userId",
entityColumn = "userCreatorId"
)
val playlists: List<Playlist>
)
Nhiều-nhiều
Danh sách phát có thể có nhiều bài hát và bài hát có thể nằm trong nhiều danh sách phát. Cần có một bảng kết hợp.
@Entity
data class Song(@PrimaryKey val songId: Long, val songName: String)
@Entity(primaryKeys = ["playlistId", "songId"])
data class PlaylistSongCrossRef(val playlistId: Long, val songId: Long)
data class PlaylistWithSongs(
@Embedded val playlist: Playlist,
@Relation(
parentColumn = "playlistId",
entityColumn = "songId",
associateBy = Junction(PlaylistSongCrossRef::class)
)
val songs: List<Song>
)
Mối quan hệ lồng ghép
Truy vấn người dùng, danh sách phát của họ và tất cả bài hát trong những danh sách phát đó.
data class UserWithPlaylistsAndSongs(
@Embedded val user: User,
@Relation(
entity = Playlist::class,
parentColumn = "userId",
entityColumn = "userCreatorId"
)
val playlists: List<PlaylistWithSongs> // Nesting PlaylistWithSongs
)
Quản lý cơ sở dữ liệu
Phần này đề cập đến nhiều khía cạnh của việc quản lý cơ sở dữ liệu Room, bao gồm cả chế độ xem cơ sở dữ liệu, dữ liệu điền sẵn và hoạt động di chuyển cơ sở dữ liệu.
Chế độ xem cơ sở dữ liệu
Đóng gói một truy vấn phức tạp vào một lớp được chú thích bằng @DatabaseView.
@DatabaseView("SELECT user.id, user.name, department.name AS departmentName FROM user INNER JOIN department ON user.departmentId = department.id")
data class UserDetail(val id: Long, val name: String, val departmentName: String)
// Register in Database class
@Database(entities = [User::class], views = [UserDetail::class], version = 1)
abstract class AppDatabase : RoomDatabase() { ... }
Điền sẵn cơ sở dữ liệu
Điền dữ liệu vào cơ sở dữ liệu tại thời điểm khởi tạo từ một tệp tài sản hoặc hệ thống tệp.
Room.databaseBuilder(appContext, AppDatabase::class.java, "Sample.db")
.createFromAsset("database/myapp.db")
.build()
Di chuyển
Khi bạn thay đổi giản đồ, hãy tăng phiên bản cơ sở dữ liệu và xác định một đối tượng Migration.
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE User ADD COLUMN age INTEGER NOT NULL DEFAULT 0")
}
}
Room.databaseBuilder(applicationContext, AppDatabase::class.java, "database-name")
.addMigrations(MIGRATION_1_2)
.build()
- Di chuyển tự động: Nếu sử dụng Room 2.4.0 trở lên, bạn có thể dùng
@AutoMigrationđể tự động di chuyển các thay đổi cơ bản về giản đồ. Bạn cần đặtexportSchemathànhtruetrong cấu hình cơ sở dữ liệu:@Database(version = 2, autoMigrations = [AutoMigration(from = 1, to = 2)]). - Dự phòng huỷ dữ liệu: Nếu bạn chấp nhận mất dữ liệu khi thiếu đường dẫn di chuyển, hãy gọi
.fallbackToDestructiveMigrationkhi tạo cơ sở dữ liệu.
Kiểm thử các quá trình di chuyển
Để xác minh quá trình di chuyển, hãy sử dụng MigrationTestHelper từ cấu phần phần mềm room-testing. Để hỗ trợ việc này, hãy đảm bảo rằng bạn xuất các giản đồ trong cấu hình build.gradle.
@RunWith(AndroidJUnit4::class)
class MigrationTest {
@get:Rule
val helper: MigrationTestHelper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java.canonicalName,
FrameworkSQLiteOpenHelperFactory()
)
@Test
fun migrate1To2() {
var db = helper.createDatabase("test-db", 1).apply {
execSQL("INSERT INTO User VALUES (1, 'John')")
close()
}
db = helper.runMigrationsAndValidate("test-db", 2, true, MIGRATION_1_2)
// Verify data was migrated correctly
}
}
Di chuyển từ SQLite sang Room
Để di chuyển ứng dụng của bạn từ SQLite sang Room, hãy hoàn tất các bước sau:
- Cập nhật các phần phụ thuộc để thêm Room.
- Chú giải các lớp mô hình bằng
@Entity,@PrimaryKeyvà@ColumnInfo. - Tạo DAO để thay thế các phương thức truy vấn trợ giúp.
- Tạo một lớp RoomDatabase tham chiếu đến các thực thể và DAO của bạn. Tăng số phiên bản.
- Xác định một đường dẫn di chuyển trống vì giản đồ không thay đổi, chỉ có khung thay đổi:
kotlin val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) {} } - Cập nhật quá trình khởi tạo để sử dụng
Room.databaseBuildertheo đường dẫn di chuyển.