ย้ายข้อมูลฐานข้อมูลห้อง

เมื่อคุณเพิ่มและเปลี่ยนฟีเจอร์ในแอป คุณต้องแก้ไขคลาสเอนทิตี Room และตารางฐานข้อมูลที่เกี่ยวข้องเพื่อให้แสดงการเปลี่ยนแปลงเหล่านี้ สิ่งสำคัญคือต้องเก็บรักษาข้อมูลผู้ใช้ที่อยู่ในฐานข้อมูลในอุปกรณ์ไว้เมื่อการอัปเดตแอปเปลี่ยนสคีมาของฐานข้อมูล

Room รองรับทั้งตัวเลือกอัตโนมัติและตัวเลือกด้วยตนเองสำหรับการย้ายข้อมูลแบบเพิ่ม การย้ายข้อมูลอัตโนมัติใช้ได้กับการเปลี่ยนแปลงสคีมาพื้นฐานส่วนใหญ่ แต่คุณอาจต้องกำหนดเส้นทางการย้ายข้อมูลด้วยตนเองสำหรับการเปลี่ยนแปลงที่ซับซ้อนมากขึ้น

การย้ายข้อมูลอัตโนมัติ

หากต้องการประกาศการย้ายข้อมูลอัตโนมัติระหว่างฐานข้อมูล 2 เวอร์ชัน ให้เพิ่ม @AutoMigration ลงใน autoMigrations ใน @Database ดังนี้

// Database class before the version update.
@Database(
  version = 1,
  entities = [User::class]
)
abstract class AppDatabaseV1 : RoomDatabase() {
  abstract fun userDao(): UserDao
}

// Database class after the version update.
@Database(
  version = 2,
  entities = [User::class],
  autoMigrations = [
    AutoMigration(from = 1, to = 2)
  ]
)
abstract class AppDatabaseV2 : RoomDatabase() {
  abstract fun userDao(): UserDao
}

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

หาก Room ตรวจพบการเปลี่ยนแปลงสคีมาที่ไม่ชัดเจนและไม่สามารถสร้างแผนการย้ายข้อมูล ได้หากไม่มีข้อมูลเพิ่มเติม ระบบจะแสดงข้อผิดพลาดในเวลาคอมไพล์ และคุณต้องระบุการใช้งาน AutoMigrationSpec โดยส่วนใหญ่แล้วข้อผิดพลาดนี้จะเกิดขึ้นเมื่อการย้ายข้อมูลเกี่ยวข้องกับสิ่งใดสิ่งหนึ่งต่อไปนี้

  • การลบหรือเปลี่ยนชื่อตาราง
  • การลบหรือเปลี่ยนชื่อคอลัมน์

คุณสามารถใช้ AutoMigrationSpec เพื่อให้ข้อมูลเพิ่มเติมที่ Room ต้องการในการสร้างเส้นทางการย้ายข้อมูลอย่างถูกต้อง กำหนดคลาสที่ใช้ AutoMigrationSpec ในคลาส RoomDatabase และใส่คำอธิบายประกอบด้วยรายการใดรายการหนึ่งต่อไปนี้

หากต้องการใช้การใช้งาน AutoMigrationSpec สำหรับการย้ายข้อมูลอัตโนมัติ ให้ตั้งค่าพร็อพเพอร์ตี้ spec ในคำอธิบายประกอบ @AutoMigration ที่เกี่ยวข้อง

@Database(
  version = 2,
  entities = [User::class],
  autoMigrations = [
    AutoMigration (
      from = 1,
      to = 2,
      spec = MigrationSpec1To2::class
    )
  ]
)
abstract class AppDatabaseWithSpec : RoomDatabase() {
  abstract fun userDao(): UserDao
}

@RenameTable(fromTableName = "User", toTableName = "AppUser")
internal class MigrationSpec1To2 : AutoMigrationSpec

