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

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

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

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

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

Kotlin

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

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

Java

// Database class before the version update.
@Database(
  version = 1,
  entities = {User.class}
)
public abstract class AppDatabase extends RoomDatabase {
  ...
}

// Database class after the version update.
@Database(
  version = 2,
  entities = {User.class},
  autoMigrations = {
    @AutoMigration (from = 1, to = 2)
  }
)
public abstract class AppDatabase extends RoomDatabase {
  ...
}

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

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

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

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

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

Kotlin

@Database(
  version = 2,
  entities = [User::class],
  autoMigrations = [
    AutoMigration (
      from = 1,
      to = 2,
      spec = AppDatabase.MyAutoMigration::class
    )
  ]
)
abstract class AppDatabase : RoomDatabase() {
  @RenameTable(fromTableName = "User", toTableName = "AppUser")
  class MyAutoMigration : AutoMigrationSpec
  ...
}

Java

@Database(
  version = 2,
  entities = {AppUser.class},
  autoMigrations = {
    @AutoMigration (
      from = 1,
      to = 2,
      spec = AppDatabase.MyAutoMigration.class
    )
  }
)
public abstract class AppDatabase extends RoomDatabase {
  @RenameTable(fromTableName = "User", toTableName = "AppUser")
  static class MyAutoMigration implements AutoMigrationSpec { }
  ...
}

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

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

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

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

Kotlin

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

val MIGRATION_2_3 = object : Migration(2, 3) {
  override fun migrate(database: SupportSQLiteDatabase) {
    database.execSQL("ALTER TABLE Book ADD COLUMN pub_year INTEGER")
  }
}

Room.databaseBuilder(applicationContext, MyDb::class.java, "database-name")
  .addMigrations(MIGRATION_1_2, MIGRATION_2_3).build()

Java

static final Migration MIGRATION_1_2 = new Migration(1, 2) {
  @Override
  public void migrate(SupportSQLiteDatabase database) {
    database.execSQL("CREATE TABLE `Fruit` (`id` INTEGER, "
      + "`name` TEXT, PRIMARY KEY(`id`))");
  }
};

static final Migration MIGRATION_2_3 = new Migration(2, 3) {
  @Override
  public void migrate(SupportSQLiteDatabase database) {
    database.execSQL("ALTER TABLE Book "
      + " ADD COLUMN pub_year INTEGER");
  }
};

Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name")
  .addMigrations(MIGRATION_1_2, MIGRATION_2_3).build();

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

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

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

ส่งออกสคีมา

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

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

หากใช้ Room เวอร์ชัน 2.6.0 ขึ้นไป คุณสามารถใช้ Room Gradle Plugin และใช้ส่วนขยาย room เพื่อระบุไดเรกทอรีสคีมาได้

Groovy

plugins {
  id 'androidx.room'
}

room {
  schemaDirectory "$projectDir/schemas"
}

Kotlin

plugins {
  id("androidx.room")
}

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

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

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

Groovy

room {
  // 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

room {
  // 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 เวอร์ชัน 2.5.2 หรือต่ำกว่า หรือไม่ได้ใช้ ปลั๊กอิน Room Gradle ให้ตั้งค่าตำแหน่งสคีมาโดยใช้room.schemaLocation ตัวเลือกโปรเซสเซอร์คำอธิบายประกอบ

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

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

Groovy

class RoomSchemaArgProvider implements CommandLineArgumentProvider {

  @InputDirectory
  @PathSensitive(PathSensitivity.RELATIVE)
  File schemaDir

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

  @Override
  Iterable<String> asArguments() {
    // Note: If you're using KAPT and javac, change the line below to
    // return ["-Aroom.schemaLocation=${schemaDir.path}".toString()].
    return ["room.schemaLocation=${schemaDir.path}".toString()]
  }
}

Kotlin

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

  override fun asArguments(): Iterable<String> {
    // Note: If you're using KAPT and javac, change the line below to
    // return listOf("-Aroom.schemaLocation=${schemaDir.path}").
    return listOf("room.schemaLocation=${schemaDir.path}")
  }
}

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

Groovy

// For KSP, configure using KSP extension:
ksp {
  arg(new RoomSchemaArgProvider(new File(projectDir, "schemas")))
}

// For javac or KAPT, configure using android DSL:
android {
  ...
  defaultConfig {
    javaCompileOptions {
      annotationProcessorOptions {
        compilerArgumentProviders(
          new RoomSchemaArgProvider(new File(projectDir, "schemas"))
        )
      }
    }
  }
}

Kotlin

// For KSP, configure using KSP extension:
ksp {
  arg(RoomSchemaArgProvider(File(projectDir, "schemas")))
}

// For javac or KAPT, configure using android DSL:
android {
  ...
  defaultConfig {
    javaCompileOptions {
      annotationProcessorOptions {
        compilerArgumentProviders(
          RoomSchemaArgProvider(File(projectDir, "schemas"))
        )
      }
    }
  }
}

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

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

build.gradle

Groovy

android {
    ...
    sourceSets {
        // Adds exported schema location as test app assets.
        androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
    }
}

dependencies {
    ...
    androidTestImplementation "androidx.room:room-testing:2.7.2"
}

Kotlin

android {
    ...
    sourceSets {
        // Adds exported schema location as test app assets.
        getByName("androidTest").assets.srcDir("$projectDir/schemas")
    }
}

dependencies {
    ...
    testImplementation("androidx.room:room-testing:2.7.2")
}

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

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

Kotlin

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

    @get:Rule
    val helper: MigrationTestHelper = MigrationTestHelper(
            InstrumentationRegistry.getInstrumentation(),
            MigrationDb::class.java.canonicalName,
            FrameworkSQLiteOpenHelperFactory()
    )

    @Test
    @Throws(IOException::class)
    fun migrate1To2() {
        var db = helper.createDatabase(TEST_DB, 1).apply {
            // Database has schema version 1. Insert some data using SQL queries.
            // You can't use DAO classes because they expect the latest schema.
            execSQL(...)

            // Prepare for the next version.
            close()
        }

        // Re-open the database with version 2 and provide
        // MIGRATION_1_2 as the migration process.
        db = helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2)

        // MigrationTestHelper automatically verifies the schema changes,
        // but you need to validate that the data was migrated properly.
    }
}

