ย้ายข้อมูลห้องแชทไปยัง Kotlin Multiplatform

เอกสารนี้จะอธิบายวิธีย้ายข้อมูลการใช้งานห้องแชทที่มีอยู่ไปยัง ที่ใช้ Kotlin Multiplatform (KMP)

ย้ายข้อมูลการใช้ห้องใน Codebase ของ Android ที่มีอยู่ไปยัง KMP ที่ใช้ร่วมกันทั่วไป โมดูลอาจมีความยากแตกต่างกันไปขึ้นอยู่กับ API ของห้องที่ใช้หรือหาก โค้ดเบสใช้ Coroutines อยู่แล้ว ส่วนนี้จะมีคำแนะนำและเคล็ดลับบางส่วน เมื่อพยายามย้ายข้อมูลการใช้งาน Room ไปยังโมดูลทั่วไป

คุณจำเป็นต้องทำความคุ้นเคยกับความแตกต่างและการขาดข้อมูลดังกล่าวก่อน ฟีเจอร์ระหว่าง Room เวอร์ชัน Android และเวอร์ชัน KMP เกี่ยวกับการตั้งค่าที่เกี่ยวข้อง โดยพื้นฐานแล้ว การย้ายข้อมูลที่ประสบความสำเร็จจะต้องมีการเปลี่ยนโครงสร้างภายในโค้ด การใช้งาน API ของ SupportSQLite* และแทนที่ด้วย SQLite Driver API พร้อมกับการย้ายการประกาศห้อง (คลาสที่มีคำอธิบายประกอบ @Database รายการ, DAO เอนทิตี และอื่นๆ) เป็นโค้ดทั่วไป

โปรดทบทวนข้อมูลต่อไปนี้ก่อนดำเนินการต่อ

ส่วนถัดไปจะอธิบายขั้นตอนต่างๆ ที่จำเป็นต่อการประสบความสำเร็จ การย้ายข้อมูล

ย้ายข้อมูลจากการรองรับ SQLite ไปยังไดรเวอร์ SQLite

API ใน androidx.sqlite.db ใช้ได้กับ Android เท่านั้นและต้องมีการใช้งาน เปลี่ยนโครงสร้างภายในโค้ดด้วย SQLite Driver API เพื่อความเข้ากันได้แบบย้อนหลังและ RoomDatabase ได้รับการกําหนดค่าด้วย SupportSQLiteOpenHelper.Factory (เช่น ไม่ได้ตั้งค่า SQLiteDriver) แสดงว่าห้องทำงานใน "โหมดความเข้ากันได้" ที่ไหน ทั้ง Support SQLite และ SQLite Driver API ทำงานได้ตามที่คาดไว้ วิธีนี้ช่วยให้ การย้ายข้อมูลที่เพิ่มขึ้นเพื่อที่คุณจะได้ไม่ต้องแปลง SQLite ที่สนับสนุนทั้งหมด กับ SQLite Driver จากการเปลี่ยนแปลงเพียงครั้งเดียว

ตัวอย่างต่อไปนี้คือการใช้งานทั่วไปของการสนับสนุน SQLite และ SQLite ฝั่งคนขับ:

รองรับ SQLite (จาก)

ดำเนินการค้นหาที่ไม่มีผลลัพธ์

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

ดำเนินการค้นหาที่มีผลลัพธ์แต่ไม่มีอาร์กิวเมนต์

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

ดำเนินการค้นหาด้วยผลลัพธ์และอาร์กิวเมนต์

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

ไดรเวอร์ SQLite (ไปยัง)

ดำเนินการค้นหาที่ไม่มีผลลัพธ์

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

ดำเนินการค้นหาที่มีผลลัพธ์แต่ไม่มีอาร์กิวเมนต์

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

ดำเนินการค้นหาด้วยผลลัพธ์และอาร์กิวเมนต์

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

API ธุรกรรมฐานข้อมูลจะใช้งานได้โดยตรงใน SupportSQLiteDatabase ด้วย beginTransaction(), setTransactionSuccessful() และ endTransaction() และยังใช้งานผ่านห้องแชทโดยใช้ runInTransaction() ได้ด้วย ย้ายข้อมูลเหล่านี้ กับ SQLite Driver API

รองรับ SQLite (จาก)

ทำธุรกรรม (โดยใช้ RoomDatabase)

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

ทำธุรกรรม (โดยใช้ SupportSQLiteDatabase)

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

ไดรเวอร์ SQLite (ไปยัง)

ทำธุรกรรม (โดยใช้ RoomDatabase)

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

ทำธุรกรรม (โดยใช้ 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")
}

คุณจะต้องย้ายข้อมูลการลบล้าง Callback ต่างๆ ไปยังฝั่งคนขับด้วย

รองรับ SQLite (จาก)

คลาสย่อยการย้ายข้อมูล

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

คลาสย่อยของข้อกำหนดการย้ายข้อมูลอัตโนมัติ

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

คลาสย่อย Callback ของฐานข้อมูล

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

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

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

ไดรเวอร์ SQLite (ไปยัง)

คลาสย่อยการย้ายข้อมูล

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

คลาสย่อยของข้อกำหนดการย้ายข้อมูลอัตโนมัติ

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

คลาสย่อย Callback ของฐานข้อมูล

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

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

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