หากแอปต้องทำงานเพิ่มเติมหลังจากที่การย้ายข้อมูลอัตโนมัติเสร็จสมบูรณ์แล้ว คุณ สามารถใช้ onPostMigrate ได้ หากคุณใช้ฟังก์ชันนี้ใน AutoMigrationSpec Room จะเรียกใช้ฟังก์ชันนี้หลังจากที่การย้ายข้อมูลอัตโนมัติเสร็จสมบูรณ์แล้ว

การย้ายข้อมูลด้วยตนเอง

หากการย้ายข้อมูลเกี่ยวข้องกับการเปลี่ยนแปลงสคีมาที่ซับซ้อน Room อาจไม่สามารถสร้างเส้นทางการย้ายข้อมูลที่เหมาะสมโดยอัตโนมัติ เช่น หากคุณตัดสินใจแยกข้อมูลในตารางออกเป็น 2 ตาราง Room จะไม่สามารถระบุวิธีแยกข้อมูลนี้ได้ ในกรณีเช่นนี้ คุณต้องกำหนดเส้นทางการย้ายข้อมูลด้วยตนเองโดยใช้คลาส Migration

คลาส Migration จะกำหนดเส้นทางการย้ายข้อมูลอย่างชัดเจนระหว่าง startVersion กับ endVersion โดยการลบล้างฟังก์ชัน migrate เพิ่มคลาส Migration ลงในตัวสร้างฐานข้อมูลโดยใช้ฟังก์ชัน addMigrations ดังนี้

val MIGRATION_1_2 = object : Migration(1, 2) {
  override suspend fun migrate(connection: SQLiteConnection) {
    connection.executeSQL("CREATE TABLE `Fruit` (`id` INTEGER, `name` TEXT, " +
      "PRIMARY KEY(`id`))")
  }
}

val MIGRATION_2_3 = object : Migration(2, 3) {
  override suspend fun migrate(connection: SQLiteConnection) {
    connection.executeSQL("ALTER TABLE Book ADD COLUMN pub_year INTEGER")
  }
}

Room.databaseBuilder<ManualMigrationDatabase>(applicationContext, "database-name")
  .addMigrations(MIGRATION_1_2, MIGRATION_2_3)
  .build()

เมื่อกำหนดเส้นทางการย้ายข้อมูล คุณสามารถใช้การย้ายข้อมูลอัตโนมัติสำหรับบางเวอร์ชันและการย้ายข้อมูลด้วยตนเองสำหรับเวอร์ชันอื่นๆ หากคุณกำหนดทั้งการย้ายข้อมูลอัตโนมัติและการย้ายข้อมูลด้วยตนเองสำหรับเวอร์ชันเดียวกัน Room จะใช้การย้ายข้อมูลด้วยตนเอง

ทดสอบการย้ายข้อมูล

การย้ายข้อมูลมักจะซับซ้อน และการย้ายข้อมูลที่กำหนดไว้อย่างไม่ถูกต้องอาจทำให้แอปขัดข้อง ทดสอบการย้ายข้อมูลเพื่อรักษาความเสถียรของแอป Room มีอาร์ติแฟกต์ Maven room3-testing เพื่อช่วยในกระบวนการทดสอบสำหรับการย้ายข้อมูลทั้งแบบอัตโนมัติและแบบด้วยตนเอง คุณต้องส่งออกสคีมาของฐานข้อมูลก่อนเพื่อให้ใช้งานอาร์ติแฟกต์นี้ได้

ส่งออกสคีมา

Room จะส่งออกข้อมูลสคีมาของฐานข้อมูลเป็นไฟล์ JSON ในเวลาคอมไพล์ ไฟล์ JSON ที่ส่งออกจะแสดงประวัติสคีมาของฐานข้อมูล จัดเก็บไฟล์เหล่านี้ไว้ในระบบควบคุมเวอร์ชันเพื่อให้คุณสร้างฐานข้อมูลเวอร์ชันที่ต่ำกว่าขึ้นมาใหม่สำหรับการทดสอบและรองรับการสร้างการย้ายข้อมูลอัตโนมัติได้

