ย้ายข้อมูลจาก Room 2.x ไปยัง Room 3.0

Room 3.0 เป็นการอัปเดตเวอร์ชันหลักที่เปลี่ยนไลบรารีให้เป็น Kotlin-first โดยรองรับ Kotlin Multiplatform (KMP), ต้องใช้ Kotlin Symbol Processing (KSP) และบังคับใช้ Coroutine สำหรับการดำเนินการแบบอะซิงโครนัส

Room 3.0 อยู่ในแพ็กเกจใหม่ androidx.room3 เพื่อป้องกันปัญหาความเข้ากันได้กับแอป Room 2.x ที่มีอยู่และการขึ้นต่อกันแบบถ่ายทอด

คู่มือนี้จะอธิบายขั้นตอนที่จำเป็นในการย้ายข้อมูลการติดตั้งใช้งาน Room 2.x ที่มีอยู่ไปยัง Room 3.0

การเปลี่ยนแปลงที่สำคัญใน Room 3.0

ก่อนเริ่มการย้ายข้อมูล โปรดทำความคุ้นเคยกับความแตกต่างที่สำคัญดังนี้

  • แพ็กเกจและอาร์ติแฟกต์ใหม่: คลาสทั้งหมดอยู่ใน androidx.room3 อาร์ติแฟกต์ใช้คำนำหน้า room3 เช่น androidx.room3:room3-runtime
  • Kotlin และ KSP เท่านั้น: Room 3.0 ไม่รองรับการสร้างโค้ด Java ให้ใช้ KSP แทน KAPT หรือโปรเซสเซอร์คำอธิบายประกอบ Java Room 3.0 ยังคงรองรับแหล่งที่มาของ Java เป็นอินพุต
  • Coroutine เป็นอันดับแรก: ฟังก์ชัน DAO ต้องเป็นฟังก์ชัน suspend ยกเว้นประเภทที่ สังเกตได้ CoroutineContext จะแทนที่ Executor
  • ไม่รองรับ SQLite: SQLiteDriver API จะสำรองข้อมูล Room Room จะนำ SupportSQLiteDatabase ออกจาก API หลัก
  • การเปลี่ยนแปลง API: การย้ายข้อมูลและการเรียกกลับของฐานข้อมูลใช้ SQLiteConnection แทน SupportSQLiteDatabase
  • ตัวแปลงประเภทสำหรับประเภทรีแอกทีฟ: ประเภทการคืนค่า RxJava, LiveData, Guava และ Paging กำหนดให้คุณต้องลงทะเบียน @DaoReturnTypeConverters

เราขอแนะนำให้ย้ายข้อมูลเป็น 2 ระยะที่แตกต่างกัน โดยระยะแรกคือการเตรียมและปรับฐานของโค้ดให้ทันสมัยใน Room 2.x จากนั้นจึงเปลี่ยนไปใช้ Room 3.0


เตรียมและปรับให้ทันสมัยใน Room 2.x

ก่อนย้ายข้อมูลไปยัง Room 3.0 คุณสามารถทำงานปรับให้ทันสมัยส่วนใหญ่ได้โดยการอัปเดตเป็น Room 2.x เวอร์ชันปัจจุบัน เช่น Room 2.8 Room 2.8 รองรับ Kotlin Multiplatform หรือ KMP และมี API ของไดรเวอร์มากมายที่ Room 3.0 ใช้

อัปเดตเป็น Room 2.8 ขึ้นไป

อัปเดตการกำหนดค่าบิลด์เพื่อใช้ Room 2.x เวอร์ชันปัจจุบัน

[versions]
room2 = "2.8.4" # Use the current Room 2.8 version

[libraries]
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room2" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room2" }

ย้ายข้อมูลจาก KAPT ไปยัง KSP

Room 3.0 ไม่รองรับโปรเซสเซอร์คำอธิบายประกอบ Java หรือ KAPT คุณต้องใช้ Kotlin Symbol Processing (KSP) คุณสามารถทำการเปลี่ยนนี้ในขณะที่ยังใช้ Room 2.x อยู่

  1. ใน build.gradle.kts ของโมดูล ให้ใช้ปลั๊กอิน KSP ดังนี้

    plugins {
        id("com.google.devtools.ksp") version "<ksp_version>"
    }
    

    ตรวจสอบว่าเวอร์ชัน KSP เข้ากันได้กับเวอร์ชัน Kotlin

  2. แทนที่ kapt หรือ annotationProcessor ด้วย ksp สำหรับทรัพยากร Dependency ของคอมไพเลอร์ Room ดังนี้

    dependencies {
        implementation(libs.androidx.room.runtime)
        ksp(libs.androidx.room.compiler)
    }
    

