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

เมื่อเพิ่มและเปลี่ยนแปลงฟีเจอร์ในแอป คุณจะต้องแก้ไขคลาสเอนทิตี 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 และกำกับเนื้อหาด้วยข้อใดข้อหนึ่งต่อไปนี้

หากต้องการใช้การติดตั้งใช้งาน 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 เพื่อช่วยในกระบวนการทดสอบสำหรับการย้ายข้อมูลทั้งแบบอัตโนมัติและด้วยตนเอง หากต้องการให้อาร์ติแฟกต์นี้ทํางาน คุณต้องส่งออกสคีมาของฐานข้อมูลก่อน

ส่งออกสคีมา

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

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

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

Groovy

plugins {
  id 'androidx.room'
}

room {
  schemaDirectory "$projectDir/schemas"
}

Kotlin

plugins {
  id("androidx.room")
}

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

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

ตรวจสอบว่ารายการเหล่านี้ครอบคลุมทุกตัวแปร นอกจากนี้ คุณยังใส่ schemaDirectory() ที่ไม่มี variantMatchName เพื่อจัดการตัวแปรที่การกําหนดค่าอื่นๆ ไม่ตรงกันได้อีกด้วย ตัวอย่างเช่น ในแอปที่มี Flavor ของบิลด์ 2 รายการ demo และ full และประเภทบิลด์ 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 หรือต่ำกว่า หรือไม่ได้ใช้ปลั๊กอิน Gradle ของ Room ให้ตั้งค่าตำแหน่งสคีมาโดยใช้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 ลงในข้อกําหนดของข้อทดสอบ และเพิ่มตําแหน่งของสคีมาซึ่งส่งออกเป็นโฟลเดอร์ชิ้นงาน

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.6.1"
}

Kotlin

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

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

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

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

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 ขึ้นไปในภายหลังและเปลี่ยนคลาสเอนทิตี Song ให้รวมค่าเริ่มต้นสำหรับ tag โดยใช้คำอธิบายประกอบ @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");
    }
};