عند إضافة ميزات وتغييرها في تطبيقك، عليك تعديل فئات عناصر Room وجداول قاعدة البيانات الأساسية لعكس هذه التغييرات. من المهم الحفاظ على بيانات المستخدمين المخزَّنة في قاعدة البيانات على الجهاز فقط عند تغيير مخطط قاعدة البيانات من خلال تحديث التطبيق.
تتيح Room خيارات مبرمَجة ويدوية لنقل البيانات بشكل تدريجي. تعمل عمليات نقل البيانات التلقائية مع معظم التغييرات الأساسية على المخطط، ولكن قد تحتاج إلى تحديد مسارات نقل البيانات يدويًا لإجراء تغييرات أكثر تعقيدًا.
عمليات نقل البيانات المبرمَجة
للإشارة إلى عملية نقل بيانات تلقائية بين إصدارَين من قاعدة البيانات، أضِف التعليق التوضيحي
@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 إنشاء مسار نقل بيانات مناسب تلقائيًا. على سبيل المثال، إذا قرّرت تقسيم البيانات في جدول إلى جدولَين، لا يمكن لـ 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 room3-testingعنصر Maven للمساعدة في عملية الاختبار لكل من عمليات النقل التلقائية واليدوية. لكي يعمل هذا العنصر، يجب أولاً تصدير مخطط قاعدة البيانات.
مخططات التصدير
يصدّر Room معلومات مخطّط قاعدة البيانات إلى ملف JSON في وقت الترجمة البرمجية. تمثّل ملفات JSON التي تم تصديرها سجلّ مخطط قاعدة البيانات. خزِّن هذه الملفات في نظام التحكّم في الإصدارات لتتمكّن من إعادة إنشاء إصدارات أقدم من قاعدة البيانات لأغراض الاختبار وإنشاء عملية ترحيل تلقائية.
ضبط موقع المخطط باستخدام المكوّن الإضافي لنظام Gradle المتوافق مع Room
لتحديد دليل المخطط، طبِّق المكوّن الإضافي Room Gradle واستخدِم الإضافة room3.
أنيق
plugins {
id 'androidx.room3'
}
room3 {
schemaDirectory "$projectDir/schemas"
}
Kotlin
plugins {
id("androidx.room3")
}
room3 {
schemaDirectory("$projectDir/schemas")
}
إذا كان مخطط قاعدة البيانات يختلف استنادًا إلى الصيغة أو الإصدار أو نوع الإصدار، عليك تحديد مواقع مختلفة باستخدام schemaDirectoryالإعدادschemaDirectory عدة مرات، مع استخدام variantMatchName كمعلَمة أولى في كل مرة. يمكن أن يتطابق كل إعداد مع صيغة واحدة أو أكثر استنادًا إلى مقارنة بسيطة مع اسم الصيغة.
تأكَّد من أنّ هذه القائمة شاملة وتغطي جميع الصيغ. يمكنك أيضًا تضمين
schemaDirectory() بدون variantMatchName للتعامل مع الصيغ التي لا تتطابق مع أي من الإعدادات الأخرى. على سبيل المثال، في تطبيق يتضمّن نكهتَي إصدار demo وfull ونوعَي إصدار 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
إلى تبعيات الاختبار وأضِف موقع المخطط الذي تم تصديره
كالدليل الخاص بمواد العرض:
أنيق
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بدلاً من ذلك.