ใช้ Coroutine

Room 3.0 กำหนดให้ต้องใช้ Coroutine สำหรับการดำเนินการแบบอะซิงโครนัส

  • อัปเดต DAO: ฟังก์ชัน DAO ทั้งหมดต้องเป็นฟังก์ชัน suspend ยกเว้นฟังก์ชันที่แสดงผลประเภทรีแอกทีฟที่สังเกตได้ เช่น ประเภท Flow หรือ RxJava
// Before (Blocking)
@Dao
interface UserDao {
    @Query("SELECT * FROM User")
    fun getAll(): List<User>
}

// After (Suspend)
@Dao
interface UserDao {
    @Query("SELECT * FROM User")
    suspend fun getAll(): List<User>
}
  • หากคุณกำหนดค่า RoomDatabase ด้วย Executor ที่กำหนดเองเพื่อดำเนินการ ฐานข้อมูล ให้ย้ายข้อมูลไปยัง CoroutineContext โดยใช้ setQueryCoroutineContext ใน Builder ดังนี้
Room.databaseBuilder<AppDatabase>(context, "db")
    .setQueryCoroutineContext(Dispatchers.IO)
    .build()

ใช้ API ของไดรเวอร์และหลีกเลี่ยง Support SQLite

Room 3.0 ได้รับการสนับสนุนอย่างเต็มที่จาก SQLiteDriver และไม่รองรับ SupportSQLiteDatabase ใน API หลักอีกต่อไป

หากคุณไม่ได้เรียก setDriver เพื่อตั้งค่า SQLiteDriver ใน Builder ของฐานข้อมูล Room 2.8 จะทำงานในโหมดความเข้ากันได้ที่ทั้ง Support SQLite และ API ของไดรเวอร์ทำงานได้ โหมดความเข้ากันได้นี้ช่วยให้คุณแปลงฐานของโค้ดได้ทีละน้อยก่อนที่จะเปิดใช้ไดรเวอร์

  • แปลงการย้ายข้อมูล: ย้ายข้อมูล Migration และ AutoMigrationSpec คลาสย่อยเพื่อใช้ SQLiteConnection แทน SupportSQLiteDatabase
// Before (SupportSQLiteDatabase)
val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(db: SupportSQLiteDatabase) {
        db.execSQL("ALTER TABLE User ADD COLUMN age INTEGER DEFAULT 0 NOT NULL")
    }
}

// After (SQLiteConnection - Room 2.8)
import androidx.sqlite.SQLiteConnection
import androidx.sqlite.execSQL

val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(connection: SQLiteConnection) {
        connection.execSQL(
            "ALTER TABLE User ADD COLUMN age INTEGER DEFAULT 0 NOT NULL"
        )
    }
}
  • แปลงการเรียกกลับของฐานข้อมูล: อัปเดต RoomDatabase.Callback การติดตั้งใช้งานเพื่อใช้ SQLiteConnection ดังนี้
// Before (SupportSQLiteDatabase)
val callback = object : RoomDatabase.Callback() {
    override fun onCreate(db: SupportSQLiteDatabase) {
        // ...
    }
}

// After (SQLiteConnection - Room 2.8)
val callback = object : RoomDatabase.Callback() {
    override fun onCreate(connection: SQLiteConnection) {
        // ...
    }
}
  • แปลงฟังก์ชัน DAO ของ @RawQuery: สำหรับฟังก์ชันที่มีคำอธิบายประกอบด้วย @RawQuery ให้ใช้ RoomRawQuery แทน SupportSQLiteQuery ดังนี้
// Before (SupportSQLiteQuery)
@Dao
interface UserDao {
    @RawQuery
    fun getUser(query: SupportSQLiteQuery): User
}

// After (RoomRawQuery)
@Dao
interface UserDao {
    @RawQuery
    suspend fun getUser(query: RoomRawQuery): User
}

คุณสามารถสร้าง RoomRawQuery ได้ในรันไทม์ ดังนี้

val query = RoomRawQuery(
    sql = "SELECT * FROM User WHERE id = ?",
    onBindStatement = { statement ->
        statement.bindInt(1, userId)
    }
)
  • แปลง API ของธุรกรรม: แทนที่บล็อก withTransaction และ runInTransaction ที่ใช้ได้เฉพาะใน Android ด้วย withWriteTransaction หรือ withReadTransaction ดังนี้
