Di chuyển Room sang Multiplaform Kotlin

Tài liệu này mô tả cách di chuyển phương thức triển khai Room hiện có sang một phương thức sử dụng Kotlin Multiplatform (KMP).

Việc di chuyển các trường hợp sử dụng Room trong cơ sở mã Android hiện có sang mô-đun KMP dùng chung phổ biến có thể khác nhau 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 hay chưa. Phần này cung cấp một số hướng dẫn và mẹo khi cố gắng di chuyển các trường hợp sử dụng Room sang một mô-đun chung.

Trước tiên, bạn cần làm quen với sự khác biệt và thiếu các tính năng 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ề cơ bản, quá trình di chuyển thành công bao gồm việc tái cấu trúc các trường hợp 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 nội dung khai báo Room (lớp, DAO, thực thể @Database, v.v.) thành mã chung.

Xem 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 cần thiết để di chuyển thành công.

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

Các API trong androidx.sqlite.db chỉ dành cho Android và mọi trường hợp sử dụng đều cần đượ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 một SupportSQLiteOpenHelper.Factory (tức là không đặt SQLiteDriver), thì Room sẽ hoạt động ở "chế độ tương thích", trong đó cả API Trình điều khiển hỗ trợ SQLite và SQLite đều hoạt động như mong đợi. Nhờ vậy, bạn sẽ gia tăng được quá trình di chuyển, nhờ đó, bạn không cần phải chuyển đổi tất cả các trường hợp sử dụng hỗ trợ SQLite sang Trình điều khiển SQLite trong một thay đổi duy nhất.

Sau đây là ví dụ về cách sử dụng phổ biến của API Hỗ trợ SQLite và các phiên bản tương ứng của Trình điều khiển SQLite:

Hỗ trợ SQLite (từ)

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

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

Thực thi một 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 bằng cá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 hiện truy vấn mà không có kết quả

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

Thực thi một 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 bằng cá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ủa cơ sở dữ liệu có sẵn trực tiếp trong SupportSQLiteDatabase cùng với beginTransaction(), setTransactionSuccessful()endTransaction(). Bạn cũng có thể dùng runInTransaction() thông qua Room. Di chuyển các trường hợp sử dụng này sang 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 trình điều khiển tương ứng:

Hỗ trợ SQLite (từ)

Lớp con 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 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 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 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 của SQLiteDatabase bằng SQLiteConnection khi không có RoomDatabase, chẳng hạn như trong các chế độ ghi đè lệnh gọi lại (onMigrate, onCreate, v.v.). Nếu có RoomDatabase, hãy truy cập kết nối cơ sở dữ liệu cơ bản bằng cách sử dụng RoomDatabase.useReaderConnectionRoomDatabase.useWriterConnection thay vì RoomDatabase.openHelper.writtableDatabase.

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

Phiên bản KMP của Room dựa vào coroutine để thực hiện các thao tác I/O 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>

Tạm ngưng hàm DAO (đến)

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

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

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 là hàm tạm ngưng. Các hàm DAO trả về kiểu 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, có một số kiểu, chẳng hạn như LiveData, không tương thích với KMP. Bạn phải di chuyển các hàm DAO có loại trả về phản ứng 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>>

Tham khảo phần Luồng trong Android để bắt đầu sử dụng Luồng trong cơ sở mã của bạn.

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 với 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 với cơ sở dữ liệu. Vì executor không tương thích với KMP, nên API setQueryExecutor() của Room không có sẵn cho các nguồn phổ biến. Thay vào đó, bạn phải định cấu hình RoomDatabase bằng CoroutineContext. Bạn có thể đặt ngữ cảnh bằng RoomDatabase.Builder.setCoroutineContext(). Nếu bạn không đặt ngữ cảnh nào thì RoomDatabase sẽ mặc định sử dụng Dispatchers.IO.

Thiết lập trình điều khiển SQLite

Sau khi di chuyển các trường hợp sử dụng hỗ trợ SQLite sang API trình điều khiển SQLite, bạn phải định cấu hình trình điều khiển bằng RoomDatabase.Builder.setDriver. Trình điều khiển được đề xuất là BundledSQLiteDriver. Xem nội dung Triển khai trình điều khiển để biết nội dung mô tả các cách triển khai 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. Bạn cần triển khai lại các tính năng do trình trợ giúp mở tuỳ chỉnh cung cấp 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 các định nghĩa Room sang một nhóm tài nguyên chung. Xin lưu ý rằng bạn có thể dùng các chiến lược expect / actual để 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ác hàm DAO chặn đều có thể di chuyển sang các hàm tạm ngưng, bạn 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
}