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