// Before (withTransaction)
db.withTransaction {
    // perform database operations
}

// After (withWriteTransaction - Room 2.8)
import androidx.room.withWriteTransaction

db.withWriteTransaction {
    // perform database operations
}

หากต้องการเข้าถึงการเชื่อมต่อธุรกรรมระดับต่ำโดยตรง คุณยังใช้ useWriterConnection กับ immediateTransaction ได้ด้วย

  • หลีกเลี่ยงการใช้ SupportSQLiteDatabase โดยตรง: หากคุณมีโค้ดเดิมจำนวนมากที่ยังต้องใช้ SupportSQLiteDatabase และยังย้ายข้อมูลไม่ได้ ให้ใช้อาร์ติแฟกต์ความเข้ากันได้ androidx.room:room-sqlite-wrapper ดังนี้
dependencies {
    implementation("androidx.room:room-sqlite-wrapper:$roomVersion")
}

จากนั้นใช้ getSupportWrapper เพื่อรับ SupportSQLiteDatabase จากอินสแตนซ์ฐานข้อมูล Room ดังนี้

import androidx.room.support.getSupportWrapper

val legacyDb: SupportSQLiteDatabase = roomDatabase.getSupportWrapper()
  • ตั้งค่าไดรเวอร์ SQLite: หลังจากย้ายข้อมูลการใช้งาน Room API ทั้งหมดไปยัง API ของไดรเวอร์แล้ว ให้กำหนดค่าไดรเวอร์ เช่น BundledSQLiteDriver หรือ AndroidSQLiteDriver โดยเรียก setDriver ใน Builder ของ RoomDatabase ดังนี้
import androidx.sqlite.driver.bundled.BundledSQLiteDriver

val db = Room.databaseBuilder<AppDatabase>(context, "db")
    .setDriver(BundledSQLiteDriver())
    .build()

ใช้การติดตามการระบุว่าไม่ถูกต้องตาม Flow

Room 2.8 ขอแนะนำ API InvalidationTracker.createFlow ใช้ API นี้เพื่อย้ายข้อมูลจากการติดตั้งใช้งาน InvalidationTracker.Observer เดิมในขณะที่ยังใช้ Room 2.x อยู่ ซึ่งจะเตรียมโค้ดเบสสำหรับ Room 3.0 ที่นำ Observer ออกอย่างสมบูรณ์

// Before (InvalidationTracker.Observer)
val observer = object : InvalidationTracker.Observer("User") {
    override fun onInvalidated(tables: Set<String>) {
        // reload user data
    }
}
db.invalidationTracker.addObserver(observer)

// After (createFlow - Room 2.8)
val userFlow = db.invalidationTracker.createFlow("User").map { _ ->
    userDao.getAllUsers()
}

ย้ายข้อมูลไปยัง Room 3.0

เมื่อปรับแอปพลิเคชันให้ทันสมัยใน Room 2.x แล้ว การเปลี่ยนไปใช้ Room 3.0 จะเกี่ยวข้องกับการอัปเดตการขึ้นต่อกัน การนำเข้าแพ็กเกจ และการเรียกกลับของฐานข้อมูล

อัปเดตการขึ้นต่อกันและการนำเข้าแพ็กเกจ

  • ในการกำหนดค่าบิลด์ ให้แทนที่การขึ้นต่อกัน androidx.room ด้วย androidx.room3 ดังนี้
[versions]
room3 = "3.0.0" # Use the current Room 3.0 version

[libraries]
androidx-room3-runtime = { module = "androidx.room3:room3-runtime", version.ref = "room3" }
androidx-room3-compiler = { module = "androidx.room3:room3-compiler", version.ref = "room3" }
  • อัปเดตบล็อกการขึ้นต่อกัน ดังนี้
dependencies {
    implementation(libs.androidx.room3.runtime)
    ksp(libs.androidx.room3.compiler)
}
  • อัปเดตการนำเข้าแพ็กเกจ แทนที่ import androidx.room.* ด้วย import androidx.room3.*

อัปเดต API ของตัวแปลงประเภท

Room 3.0 เปลี่ยนชื่อ API ของตัวแปลงประเภทเพื่อชี้แจงการใช้งานสำหรับการแปลงค่าคอลัมน์และเพื่อหลีกเลี่ยงความสับสนกับตัวแปลงประเภทการคืนค่า DAO