ตั้งค่าตำแหน่งสคีมาโดยใช้ปลั๊กอิน Room Gradle

หากต้องการระบุไดเรกทอรีสคีมา ให้ใช้ ปลั๊กอิน Room Gradle และใช้ room3 ส่วนขยาย

ดึงดูด

plugins {
  id 'androidx.room3'
}

room3 {
  schemaDirectory "$projectDir/schemas"
}

Kotlin

plugins {
  id("androidx.room3")
}

room3 {
  schemaDirectory("$projectDir/schemas")
}

หากสคีมาของฐานข้อมูลแตกต่างกันไปตามรูปแบบ เวอร์ชัน หรือประเภทบิลด์ คุณต้องระบุตำแหน่งต่างๆ โดยใช้การกำหนดค่า schemaDirectory หลายครั้ง โดยแต่ละครั้งจะมี variantMatchName เป็นอาร์กิวเมนต์แรก การกำหนดค่าแต่ละรายการสามารถจับคู่กับรูปแบบ 1 รายการขึ้นไปโดยอิงตามการเปรียบเทียบอย่างง่ายกับชื่อรูปแบบ

ตรวจสอบว่าการกำหนดค่าเหล่านี้ครอบคลุมและครอบคลุมรูปแบบทั้งหมด นอกจากนี้ คุณยังรวม schemaDirectory() ไว้โดยไม่มี variantMatchName เพื่อจัดการรูปแบบที่การกำหนดค่าอื่นๆ ไม่ได้จับคู่ไว้ได้ด้วย ตัวอย่างเช่น ในแอปที่มีเวอร์ชันบิลด์ 2 เวอร์ชัน ได้แก่ demo และ full และประเภทบิลด์ 2 ประเภท ได้แก่ debug และ release การกำหนดค่าต่อไปนี้เป็นการกำหนดค่าที่ถูกต้อง

ดึงดูด

room3 {
  // Applies to 'demoDebug' only
  schemaDirectory "demoDebug", "$projectDir/schemas/demoDebug"

  // Applies to 'demoDebug' and 'demoRelease'
  schemaDirectory "demo", "$projectDir/schemas/demo"

  // Applies to 'demoDebug' and 'fullDebug'
  schemaDirectory "debug", "$projectDir/schemas/debug"

  // Applies to variants that aren't matched by other configurations.
  schemaDirectory "$projectDir/schemas"
}

Kotlin

room3 {
  // Applies to 'demoDebug' only
  schemaDirectory("demoDebug", "$projectDir/schemas/demoDebug")

  // Applies to 'demoDebug' and 'demoRelease'
  schemaDirectory("demo", "$projectDir/schemas/demo")

  // Applies to 'demoDebug' and 'fullDebug'
  schemaDirectory("debug", "$projectDir/schemas/debug")

  // Applies to variants that aren't matched by other configurations.
  schemaDirectory("$projectDir/schemas")
}

ตั้งค่าตำแหน่งสคีมาโดยใช้ตัวเลือกโปรเซสเซอร์สำหรับคำอธิบายประกอบ

หากไม่ได้ใช้ปลั๊กอิน Room Gradle ให้ตั้งค่าตำแหน่งสคีมาโดยใช้ตัวเลือกโปรเซสเซอร์สำหรับคำอธิบายประกอบ room.schemaLocation

Gradle จะใช้ไฟล์ในไดเรกทอรีนี้เป็นอินพุตและเอาต์พุตสำหรับงาน Gradle บางอย่าง คุณต้องใช้ CommandLineArgumentProvider ของ Gradle เพื่อแจ้งให้ Gradle ทราบเกี่ยวกับ ไดเรกทอรีนี้เพื่อให้บิลด์แบบเพิ่มและบิลด์ที่แคชไว้ถูกต้องและมีประสิทธิภาพ