Java

@RunWith(AndroidJUnit4.class)
public class MigrationTest {
    private static final String TEST_DB = "migration-test";

    @Rule
    public MigrationTestHelper helper;

    public MigrationTest() {
        helper = new MigrationTestHelper(InstrumentationRegistry.getInstrumentation(),
                MigrationDb.class.getCanonicalName(),
                new FrameworkSQLiteOpenHelperFactory());
    }

    @Test
    public void migrate1To2() throws IOException {
        SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1);

        // Database has schema version 1. Insert some data using SQL queries.
        // You can't use DAO classes because they expect the latest schema.
        db.execSQL(...);

        // Prepare for the next version.
        db.close();

        // Re-open the database with version 2 and provide
        // MIGRATION_1_2 as the migration process.
        db = helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2);

        // MigrationTestHelper automatically verifies the schema changes,
        // but you need to validate that the data was migrated properly.
    }
}

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

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

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

Kotlin

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

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

    @get:Rule
    val helper: MigrationTestHelper = MigrationTestHelper(
            InstrumentationRegistry.getInstrumentation(),
            AppDatabase::class.java.canonicalName,
            FrameworkSQLiteOpenHelperFactory()
    )

    @Test
    @Throws(IOException::class)
    fun migrateAll() {
        // Create earliest version of the database.
        helper.createDatabase(TEST_DB, 1).apply {
            close()
        }

        // Open latest version of the database. Room validates the schema
        // once all migrations execute.
        Room.databaseBuilder(
            InstrumentationRegistry.getInstrumentation().targetContext,
            AppDatabase::class.java,
            TEST_DB
        ).addMigrations(*ALL_MIGRATIONS).build().apply {
            openHelper.writableDatabase.close()
        }
    }
}

Java

@RunWith(AndroidJUnit4.class)
public class MigrationTest {
    private static final String TEST_DB = "migration-test";

    @Rule
    public MigrationTestHelper helper;

    public MigrationTest() {
        helper = new MigrationTestHelper(InstrumentationRegistry.getInstrumentation(),
                AppDatabase.class.getCanonicalName(),
                new FrameworkSQLiteOpenHelperFactory());
    }

    @Test
    public void migrateAll() throws IOException {
        // Create earliest version of the database.
        SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1);
        db.close();

        // Open latest version of the database. Room validates the schema
        // once all migrations execute.
        AppDatabase appDb = Room.databaseBuilder(
                InstrumentationRegistry.getInstrumentation().getTargetContext(),
                AppDatabase.class,
                TEST_DB)
                .addMigrations(ALL_MIGRATIONS).build();
        appDb.getOpenHelper().getWritableDatabase();
        appDb.close();
    }

    // Array of all migrations.
    private static final Migration[] ALL_MIGRATIONS = new Migration[]{
            MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4};
}

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

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

Kotlin

Room.databaseBuilder(applicationContext, MyDb::class.java, "database-name")
        .fallbackToDestructiveMigration()
        .build()

Java

Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name")
        .fallbackToDestructiveMigration()
        .build();

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

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

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

จัดการค่าเริ่มต้นของคอลัมน์เมื่ออัปเกรดเป็น Room 2.2.0

