Room (Đa nền tảng Kotlin)

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

Phiên bản Room hiện tại hỗ trợ KMP là phiên bản 2.7.0-alpha01 trở lên.

Để 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 cho mô-đun của bạn:

  • androidx.room:room-gradle-plugin – Trình bổ trợ Gradle để định cấu hình giản đồ Room
  • androidx.room:room-compiler – Bộ xử lý KSP tạo mã
  • androidx.room:room-runtime – Phần thời gian chạy của thư viện
  • androidx.sqlite:sqlite-bundled – (Không bắt buộc) Thư viện SQLite đi kèm

Ngoài ra, bạn cần định cấu hình trình điều khiển SQLite của Room. Các trình điều khiển này khác nhau tuỳ theo nền tảng mục tiêu. Hãy xem phần 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ó.

Để biết thêm thông tin về cách thiết lập, hãy xem các phần sau:

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ú thích bằng @Database cùng với các DAO và thực thể bên trong nhóm tài nguyê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 phổ biến sẽ cho phép chia sẻ các lớp này trên tất cả các nền tảng mục tiêu.

Khi bạn khai báo đố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 "Expected object 'AppDatabaseConstructor' has no actual declaration in module"; bạn có thể ngăn cảnh báo bằng @Suppress("NO_ACTUAL_FOR_EXPECT").

// 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("NO_ACTUAL_FOR_EXPECT")
expect object AppDatabaseConstructor : RoomDatabaseConstructor<AppDatabase> {
    override fun initialize(): AppDatabase
}

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

@Entity
data class TodoEntity(
  @PrimaryKey(autoGenerate = true) val id: Long = 0,
  val title: String,
  val content: String
)

Xin lưu ý rằng bạn có thể tuỳ ý sử dụng các nội dung khai báo thực tế / dự kiến để tạo các phương thức triển khai Room dành riêng cho nền tảng. Ví dụ: bạn có thể thêm một DAO dành riêng cho nền tảng được xác định trong mã chung bằng cách sử dụng expect, sau đó chỉ định các định nghĩa actual bằng các truy vấn bổ sung trong các nhóm tài nguyên dành riêng cho nền tảng.

Tạo trình tạo cơ sở dữ liệu

Bạn cần xác định một trình tạo cơ sở dữ liệu để tạo bản sao 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 nền tảng do sự khác biệt trong các API hệ thống tệp. Ví dụ: trong Android, vị trí cơ sở dữ liệu thường được lấy thông qua API Context.getDatabasePath(), trong khi đối với iOS, vị trí cơ sở dữ liệu được lấy bằng NSFileManager.

Android

Để tạo thực thể cơ sở dữ liệu, hãy chỉ định Ngữ cảnh cùng với đường dẫn cơ sở dữ liệu.

// shared/src/androidMain/kotlin/Database.kt

fun getDatabaseBuilder(ctx: Context): RoomDatabase.Builder<AppDatabase> {
  val appContext = ctx.applicationContext
  val dbFile = appContext.getDatabasePath("my_room.db")
  return Room.databaseBuilder<AppDatabase>(
    context = appContext,
    name = dbFile.absolutePath
  )
}

iOS

Để tạo thực thể cơ sở dữ liệu, hãy cung cấp đường dẫn cơ sở dữ liệu bằng cách sử dụng NSFileManager, thường nằm trong NSDocumentDirectory.

// shared/src/iosMain/kotlin/Database.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 đường dẫn cơ sở dữ liệu bằng API Java hoặc Kotlin.

// shared/src/jvmMain/kotlin/Database.kt

fun getDatabaseBuilder(): RoomDatabase.Builder<AppDatabase> {
    val dbFile = File(System.getProperty("java.io.tmpdir"), "my_room.db")
    return Room.databaseBuilder<AppDatabase>(
        name = dbFile.absolutePath,
    )
}

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 để Room có thể tìm thấy cách triển khai được tạo của định nghĩa cơ sở dữ liệu:

-keep class * extends androidx.room.RoomDatabase { <init>(); }