อัปเดตคำอธิบายประกอบและฟังก์ชันต่อไปนี้ในฐานของโค้ด

  • เปลี่ยนชื่อ @TypeConverter เป็น @ColumnTypeConverter
  • เปลี่ยนชื่อ @TypeConverters เป็น @ColumnTypeConverters
  • เปลี่ยนชื่อ @ProvidedTypeConverter เป็น @ProvidedColumnTypeConverter
  • เปลี่ยนชื่อ RoomDatabase.Builder.addTypeConverter เป็น addColumnTypeConverter

ตัวอย่าง

// Before
@ProvidedTypeConverter
class Converters {
  @TypeConverter
  fun fromTimestamp(value: Long?): Date? = ...
}

@Database(entities = [User::class], version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase()

val db = Room.databaseBuilder<AppDatabase>(...)
  .addTypeConverter(convertersInstance)
  .build()
// After
import androidx.room3.ColumnTypeConverter
import androidx.room3.ColumnTypeConverters
import androidx.room3.ProvidedColumnTypeConverter

@ProvidedColumnTypeConverter
class Converters {
  @ColumnTypeConverter
  fun fromTimestamp(value: Long?): Date? = ...
}

@Database(entities = [User::class], version = 1)
@ColumnTypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase()

val db = Room.databaseBuilder<AppDatabase>(...)
  .addColumnTypeConverter(convertersInstance)
  .build()

อัปเดตการเรียกกลับเป็นฟังก์ชัน suspend

ใน Room 3.0 การเรียกกลับของฐานข้อมูลและการย้ายข้อมูลใช้ SQLiteConnection และเป็นฟังก์ชัน suspend

  • อัปเดตคลาส Migration ด้วยตนเอง ดังนี้
import androidx.sqlite.SQLiteConnection
import androidx.sqlite.async.executeSQL

val MIGRATION_1_2 = object : Migration(1, 2) {
    override suspend fun migrate(connection: SQLiteConnection) {
        connection.executeSQL(
            "ALTER TABLE User ADD COLUMN age INTEGER DEFAULT 0 NOT NULL"
        )
    }
}
  • อัปเดตการติดตั้งใช้งาน RoomDatabase.Callback ดังนี้
val callback = object : RoomDatabase.Callback() {
    override suspend fun onCreate(connection: SQLiteConnection) {
        // ...
    }
}

ลงทะเบียนตัวแปลงประเภทการแสดงผล DAO

ใน Room 3.0 ประเภทการคืนค่ารีแอกทีฟ เช่น RxJava, LiveData, Guava และ เพจจิ้ง กำหนดให้คุณต้องลงทะเบียนตัวแปลงประเภทการแสดงผล DAO โดยใช้ @DaoReturnTypeConverters

import androidx.room3.paging.PagingSourceDaoReturnTypeConverter

@Dao
@DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class)
interface UserDao {
    @Query("SELECT * FROM User")
    fun getAllPaginated(): PagingSource<Int, User>
}
  • เพจจิ้ง (PagingSource): รีจิสเตอร์ PagingSourceDaoReturnTypeConverter จากอาร์ติแฟกต์ androidx.room3:room3-paging
  • RxJava (Observable, Flowable, Single, Maybe, Completable): ลงทะเบียน RxDaoReturnTypeConverters จากอาร์ติแฟกต์ androidx.room3:room3-rxjava3
  • Guava (ListenableFuture): ลงทะเบียน GuavaDaoReturnTypeConverter จากอาร์ติแฟกต์ androidx.room3:room3-guava
  • LiveData (LiveData): ลงทะเบียน LiveDataDaoReturnTypeConverter จากอาร์ติแฟกต์ androidx.room3:room3-livedata

ยืนยันการนำ InvalidationTracker Observer ออก

Room 3.0 นำ InvalidationTracker.Observer และเมธอดการลงทะเบียนที่เกี่ยวข้อง เช่น addObserver และ removeObserver ออกอย่างสมบูรณ์

หากคุณยังไม่ได้เปลี่ยนไปใช้ Flow ของ Coroutine ใน ระยะที่ 1 คุณต้องย้ายข้อมูลการใช้งาน Observer ทั้งหมดไปยัง createFlow ดังนี้

val userFlow = db.invalidationTracker.createFlow("User").map { _ ->
    userDao.getAllUsers()
}