Di chuyển Room sang Kotlin Multiplatform

Tài liệu này mô tả cách di chuyển mô-đun triển khai Room hiện tại sang một sử dụng Kotlin Multiplatform (KMP).

Di chuyển việc sử dụng Room trong cơ sở mã Android hiện có sang một KMP dùng chung chung mô-đun có thể khác nhau nhiều về độ khó tuỳ thuộc vào các API Room được sử dụng hoặc việc cơ sở mã đã sử dụng Coroutine. Phần này cung cấp một số hướng dẫn và mẹo khi cố gắng di chuyển hoạt động sử dụng Room sang một mô-đun chung.

Trước tiên, bạn cần làm quen với những điểm khác biệt và giữa phiên bản Android của Room và phiên bản KMP cùng với quá trình thiết lập liên quan. Về bản chất, quá trình di chuyển thành công bao gồm việc tái cấu trúc việc sử dụng API SupportSQLite* và thay thế chúng bằng API Trình điều khiển SQLite cùng với việc di chuyển các phần khai báo Room (lớp có chú giải @Database, DAO, thực thể, v.v.) vào mã chung.

Kiểm tra lại thông tin sau đây trước khi tiếp tục:

Các phần tiếp theo mô tả các bước khác nhau cần thực hiện để thành công di chuyển.

Di chuyển từ Hỗ trợ SQLite sang Trình điều khiển SQLite

Các API trong androidx.sqlite.db chỉ dành cho Android và cần phải được tái cấu trúc bằng API Trình điều khiển SQLite. Để 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 (tức là không có SQLiteDriver nào được đặt), thì Room sẽ hoạt động ở "chế độ tương thích" ở đâu cả API hỗ trợ SQLite và API Trình điều khiển SQLite đều hoạt động như dự kiến. Điều này cho phép di chuyển dần dần để bạn không cần chuyển đổi tất cả SQLite hỗ trợ vào Trình điều khiển SQLite trong một thay đổi duy nhất.

Sau đây là các ví dụ về cách sử dụng phổ biến của SQLite hỗ trợ và SQLite của các ngôn ngữ đó Bản sao của tài xế:

Hỗ trợ SQLite (từ)

Thực thi truy vấn mà không có kết quả

val database: SupportSQLiteDatabase = ...
database.execSQL("ALTER TABLE ...")

Thực thi truy vấn có kết quả nhưng không có đối số

val database: SupportSQLiteDatabase = ...
database.query("SELECT * FROM Pet").use { cursor ->
  while (cusor.moveToNext()) {
    // read columns
    cursor.getInt(0)
    cursor.getString(1)
  }
}

Thực thi truy vấn có kết quả và đối số

database.query("SELECT * FROM Pet WHERE id = ?", id).use { cursor ->
  if (cursor.moveToNext()) {
    // row found, read columns
  } else {
    // row not found
  }
}

Trình điều khiển SQLite (đến)

Thực thi truy vấn mà không có kết quả

val connection: SQLiteConnection = ...
connection.execSQL("ALTER TABLE ...")

Thực thi truy vấn có kết quả nhưng không có đối số

val connection: SQLiteConnection = ...
connection.prepare("SELECT * FROM Pet").use { statement ->
  while (statement.step()) {
    // read columns
    statement.getInt(0)
    statement.getText(1)
  }
}

Thực thi truy vấn có kết quả và đối số

connection.prepare("SELECT * FROM Pet WHERE id = ?").use { statement ->
  statement.bindInt(1, id)
  if (statement.step()) {
    // row found, read columns
  } else {
    // row not found
  }
}

Các API giao dịch cơ sở dữ liệu có sẵn trực tiếp trong SupportSQLiteDatabase nhờ beginTransaction(), setTransactionSuccessful()endTransaction(). Bạn cũng có thể dùng runInTransaction() thông qua Room. Di chuyển các mã này cho API Trình điều khiển SQLite.

Hỗ trợ SQLite (từ)