Tạo bản sao 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 bản sao cơ sở dữ liệu thực tế.

// shared/src/commonMain/kotlin/Database.kt

fun getRoomDatabase(
    builder: RoomDatabase.Builder<AppDatabase>
): AppDatabase {
  return builder
      .addMigrations(MIGRATIONS)
      .fallbackToDestructiveMigrationOnDowngrade()
      .setDriver(BundledSQLiteDriver())
      .setQueryCoroutineContext(Dispatchers.IO)
      .build()
}

Chọn SQLiteDriver

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 trình điều khiển dành riêng cho nền tảng. Đối với Android, bạn có thể sử dụng AndroidSQLiteDriver, còn đối với iOS, bạn có thể sử dụng NativeSQLiteDriver. Để sử dụng NativeSQLiteDriver, bạn cần cung cấp tuỳ chọn trình liên kết để ứ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")
        }
    }
}

Điểm khác biệt

Room ban đầu được phát triển dưới dạng thư viện Android và sau đó được di chuyển sang KMP, tập trung vào khả năng tương thích với API. Phiên bản KMP của Room có một số điểm khác biệt giữa các nền tảng và 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.

Chặn các hàm DAO

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 phải là hàm suspend, ngoại trừ các loại dữ liệu trả về phản ứng, chẳng hạn như Flow.

// shared/src/commonMain/kotlin/MultiplatformDao.kt

@Dao
interface MultiplatformDao {
  // ERROR: Blocking function not valid for non-Android targets
  @Query("SELECT * FROM Entity")
  fun blockingQuery(): List<Entity>

  // OK
  @Query("SELECT * FROM Entity")
  suspend fun query(): List<Entity>

  // OK
  @Query("SELECT * FROM Entity")
  fun queryFlow(): Flow<List<Entity>>

  // ERROR: Blocking function not valid for non-Android targets
  @Transaction
  fun blockingTransaction() { // … }

  // OK
  @Transaction
  suspend fun transaction() { // … }
}

Room được 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 dự án KMP, ngoại trừ các DAO dành riêng cho Android để duy trì khả năng tương thích ngược với cơ sở mã hiện có.

Điểm khác biệt về tính năng với KMP

Phần này mô tả sự khác biệt về tính năng giữa các phiên bản Room dành cho KMP và nền tảng Android.

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.

@Dao
interface TodoDao {
  @RawQuery
  suspend fun getTodos(query RoomRawQuery): List<TodoEntity>
}

Sau đó, bạn có thể sử dụng RoomRawQuery để tạo truy vấn trong thời gian chạy:

suspend fun getTodosWithLowercaseTitle(title: String): List<TodoEntity> {
  val query = RoomRawQuery(
    sql = "SELECT * FROM TodoEntity WHERE title = ?"
    onBindStatement = {
      it.bindText(1, title.lowercase())
    }
  )
  return todosDao.getTodos(query)
}

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ó sẵn trong các nền tảng phổ biến và do đó không có sẵn trong các nền tảng khác ngoài Android.

  • RoomDatabase.Builder.setQueryCallback
  • RoomDatabase.QueryCallback

Chúng tôi dự định hỗ trợ lệnh gọi lại truy vấn trong phiên bản Room trong tương lai.

API để định cấu hình RoomDatabase bằng lệnh gọi lại truy vấn RoomDatabase.Builder.setQueryCallback cùng với giao diện gọi lại RoomDatabase.QueryCallback không có sẵn chung và do đó không có sẵn trên các nền tảng khác ngoài Android.

Cơ sở dữ liệu tự động đóng

API để bật tính năng tự động đóng sau khi hết 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 đóng gói trước

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 trước) không có sẵn và do đó không có sẵn trên 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 hỗ trợ thêm cơ sở dữ liệu đóng gói sẵn trong phiên bản Room trong tương lai.

Vô hiệu hoá nhiều phiên bản

API để bật tính năng vô hiệu hoá nhiều thực thể, RoomDatabase.Builder.enableMultiInstanceInvalidation chỉ có trên Android và không có trong các nền tảng phổ biến hoặc nền tảng khác.