ขั้นแรก ให้คัดลอกคลาส RoomSchemaArgProvider ต่อไปนี้ลงในไฟล์บิลด์ Gradle ของโมดูล ฟังก์ชัน asArguments ในคลาสตัวอย่างจะส่ง room.schemaLocation=${schemaDir.path} ไปยัง KSP หากคุณใช้ KAPT และ javac ให้เปลี่ยนค่านี้เป็น -Aroom.schemaLocation=${schemaDir.path} แทน

ดึงดูด

class RoomSchemaArgProvider implements CommandLineArgumentProvider {

  @InputDirectory
  @PathSensitive(PathSensitivity.RELATIVE)
  File schemaDir

  RoomSchemaArgProvider(File schemaDir) {
    this.schemaDir = schemaDir
  }

  @Override
  Iterable<String> asArguments() {
    return ["room.schemaLocation=${schemaDir.path}".toString()]
  }
}

Kotlin

class RoomSchemaArgProvider(
  @get:InputDirectory
  @get:PathSensitive(PathSensitivity.RELATIVE)
  val schemaDir: File
) : CommandLineArgumentProvider {

  override fun asArguments(): Iterable<String> {
    return listOf("room.schemaLocation=${schemaDir.path}")
  }
}

จากนั้นกำหนดค่าตัวเลือกการคอมไพล์ให้ใช้ RoomSchemaArgProvider กับไดเรกทอรีสคีมาที่ระบุ

ดึงดูด

ksp {
  arg(new RoomSchemaArgProvider(new File(projectDir, "schemas")))
}

Kotlin

ksp {
  arg(RoomSchemaArgProvider(File(projectDir, "schemas")))
}

ทดสอบการย้ายข้อมูลรายการเดียว

ก่อนที่จะทดสอบการย้ายข้อมูล ให้เพิ่มอาร์ติแฟกต์ androidx.room3:room3-testing ลงในทรัพยากร Dependency ของการทดสอบ และเพิ่มตำแหน่งสคีมาที่ส่งออกเป็นไดเรกทอรีเนื้อหา

ดึงดูด

android {
    ...
    sourceSets {
        // Adds exported schema location as test app assets if not using
        // the Room Gradle Plugin.
        androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
    }
}

dependencies {
    ...
    androidTestImplementation "androidx.room3:room3-testing:3.0.1"
}

Kotlin

android {
    ...
    sourceSets {
        // Adds exported schema location as test app assets if not using
        // the Room Gradle Plugin.
        getByName("androidTest").assets.srcDir("$projectDir/schemas")
    }
}

dependencies {
    ...
    testImplementation("androidx.room3:room3-testing:3.0.1")
}

แพ็กเกจการทดสอบมีคลาส MigrationTestHelper ซึ่งอ่าน ไฟล์สคีมาที่ส่งออกได้ นอกจากนี้ แพ็กเกจยังใช้อินเทอร์เฟซ JUnit4 TestRule เพื่อจัดการฐานข้อมูลที่สร้างขึ้น

ตัวอย่างต่อไปนี้แสดงการทดสอบการย้ายข้อมูลรายการเดียว

@RunWith(AndroidJUnit4::class)
class MigrationTest {
    private val TEST_DB = "migration-test"

    private val instrumentation = InstrumentationRegistry.getInstrumentation()

    @get:Rule
    val helper = MigrationTestHelper(
        instrumentation = instrumentation,
        databaseClass = MigrationDb::class,
        driver = AndroidSQLiteDriver(),
        file = instrumentation.targetContext.getDatabasePath(TEST_DB),
    )