สรุปคือแทนที่การใช้งาน SQLiteDatabase ด้วย SQLiteConnection เมื่อ RoomDatabase ไม่สามารถใช้ได้ เช่น ในการลบล้าง Callback (onMigrate, onCreate ฯลฯ) หากมี RoomDatabase ให้ใช้งาน ให้เข้าถึงไฟล์ที่เกี่ยวข้อง การเชื่อมต่อฐานข้อมูลโดยใช้ RoomDatabase.useReaderConnection และ RoomDatabase.useWriterConnection แทนที่จะเป็น RoomDatabase.openHelper.writtableDatabase

แปลงฟังก์ชัน DAO การบล็อกเป็นการระงับฟังก์ชัน

Room เวอร์ชัน KMP ใช้โครูทีนในการดำเนินการ I/O ใน CoroutineContext ที่กำหนดค่าไว้ ซึ่งหมายความว่าคุณ ต้องย้ายข้อมูลฟังก์ชัน DAO ที่บล็อกอยู่ไปยังการระงับฟังก์ชัน

การบล็อกฟังก์ชัน DAO (จาก)

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

กำลังระงับฟังก์ชัน DAO (ถึง)

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

สามารถย้ายข้อมูลฟังก์ชันการบล็อก DAO ที่มีอยู่ไปยังฟังก์ชันระงับ ซึ่งจะซับซ้อนหากโค้ดเบสที่มีอยู่ยังไม่ได้รวมโครูทีน โปรดดูCoroutines ใน Android เพื่อเริ่มใช้โครูทีน ในฐานของโค้ด

แปลงประเภทผลตอบแทนเชิงรับเป็นโฟลว์

ฟังก์ชัน DAO บางรายการไม่จำเป็นต้องระงับฟังก์ชัน ฟังก์ชัน DAO ที่แสดงผล ไม่ควรแปลงประเภทเชิงรับ เช่น LiveData หรือ Flowable ของ RxJava เพื่อระงับฟังก์ชัน อย่างไรก็ตาม บางประเภท เช่น LiveData ไม่ใช่ KMP ที่เข้ากันได้ ต้องย้ายข้อมูลฟังก์ชัน DAO ที่มีประเภทผลลัพธ์เชิงรับไปยัง โครูทีน

ประเภท KMP ที่ใช้ร่วมกันไม่ได้ (จาก)

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

ประเภท KMP ที่เข้ากันได้ (ถึง)

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

โปรดดูโฟลว์ใน Android เพื่อเริ่มต้นใช้งาน Flow ใน ฐานของโค้ด

ตั้งค่าบริบท Coroutine (ไม่บังคับ)

คุณเลือกกำหนดค่า RoomDatabase ด้วยแอปพลิเคชันที่แชร์ได้ ผู้ดำเนินการที่ใช้ RoomDatabase.Builder.setQueryExecutor() เพื่อทำฐานข้อมูล การดำเนินงาน เนื่องจากผู้ดำเนินการใช้ไม่ได้กับ KMP setQueryExecutor() ของห้อง API ไม่พร้อมใช้งานสำหรับแหล่งที่มาทั่วไป แต่ RoomDatabase จะต้อง กำหนดค่าด้วย CoroutineContext ตั้งค่าบริบทได้โดยใช้ RoomDatabase.Builder.setCoroutineContext() หากไม่ได้ตั้งค่าไว้ พารามิเตอร์ RoomDatabase จะมีค่าเริ่มต้นเป็น Dispatchers.IO

ตั้งค่าไดรเวอร์ SQLite

เมื่อย้ายการใช้งาน SQLite สำหรับการสนับสนุนไปยัง SQLite Driver API แล้ว ต้องกำหนดค่าไดรเวอร์โดยใช้ RoomDatabase.Builder.setDriver คนขับที่แนะนำคือ BundledSQLiteDriver โปรดดูการใช้งานไดรเวอร์สำหรับ รายละเอียดของการติดตั้งใช้งานไดรเวอร์ที่ใช้ได้

SupportSQLiteOpenHelper.Factory ที่กำหนดเองซึ่งกำหนดค่าโดยใช้ RoomDatabase.Builder.openHelperFactory()ไม่รองรับใน KMP ฟีเจอร์ที่ได้รับการจัดเตรียมโดยตัวช่วยแบบเปิดที่กำหนดเองจะต้องมีการนำไปใช้งานอีกครั้ง อินเทอร์เฟซของไดรเวอร์ SQLite

การประกาศการย้ายห้อง

เมื่อขั้นตอนการย้ายข้อมูลส่วนใหญ่เสร็จสิ้นแล้ว ผู้ใช้ก็จะย้ายห้องแชทได้ ของชุดแหล่งข้อมูลทั่วไป โปรดทราบว่ากลยุทธ์ expect / actual รายการช่วยให้คุณทำสิ่งต่อไปนี้ได้ เพื่อค่อยๆ ย้ายคำจำกัดความที่เกี่ยวข้องกับห้อง เช่น หากไม่ทั้งหมด สามารถย้ายข้อมูลฟังก์ชัน DAO ที่บล็อกไปยังฟังก์ชัน ทำให้สามารถ ประกาศอินเทอร์เฟซที่มีคำอธิบายประกอบ expect @Dao ซึ่งว่างเปล่าในโค้ดทั่วไป มีฟังก์ชันการบล็อกใน 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
}