Thực hiện giao dịch (sử dụng RoomDatabase)

val database: RoomDatabase = ...
database.runInTransaction {
  // perform database operations in transaction
}

Thực hiện giao dịch (sử dụng SupportSQLiteDatabase)

val database: SupportSQLiteDatabase = ...
database.beginTransaction()
try {
  // perform database operations in transaction
  database.setTransactionSuccessful()
} finally {
  database.endTransaction()
}

Trình điều khiển SQLite (đến)

Thực hiện giao dịch (sử dụng RoomDatabase)

val database: RoomDatabase = ...
database.useWriterConnection { transactor ->
  transactor.immediateTransaction {
    // perform database operations in transaction
  }
}

Thực hiện giao dịch (sử dụng SQLiteConnection)

val connection: SQLiteConnection = ...
connection.execSQL("BEGIN IMMEDIATE TRANSACTION")
try {
  // perform database operations in transaction
  connection.execSQL("END TRANSACTION")
} catch(t: Throwable) {
  connection.execSQL("ROLLBACK TRANSACTION")
}

Bạn cũng cần di chuyển nhiều chế độ ghi đè lệnh gọi lại sang các trình điều khiển tương ứng:

Hỗ trợ SQLite (từ)

Lớp con trong quá trình di chuyển

object Migration_1_2 : Migration(1, 2) {
  override fun migrate(db: SupportSQLiteDatabase) {
    // ...
  }
}

Lớp con thông số kỹ thuật tự động di chuyển

class AutoMigrationSpec_1_2 : AutoMigrationSpec {
  override fun onPostMigrate(db: SupportSQLiteDatabase) {
    // ...
  }
}

Lớp con của lệnh gọi lại cơ sở dữ liệu

object MyRoomCallback : RoomDatabase.Callback {
  override fun onCreate(db: SupportSQLiteDatabase) {
    // ...
  }

  override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
    // ...
  }

  override fun onOpen(db: SupportSQLiteDatabase) {
    // ...
  }
}

Trình điều khiển SQLite (đến)

Lớp con trong quá trình di chuyển

object Migration_1_2 : Migration(1, 2) {
  override fun migrate(connection: SQLiteConnection) {
    // ...
  }
}

Lớp con thông số kỹ thuật tự động di chuyển

class AutoMigrationSpec_1_2 : AutoMigrationSpec {
  override fun onPostMigrate(connection: SQLiteConnection) {
    // ...
  }
}

Lớp con của lệnh gọi lại cơ sở dữ liệu

object MyRoomCallback : RoomDatabase.Callback {
  override fun onCreate(connection: SQLiteConnection) {
    // ...
  }

  override fun onDestructiveMigration(connection: SQLiteConnection) {
    // ...
  }

  override fun onOpen(connection: SQLiteConnection) {
    // ...
  }
}

Tóm lại, hãy thay thế các cách sử dụng SQLiteDatabase bằng SQLiteConnection khi một RoomDatabase không có sẵn, chẳng hạn như trong ghi đè lệnh gọi lại (onMigrate, onCreate, v.v.). Nếu có RoomDatabase, hãy truy cập vào cơ sở kết nối cơ sở dữ liệu bằng cách sử dụng RoomDatabase.useReaderConnectionRoomDatabase.useWriterConnection thay vì RoomDatabase.openHelper.writtableDatabase.

Chuyển đổi các hàm DAO chặn thành tạm ngưng hàm tạm ngưng

Phiên bản KMP của Room dựa vào coroutine để thực hiện I/O thao tác trên CoroutineContext đã định cấu hình. Điều này có nghĩa là bạn cần di chuyển mọi hàm DAO chặn để tạm ngưng các hàm đó.

Chặn hàm DAO (từ)

@Query("SELECT * FROM Todo")
fun getAllTodos(): List<Todo>

Hàm tạm ngưng DAO (to)

@Query("SELECT * FROM Todo")
suspend fun getAllTodos(): List<Todo>