    @Test
    fun migrate1To2() = runTest {
        val connection = helper.createDatabase(1)
        // Database has schema version 1. Insert some data using SQL queries.
        // You can't use DAO classes because they expect the latest schema.
        connection.execSQL("INSERT INTO User (id, name) VALUES (1, 'John Doe')")
        connection.close()

        // Re-open the database with version 2 and provide MIGRATION_1_2
        val migratedConnection = helper.runMigrationsAndValidate(2, listOf(MIGRATION_1_2))

        // MigrationTestHelper automatically verifies the schema changes,
        // but you need to validate that the data was migrated properly.
        val hasData = migratedConnection.prepare("SELECT COUNT(*) FROM User").use {
          it.step()
          it.getLong(0) > 0
        }
        assertTrue("Expected data was not migrated", hasData)
        migratedConnection.close()
    }
}

ทดสอบการย้ายข้อมูลทั้งหมด

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

ตัวอย่างต่อไปนี้แสดงการทดสอบการย้ายข้อมูลทั้งหมดที่กำหนดไว้

@RunWith(AndroidJUnit4::class)
class MigrationTest {
    private val TEST_DB = "migration-test"

    private val instrumentation = InstrumentationRegistry.getInstrumentation()

    // Array of all migrations.
    private val ALL_MIGRATIONS = arrayOf(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4)

    @get:Rule
    val helper: MigrationTestHelper = MigrationTestHelper(
        instrumentation = instrumentation,
        databaseClass = MigrationDb::class,
        driver = AndroidSQLiteDriver(),
        file = instrumentation.targetContext.getDatabasePath(TEST_DB),
    )

    @Test
    fun migrateAll() = runTest {
        // Create earliest version of the database.
        val connection = helper.createDatabase(1)
        connection.close()

        // Create latest version of the database.
        val db = Room.databaseBuilder<AppDatabase>(instrumentation.targetContext, TEST_DB)
          .setDriver(AndroidSQLiteDriver())
          .addMigrations(*ALL_MIGRATIONS)
          .build()
        // Open the database, Room validates the schema once all migrations
        // execute.
        db.useReaderConnection { connection ->
          // Perform additional validation
        }

        db.close()
    }
}

จัดการเส้นทางการย้ายข้อมูลที่ขาดหายไปอย่างเหมาะสม

หาก Room ไม่พบเส้นทางการย้ายข้อมูลเพื่ออัปเกรดฐานข้อมูลที่มีอยู่ใน อุปกรณ์เป็นเวอร์ชันปัจจุบัน ระบบจะแสดง IllegalStateException หากยอมรับได้ที่จะสูญเสียข้อมูลที่มีอยู่เมื่อไม่มีเส้นทางการย้ายข้อมูล ให้เรียกใช้ฟังก์ชันตัวสร้าง fallbackToDestructiveMigration เมื่อสร้างฐานข้อมูล

Room.databaseBuilder<FallbackMigrationDatabase>(applicationContext, "database-name")
        .fallbackToDestructiveMigration()
        .build()

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

หากต้องการกลับไปสร้างใหม่แบบทำลายล้างในบางสถานการณ์เท่านั้น ให้ใช้ตัวเลือกใดตัวเลือกหนึ่งต่อไปนี้แทน fallbackToDestructiveMigration

  • หากประวัติสคีมาบางเวอร์ชันทำให้เกิดข้อผิดพลาดที่คุณแก้ไม่ได้ ด้วยเส้นทางการย้ายข้อมูล ให้ใช้ fallbackToDestructiveMigrationFrom แทน ฟังก์ชันนี้ระบุว่าคุณต้องการให้ Room กลับไปสร้างใหม่แบบทำลายล้างเมื่อย้ายข้อมูลจากบางเวอร์ชันเท่านั้น
  • หากต้องการให้ Room กลับไปสร้างใหม่แบบทำลายล้างเมื่อย้ายข้อมูล จากฐานข้อมูลเวอร์ชันที่สูงกว่าเป็นเวอร์ชันที่ต่ำกว่าเท่านั้น ให้ใช้ fallbackToDestructiveMigrationOnDowngrade แทน