Thư viện lưu trữ Room cung cấp một lớp trừu tượng qua SQLite để mang lại khả năng truy cập cơ sở dữ liệu mạnh mẽ hơn, đồng thời khai thác toàn bộ sức mạnh của SQLite. Trang này tập trung vào việc sử dụng Room trong các dự án Kotlin Multiplatform (KMP). Để biết thêm thông tin về cách sử dụng Room, hãy xem bài viết Lưu dữ liệu trong cơ sở dữ liệu cục bộ bằng Room hoặc các mẫu chính thức của chúng tôi.
Thiết lập phần phụ thuộc
Để thiết lập Room trong dự án KMP, hãy thêm các phần phụ thuộc cho cấu phần phần mềm trong tệp build.gradle.kts
của mô-đun KMP.
Xác định các phần phụ thuộc trong tệp libs.versions.toml
:
[versions]
room = "2.7.2"
sqlite = "2.5.2"
ksp = "<kotlinCompatibleKspVersion>"
[libraries]
androidx-sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "sqlite" }
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
# Optional SQLite Wrapper available in version 2.8.0 and higher
androidx-room-sqlite-wrapper = { module = "androidx.room:room-sqlite-wrapper", version.ref = "room" }
[plugins]
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
androidx-room = { id = "androidx.room", version.ref = "room" }
Thêm Trình bổ trợ Room cho Gradle để định cấu hình giản đồ Room và trình bổ trợ KSP
plugins {
alias(libs.plugins.ksp)
alias(libs.plugins.androidx.room)
}
Thêm phần phụ thuộc thời gian chạy Room và thư viện SQLite đi kèm:
commonMain.dependencies {
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.sqlite.bundled)
}
// Optional when using Room SQLite Wrapper
androidMain.dependencies {
implementation(libs.androidx.room.sqlite.wrapper)
}
Thêm các phần phụ thuộc KSP vào khối root dependencies
. Xin lưu ý rằng bạn cần thêm tất cả các mục tiêu mà ứng dụng của bạn sử dụng. Để biết thêm thông tin, hãy xem KSP với Kotlin Multiplatform.
dependencies {
add("kspAndroid", libs.androidx.room.compiler)
add("kspIosSimulatorArm64", libs.androidx.room.compiler)
add("kspIosX64", libs.androidx.room.compiler)
add("kspIosArm64", libs.androidx.room.compiler)
// Add any other platform target you use in your project, for example kspDesktop
}
Xác định thư mục giản đồ Room. Để biết thêm thông tin, hãy xem phần Đặt vị trí giản đồ bằng trình bổ trợ Room cho Gradle.
room {
schemaDirectory("$projectDir/schemas")
}
Xác định các lớp cơ sở dữ liệu
Bạn cần tạo một lớp cơ sở dữ liệu có chú giải bằng @Database
cùng với các DAO và thực thể bên trong tập hợp nguồn chung của mô-đun KMP dùng chung. Việc đặt các lớp này vào các nguồn chung sẽ cho phép chia sẻ chúng trên tất cả các nền tảng mục tiêu.
// shared/src/commonMain/kotlin/Database.kt
@Database(entities = [TodoEntity::class], version = 1)
@ConstructedBy(AppDatabaseConstructor::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun getDao(): TodoDao
}
// The Room compiler generates the `actual` implementations.
@Suppress("KotlinNoActualForExpect")
expect object AppDatabaseConstructor : RoomDatabaseConstructor<AppDatabase> {
override fun initialize(): AppDatabase
}
Khi bạn khai báo một đối tượng expect
bằng giao diện RoomDatabaseConstructor
, trình biên dịch Room sẽ tạo các phương thức triển khai actual
. Android Studio có thể đưa ra cảnh báo sau đây mà bạn có thể bỏ qua bằng @Suppress("KotlinNoActualForExpect")
:
Expected object 'AppDatabaseConstructor' has no actual declaration in module`
Tiếp theo, hãy xác định một giao diện DAO mới hoặc di chuyển một giao diện hiện có sang commonMain
:
// shared/src/commonMain/kotlin/TodoDao.kt
@Dao
interface TodoDao {
@Insert
suspend fun insert(item: TodoEntity)
@Query("SELECT count(*) FROM TodoEntity")
suspend fun count(): Int
@Query("SELECT * FROM TodoEntity")
fun getAllAsFlow(): Flow<List<TodoEntity>>
}
Xác định hoặc di chuyển các thực thể của bạn sang commonMain
:
// shared/src/commonMain/kotlin/TodoEntity.kt
@Entity
data class TodoEntity(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val title: String,
val content: String
)
Tạo trình tạo cơ sở dữ liệu dành riêng cho nền tảng
Bạn cần xác định một trình tạo cơ sở dữ liệu để khởi tạo Room trên mỗi nền tảng. Đây là phần duy nhất của API bắt buộc phải có trong các nhóm tài nguyên dành riêng cho từng nền tảng do sự khác biệt về API hệ thống tệp.
Android
Trên Android, vị trí cơ sở dữ liệu thường được lấy thông qua API Context.getDatabasePath()
. Để tạo phiên bản cơ sở dữ liệu, hãy chỉ định một Context
cùng với đường dẫn cơ sở dữ liệu.
// shared/src/androidMain/kotlin/Database.android.kt
fun getDatabaseBuilder(context: Context): RoomDatabase.Builder<AppDatabase> {
val appContext = context.applicationContext
val dbFile = appContext.getDatabasePath("my_room.db")
return Room.databaseBuilder<AppDatabase>(
context = appContext,
name = dbFile.absolutePath
)
}
iOS
Để tạo phiên bản cơ sở dữ liệu trên iOS, hãy cung cấp một đường dẫn cơ sở dữ liệu bằng NSFileManager
, thường nằm trong NSDocumentDirectory
.
// shared/src/iosMain/kotlin/Database.ios.kt
fun getDatabaseBuilder(): RoomDatabase.Builder<AppDatabase> {
val dbFilePath = documentDirectory() + "/my_room.db"
return Room.databaseBuilder<AppDatabase>(
name = dbFilePath,
)
}
private fun documentDirectory(): String {
val documentDirectory = NSFileManager.defaultManager.URLForDirectory(
directory = NSDocumentDirectory,
inDomain = NSUserDomainMask,
appropriateForURL = null,
create = false,
error = null,
)
return requireNotNull(documentDirectory?.path)
}
JVM (Máy tính)
Để tạo thực thể cơ sở dữ liệu, hãy cung cấp một đường dẫn cơ sở dữ liệu bằng cách sử dụng API Java hoặc Kotlin.
// shared/src/jvmMain/kotlin/Database.desktop.kt
fun getDatabaseBuilder(): RoomDatabase.Builder<AppDatabase> {
val dbFile = File(System.getProperty("java.io.tmpdir"), "my_room.db")
return Room.databaseBuilder<AppDatabase>(
name = dbFile.absolutePath,
)
}
Tạo thực thể cho cơ sở dữ liệu
Sau khi lấy RoomDatabase.Builder
từ một trong các hàm khởi tạo dành riêng cho nền tảng, bạn có thể định cấu hình phần còn lại của cơ sở dữ liệu Room trong mã chung cùng với quá trình thực hiện cơ sở dữ liệu thực tế.
// shared/src/commonMain/kotlin/Database.kt
fun getRoomDatabase(
builder: RoomDatabase.Builder<AppDatabase>
): AppDatabase {
return builder
.setDriver(BundledSQLiteDriver())
.setQueryCoroutineContext(Dispatchers.IO)
.build()
}
Chọn một trình điều khiển SQLite
Đoạn mã trước đó gọi hàm trình tạo setDriver
để xác định trình điều khiển SQLite mà cơ sở dữ liệu Room sẽ sử dụng. Các trình điều khiển này khác nhau tuỳ theo nền tảng mục tiêu. Các đoạn mã trước đó sử dụng BundledSQLiteDriver
.
Đây là trình điều khiển được đề xuất, bao gồm SQLite được biên dịch từ nguồn, cung cấp phiên bản SQLite nhất quán và mới nhất trên tất cả các nền tảng.
Nếu bạn muốn sử dụng SQLite do hệ điều hành cung cấp, hãy sử dụng API setDriver
trong các nhóm tài nguyên dành riêng cho nền tảng để chỉ định một trình điều khiển dành riêng cho nền tảng. Hãy xem phần Cách triển khai trình điều khiển để biết nội dung mô tả về các cách triển khai trình điều khiển hiện có. Bạn có thể sử dụng một trong hai cách sau:
AndroidSQLiteDriver
trongandroidMain
NativeSQLiteDriver
trongiosMain
Để sử dụng NativeSQLiteDriver
, bạn cần cung cấp một lựa chọn trình liên kết -lsqlite3
để ứng dụng iOS liên kết linh động với SQLite của hệ thống.
// shared/build.gradle.kts
kotlin {
listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach { iosTarget ->
iosTarget.binaries.framework {
baseName = "TodoApp"
isStatic = true
// Required when using NativeSQLiteDriver
linkerOpts.add("-lsqlite3")
}
}
}
Đặt bối cảnh Coroutine (Không bắt buộc)
Bạn có thể tuỳ ý định cấu hình đối tượng RoomDatabase
trên Android bằng các trình thực thi ứng dụng dùng chung bằng cách sử dụng RoomDatabase.Builder.setQueryExecutor()
để thực hiện các thao tác trên cơ sở dữ liệu.
Vì các trình thực thi không tương thích với KMP, nên API setQueryExecutor()
của Room không có trong commonMain
. Thay vào đó, bạn phải định cấu hình đối tượng RoomDatabase
bằng CoroutineContext
. Bạn có thể đặt CoroutineContext
bằng cách sử dụng RoomDatabase.Builder.setCoroutineContext()
. Nếu không có bối cảnh nào được đặt, thì đối tượng RoomDatabase
sẽ mặc định sử dụng Dispatchers.IO
.
Giảm thiểu và làm rối mã nguồn
Nếu dự án được rút gọn hoặc làm rối mã nguồn, thì bạn phải thêm quy tắc ProGuard sau đây để Room có thể tìm thấy quá trình triển khai đã tạo của định nghĩa cơ sở dữ liệu:
-keep class * extends androidx.room.RoomDatabase { <init>(); }
Di chuyển sang Kotlin Multiplatform
Room ban đầu được phát triển dưới dạng một thư viện Android và sau đó được di chuyển sang KMP với trọng tâm là khả năng tương thích API. Phiên bản KMP của Room có phần khác biệt giữa các nền tảng và so với phiên bản dành riêng cho Android. Những điểm khác biệt này được liệt kê và mô tả như sau.
Di chuyển từ Support SQLite sang Trình điều khiển SQLite
Mọi cách sử dụng SupportSQLiteDatabase
và các API khác trong androidx.sqlite.db
đều cần được tái cấu trúc bằng API Trình điều khiển SQLite, vì các API trong androidx.sqlite.db
chỉ dành cho Android (lưu ý rằng gói này khác với gói KMP).
Để có khả năng tương thích ngược và miễn là RoomDatabase
được định cấu hình bằng SupportSQLiteOpenHelper.Factory
(ví dụ: không có SQLiteDriver
nào được đặt), thì Room sẽ hoạt động ở "chế độ tương thích" trong đó cả Support SQLite và SQLite Driver API đều hoạt động như mong đợi. Điều này cho phép di chuyển gia tăng để bạn không cần chuyển đổi tất cả các cách sử dụng Support SQLite sang Trình điều khiển SQLite trong một lần thay đổi.
Chuyển đổi các lớp con di chuyển
Các lớp con di chuyển cần được di chuyển sang các lớp tương đương của trình điều khiển SQLite:
Kotlin Multiplatform
Lớp con di chuyển
object Migration_1_2 : Migration(1, 2) {
override fun migrate(connection: SQLiteConnection) {
// …
}
}
Các lớp con của quy cách di chuyển tự động
class AutoMigrationSpec_1_2 : AutoMigrationSpec {
override fun onPostMigrate(connection: SQLiteConnection) {
// …
}
}
Chỉ dành cho Android
Lớp con di chuyển
object Migration_1_2 : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
// …
}
}
Các lớp con của quy cách di chuyển tự động
class AutoMigrationSpec_1_2 : AutoMigrationSpec {
override fun onPostMigrate(db: SupportSQLiteDatabase) {
// …
}
}
Chuyển đổi lệnh gọi lại cơ sở dữ liệu
Bạn cần di chuyển các lệnh gọi lại cơ sở dữ liệu sang các lệnh gọi lại tương ứng của trình điều khiển SQLite:
Kotlin Multiplatform
object MyRoomCallback : RoomDatabase.Callback() {
override fun onCreate(connection: SQLiteConnection) {
// …
}
override fun onDestructiveMigration(connection: SQLiteConnection) {
// …
}
override fun onOpen(connection: SQLiteConnection) {
// …
}
}
Chỉ dành cho Android
object MyRoomCallback : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
// …
}
override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
// …
}
override fun onOpen(db: SupportSQLiteDatabase) {
// …
}
}
Chuyển đổi các hàm DAO @RawQuery
Các hàm được chú thích bằng @RawQuery
được biên dịch cho các nền tảng không phải Android sẽ cần khai báo một tham số thuộc loại RoomRawQuery
thay vì SupportSQLiteQuery
.
Kotlin Multiplatform
Xác định truy vấn thô
@Dao
interface TodoDao {
@RawQuery
suspend fun getTodos(query: RoomRawQuery): List<TodoEntity>
}
Sau đó, bạn có thể dùng RoomRawQuery
để tạo một truy vấn trong thời gian chạy:
suspend fun AppDatabase.getTodosWithLowercaseTitle(title: String): List<TodoEntity> {
val query = RoomRawQuery(
sql = "SELECT * FROM TodoEntity WHERE title = ?",
onBindStatement = {
it.bindText(1, title.lowercase())
}
)
return todoDao().getTodos(query)
}
Chỉ dành cho Android
Xác định truy vấn thô
@Dao
interface TodoDao {
@RawQuery
suspend fun getTodos(query: SupportSQLiteQuery): List<TodoEntity>
}
Sau đó, bạn có thể dùng SimpleSQLiteQuery
để tạo một truy vấn trong thời gian chạy:
suspend fun AndroidOnlyDao.getTodosWithLowercaseTitle(title: String): List<TodoEntity> {
val query = SimpleSQLiteQuery(
query = "SELECT * FROM TodoEntity WHERE title = ?",
bindArgs = arrayOf(title.lowercase())
)
return getTodos(query)
}
Chuyển đổi các hàm DAO chặn
Room hưởng lợi từ thư viện kotlinx.coroutines
không đồng bộ giàu tính năng mà Kotlin cung cấp cho nhiều nền tảng. Để có chức năng tối ưu, các hàm suspend
được thực thi cho các DAO được biên dịch trong một dự án KMP, ngoại trừ các DAO được triển khai trong androidMain
để duy trì khả năng tương thích ngược với cơ sở mã hiện có. Khi sử dụng Room cho KMP, tất cả các hàm DAO được biên dịch cho các nền tảng không phải Android đều cần phải là hàm suspend
.
Kotlin Multiplatform
Tạm dừng truy vấn
@Query("SELECT * FROM Todo")
suspend fun getAllTodos(): List<Todo>
Tạm ngưng giao dịch
@Transaction
suspend fun transaction() { … }
Chỉ dành cho Android
Chặn cụm từ tìm kiếm
@Query("SELECT * FROM Todo")
fun getAllTodos(): List<Todo>
Chặn giao dịch
@Transaction
fun blockingTransaction() { … }
Chuyển đổi các loại phản ứng thành Luồng
Không phải hàm DAO nào cũng cần là hàm tạm ngưng. Các hàm DAO trả về các loại phản ứng như LiveData
hoặc Flowable
của RxJava không được chuyển đổi thành các hàm tạm ngưng. Tuy nhiên, một số loại, chẳng hạn như LiveData
không tương thích với KMP. Các hàm DAO có loại dữ liệu trả về phản ứng phải được di chuyển sang các luồng coroutine.
Kotlin Multiplatform
Các loại phản ứng Flows
@Query("SELECT * FROM Todo")
fun getTodosFlow(): Flow<List<Todo>>
Chỉ dành cho Android
Các loại phản ứng như LiveData
hoặc Flowable
của RxJava
@Query("SELECT * FROM Todo")
fun getTodosLiveData(): LiveData<List<Todo>>
Chuyển đổi API giao dịch
API giao dịch cơ sở dữ liệu cho Room KMP có thể phân biệt giữa các giao dịch ghi (useWriterConnection
) và đọc (useReaderConnection
).
Kotlin Multiplatform
val database: RoomDatabase = …
database.useWriterConnection { transactor ->
transactor.immediateTransaction {
// perform database operations in transaction
}
}
Chỉ dành cho Android
val database: RoomDatabase = …
database.withTransaction {
// perform database operations in transaction
}
Ghi giao dịch
Sử dụng các giao dịch ghi để đảm bảo rằng nhiều truy vấn ghi dữ liệu một cách nguyên tử, để người đọc có thể truy cập nhất quán vào dữ liệu. Bạn có thể thực hiện việc này bằng cách sử dụng useWriterConnection
với một trong 3 loại giao dịch:
immediateTransaction
: Ở chế độ Ghi nhật ký trước (WAL) (mặc định), loại giao dịch này sẽ nhận được một khoá khi bắt đầu, nhưng các trình đọc có thể tiếp tục đọc. Đây là lựa chọn ưu tiên cho hầu hết các trường hợp.deferredTransaction
: Giao dịch sẽ không có khoá cho đến câu lệnh ghi đầu tiên. Hãy sử dụng loại giao dịch này làm hoạt động tối ưu hoá khi bạn không chắc liệu có cần thao tác ghi trong giao dịch hay không. Ví dụ: nếu bạn bắt đầu một giao dịch để xoá các bài hát khỏi một danh sách phát chỉ với tên của danh sách phát và danh sách phát đó không tồn tại, thì bạn không cần thực hiện thao tác ghi (xoá).exclusiveTransaction
: Chế độ này hoạt động giống hệtimmediateTransaction
trong chế độ WAL. Trong các chế độ ghi nhật ký khác, chế độ này ngăn các kết nối cơ sở dữ liệu khác đọc cơ sở dữ liệu trong khi giao dịch đang diễn ra.
Đọc giao dịch
Sử dụng các giao dịch đọc để đọc nhất quán từ cơ sở dữ liệu nhiều lần. Ví dụ: khi bạn có từ 2 truy vấn riêng biệt trở lên và không sử dụng mệnh đề JOIN
. Chỉ các giao dịch bị hoãn lại mới được phép thực hiện trong các kết nối của trình đọc. Việc cố gắng bắt đầu một giao dịch tức thì hoặc độc quyền trong một kết nối đọc sẽ trả về một ngoại lệ, vì đây được coi là các thao tác "ghi".
val database: RoomDatabase = …
database.useReaderConnection { transactor ->
transactor.deferredTransaction {
// perform database operations in transaction
}
}
Không có trong Kotlin Multiplatform
Một số API có sẵn cho Android không có trong Kotlin Multiplatform.
Lệnh gọi lại truy vấn
Các API sau đây để định cấu hình lệnh gọi lại truy vấn không có trong common và do đó không có trong các nền tảng khác ngoài Android.
RoomDatabase.Builder.setQueryCallback
RoomDatabase.QueryCallback
Chúng tôi dự định sẽ thêm tính năng hỗ trợ lệnh gọi lại truy vấn trong phiên bản Room sau này.
API để định cấu hình RoomDatabase
bằng một lệnh gọi lại truy vấn RoomDatabase.Builder.setQueryCallback
cùng với giao diện lệnh gọi lại RoomDatabase.QueryCallback
không có sẵn trong các nền tảng thông thường và do đó không có sẵn trong các nền tảng khác ngoài Android.
Cơ sở dữ liệu đóng tự động
API cho phép tự động đóng sau một khoảng thời gian chờ (RoomDatabase.Builder.setAutoCloseTimeout
) chỉ có trên Android và không có trên các nền tảng khác.
Cơ sở dữ liệu được đóng gói sẵn
Các API sau đây để tạo RoomDatabase
bằng cơ sở dữ liệu hiện có (tức là cơ sở dữ liệu đóng gói sẵn) không có sẵn trong các API thông thường và do đó không có sẵn trong các nền tảng khác ngoài Android. Các API này là:
RoomDatabase.Builder.createFromAsset
RoomDatabase.Builder.createFromFile
RoomDatabase.Builder.createFromInputStream
RoomDatabase.PrepackagedDatabaseCallback
Chúng tôi dự định sẽ hỗ trợ các cơ sở dữ liệu được đóng gói sẵn trong phiên bản Room sau này.
Vô hiệu hoá nhiều phiên bản
API để bật tính năng vô hiệu hoá nhiều phiên bản, RoomDatabase.Builder.enableMultiInstanceInvalidation
chỉ có trên Android và không có trong các nền tảng chung hoặc nền tảng khác.
Đề xuất cho bạn
- Lưu ý: văn bản có đường liên kết sẽ hiện khi JavaScript tắt
- Lớp học lập trình Di chuyển các ứng dụng hiện có sang Room KMP
- Bắt đầu với Lớp học lập trình KMP
- Lưu dữ liệu trong cơ sở dữ liệu cục bộ bằng Room