ใน Room 2.2.0 ขึ้นไป คุณสามารถกำหนดค่าเริ่มต้นสำหรับคอลัมน์ได้โดยใช้ คำอธิบายประกอบ @ColumnInfo(defaultValue = "...") ในเวอร์ชันต่ำกว่า 2.2.0 วิธีเดียวในการกำหนดค่าเริ่มต้นสำหรับคอลัมน์คือการกำหนดค่าดังกล่าวโดยตรงในคำสั่ง SQL ที่ดำเนินการ ซึ่งจะสร้างค่าเริ่มต้นที่ Room ไม่รู้จัก ซึ่งหมายความว่าหากฐานข้อมูลสร้างขึ้นครั้งแรกโดย Room เวอร์ชันต่ำกว่า 2.2.0 การอัปเกรดแอปให้ใช้ Room 2.2.0 อาจกำหนดให้คุณต้องระบุเส้นทางการย้ายข้อมูลพิเศษสำหรับค่าเริ่มต้นที่มีอยู่ซึ่งคุณกำหนดโดยไม่ได้ใช้ Room API

ตัวอย่างเช่น สมมติว่าฐานข้อมูลเวอร์ชัน 1 กำหนดเอนทิตี Song ดังนี้

Kotlin

// Song entity, database version 1, Room 2.1.0.
@Entity
data class Song(
    @PrimaryKey
    val id: Long,
    val title: String
)

Java

// Song entity, database version 1, Room 2.1.0.
@Entity
public class Song {
    @PrimaryKey
    final long id;
    final String title;
}

สมมติว่าฐานข้อมูลเดียวกันเวอร์ชัน 2 เพิ่มคอลัมน์ NOT NULL ใหม่ และกำหนดเส้นทางการย้ายข้อมูลจากเวอร์ชัน 1 เป็นเวอร์ชัน 2 ดังนี้

Kotlin

// Song entity, database version 2, Room 2.1.0.
@Entity
data class Song(
    @PrimaryKey
    val id: Long,
    val title: String,
    val tag: String // Added in version 2.
)

// Migration from 1 to 2, Room 2.1.0.
val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL(
            "ALTER TABLE Song ADD COLUMN tag TEXT NOT NULL DEFAULT ''")
    }
}

Java

// Song entity, database version 2, Room 2.1.0.
@Entity
public class Song {
    @PrimaryKey
    final long id;
    final String title;
    @NonNull
    final String tag; // Added in version 2.
}


// Migration from 1 to 2, Room 2.1.0.
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
    @Override
    public void migrate(SupportSQLiteDatabase database) {
        database.execSQL(
            "ALTER TABLE Song ADD COLUMN tag TEXT NOT NULL DEFAULT ''");
    }
};

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

ใน Room เวอร์ชันต่ำกว่า 2.2.0 ความคลาดเคลื่อนนี้ไม่มีผลใดๆ อย่างไรก็ตาม หากต่อมาแอปอัปเกรดไปใช้ Room 2.2.0 ขึ้นไปและเปลี่ยนคลาสเอนทิตี Songtag ให้มีค่าเริ่มต้นโดยใช้คำอธิบายประกอบ @ColumnInfo Room จะเห็นความคลาดเคลื่อนนี้ ซึ่งส่งผลให้การตรวจสอบสคีมาล้มเหลว

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

  1. ประกาศค่าเริ่มต้นของคอลัมน์ในคลาสเอนทิตีที่เกี่ยวข้องโดยใช้คำอธิบายประกอบ @ColumnInfo
  2. เพิ่มหมายเลขเวอร์ชันของฐานข้อมูลขึ้น 1
  3. กําหนดเส้นทางการย้ายข้อมูลไปยังเวอร์ชันใหม่ที่ใช้กลยุทธ์การทิ้งและสร้างใหม่ เพื่อเพิ่มค่าเริ่มต้นที่จําเป็นลงในคอลัมน์ที่มีอยู่

ตัวอย่างต่อไปนี้แสดงกระบวนการนี้

Kotlin

// Migration from 2 to 3, Room 2.2.0.
val MIGRATION_2_3 = object : Migration(2, 3) {
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL("""
                CREATE TABLE new_Song (
                    id INTEGER PRIMARY KEY NOT NULL,
                    name TEXT,
                    tag TEXT NOT NULL DEFAULT ''
                )
                """.trimIndent())
        database.execSQL("""
                INSERT INTO new_Song (id, name, tag)
                SELECT id, name, tag FROM Song
                """.trimIndent())
        database.execSQL("DROP TABLE Song")
        database.execSQL("ALTER TABLE new_Song RENAME TO Song")
    }
}

Java

// Migration from 2 to 3, Room 2.2.0.
static final Migration MIGRATION_2_3 = new Migration(2, 3) {
    @Override
    public void migrate(SupportSQLiteDatabase database) {
        database.execSQL("CREATE TABLE new_Song (" +
                "id INTEGER PRIMARY KEY NOT NULL," +
                "name TEXT," +
                "tag TEXT NOT NULL DEFAULT '')");
        database.execSQL("INSERT INTO new_Song (id, name, tag) " +
                "SELECT id, name, tag FROM Song");
        database.execSQL("DROP TABLE Song");
        database.execSQL("ALTER TABLE new_Song RENAME TO Song");
    }
};