Bạn có thể di chuyển các hàm chặn DAO hiện có sang các hàm tạm ngưng phức tạp nếu cơ sở mã hiện tại chưa tích hợp coroutine. Tham khảo Coroutine trong Android để bắt đầu sử dụng coroutine trong cơ sở mã của bạn.

Chuyển đổi các loại dữ liệu trả về phản ứng thành Flow

Không phải hàm DAO nào cũng cần phải là hàm tạm ngưng. Các hàm DAO trả về không nên chuyển đổi các loại phản ứng như LiveData hoặc Flowable của RxJava để tạm ngưng các hàm. Tuy nhiên, một số loại, chẳng hạn như LiveData không phải là KMP tương thích. Các hàm DAO có loại dữ liệu trả về phản ứng phải được di chuyển sang luồng coroutine.

Loại KMP không tương thích (từ)

@Query("SELECT * FROM Todo")
fun getTodosLiveData(): LiveData<List<Todo>>

Loại KMP tương thích (đến)

@Query("SELECT * FROM Todo")
fun getTodosFlow(): Flow<List<Todo>>

Hãy tham khảo bài viết Flow trong Android để bắt đầu sử dụng Flow trong cơ sở mã.

Thiết lập ngữ cảnh coroutine (Không bắt buộc)

Bạn có thể tuỳ ý định cấu hình RoomDatabase bằng ứng dụng dùng chung bộ thực thi sử dụng RoomDatabase.Builder.setQueryExecutor() để thực hiện cơ sở dữ liệu các toán tử. Vì người thực thi không tương thích với KMP, setQueryExecutor() của Room API không có sẵn cho các nguồn phổ biến. Thay vào đó, RoomDatabase phải được định cấu hình bằng CoroutineContext. Ngữ cảnh có thể được đặt bằng RoomDatabase.Builder.setCoroutineContext(), nếu bạn không đặt giá trị nào thì hàm Theo mặc định, RoomDatabase sẽ sử dụng Dispatchers.IO.

Đặt trình điều khiển SQLite

Sau khi các trường hợp sử dụng SQLite hỗ trợ được di chuyển sang API trình điều khiển SQLite, trình điều khiển phải được định cấu hình bằng RoomDatabase.Builder.setDriver. Chiến lược phát hành đĩa đơn trình điều khiển được đề xuất là BundledSQLiteDriver. Xem bài viết Triển khai trình điều khiển để mô tả về các trình điều khiển hiện có.

SupportSQLiteOpenHelper.Factory tuỳ chỉnh được định cấu hình bằng RoomDatabase.Builder.openHelperFactory() không được hỗ trợ trong KMP, các tính năng do trình trợ giúp mở tuỳ chỉnh cung cấp sẽ cần được triển khai lại bằng Giao diện Trình điều khiển SQLite.

Di chuyển nội dung khai báo về phòng

Sau khi hoàn tất hầu hết các bước di chuyển, người dùng có thể di chuyển Room định nghĩa cho một nhóm tài nguyên chung. Xin lưu ý rằng các chiến lược expect / actual có thể được dùng để di chuyển dần các định nghĩa liên quan đến Room. Ví dụ: nếu không phải tất cả có thể di chuyển các hàm DAO chặn sang các hàm tạm ngưng, có thể khai báo giao diện có chú giải expect @Dao trống trong mã chung, nhưng chứa các hàm chặn trong Android.

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

@Database(entities = [TodoEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
  abstract fun getDao(): TodoDao
  abstract fun getBlockingDao(): BlockingTodoDao
}

@Dao
interface TodoDao {
    @Query("SELECT count(*) FROM TodoEntity")
    suspend fun count(): Int
}

@Dao
expect interface BlockingTodoDao
// shared/src/androidMain/kotlin/BlockingTodoDao.kt

@Dao
actual interface BlockingTodoDao {
    @Query("SELECT count(*) FROM TodoEntity")
    fun count(): Int
}