रूम डेटाबेस को माइग्रेट करना

अपने ऐप्लिकेशन में सुविधाएं जोड़ने और उनमें बदलाव करने के दौरान, आपको Room की इकाई वाली क्लास और डेटाबेस टेबल में बदलाव करना होगा, ताकि ये बदलाव दिखें. जब ऐप्लिकेशन का अपडेट, डेटाबेस स्कीमा में बदलाव करता है, तब उपयोगकर्ता के डिवाइस पर मौजूद डेटाबेस में पहले से मौजूद उपयोगकर्ता का डेटा सुरक्षित रखना ज़रूरी होता है.

Room में, इंक्रीमेंटल माइग्रेशन के लिए अपने-आप और मैन्युअल, दोनों विकल्प उपलब्ध हैं. ज़्यादातर बुनियादी स्कीमा बदलावों के लिए, डेटा अपने-आप माइग्रेट हो जाता है. हालांकि, ज़्यादा मुश्किल बदलावों के लिए, आपको माइग्रेशन पाथ मैन्युअल तरीके से तय करने पड़ सकते हैं.

अपने-आप माइग्रेट होने की सुविधा

दो डेटाबेस वर्शन के बीच अपने-आप माइग्रेट होने की सुविधा के बारे में बताने के लिए, @Database में मौजूद autoMigrations प्रॉपर्टी में @AutoMigration एनोटेशन जोड़ें:

// 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 को अतिरिक्त जानकारी दी जा सकती है. इससे Room को माइग्रेशन पाथ सही तरीके से जनरेट करने में मदद मिलती है. अपनी RoomDatabase क्लास में AutoMigrationSpec को लागू करने वाली क्लास तय करें और उसे इनमें से एक या उससे ज़्यादा एनोटेशन के साथ एनोटेट करें:

अपने-आप होने वाले माइग्रेशन के लिए, AutoMigrationSpec को लागू करने के लिए, इससे जुड़ी @AutoMigration एनोटेशन में spec प्रॉपर्टी सेट करें:

@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 क्लास, migrate फ़ंक्शन को बदलकर, startVersion और endVersion के बीच माइग्रेशन पाथ के बारे में साफ़ तौर पर बताती है. addMigrations फ़ंक्शन का इस्तेमाल करके, अपने डेटाबेस बिल्डर में Migration क्लास जोड़ें:

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 फ़ाइलें, आपके डेटाबेस के स्कीमा के इतिहास को दिखाती हैं. इन फ़ाइलों को अपने वर्शन कंट्रोल सिस्टम में सेव करें, ताकि टेस्टिंग के लिए डेटाबेस के पुराने वर्शन फिर से बनाए जा सकें. साथ ही, ऑटोमेटेड माइग्रेशन जनरेशन की सुविधा का इस्तेमाल किया जा सके.

Room Gradle प्लगिन का इस्तेमाल करके स्कीमा की जगह सेट करना

स्कीमा डायरेक्ट्री तय करने के लिए, Room Gradle Plugin लागू करें और room3 एक्सटेंशन का इस्तेमाल करें.

शानदार

plugins {
  id 'androidx.room3'
}

room3 {
  schemaDirectory "$projectDir/schemas"
}

Kotlin

plugins {
  id("androidx.room3")
}

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

अगर वैरिएंट, फ़्लेवर या बिल्ड टाइप के आधार पर आपके डेटाबेस स्कीमा में अंतर है, तो आपको अलग-अलग जगहों के बारे में बताना होगा. इसके लिए, schemaDirectory कॉन्फ़िगरेशन का इस्तेमाल कई बार करें. हर बार variantMatchName को पहले आर्ग्युमेंट के तौर पर इस्तेमाल करें. हर कॉन्फ़िगरेशन, वैरिएंट के नाम के साथ तुलना करके एक या उससे ज़्यादा वैरिएंट से मैच कर सकता है.

पक्का करें कि ये सभी वैरिएंट शामिल हों. variantMatchName के बिना भी schemaDirectory() को शामिल किया जा सकता है. इससे उन वैरिएंट को मैनेज किया जा सकता है जो किसी भी कॉन्फ़िगरेशन से मेल नहीं खाते. उदाहरण के लिए, अगर किसी ऐप्लिकेशन में दो बिल्ड फ़्लेवर 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 के कुछ टास्क के लिए इनपुट और आउटपुट के तौर पर करता है. इंक्रीमेंटल और कैश मेमोरी में सेव की गई बिल्ड के सही तरीके से काम करने और उनकी परफ़ॉर्मेंस के लिए, आपको Gradle के CommandLineArgumentProvider का इस्तेमाल करना होगा, ताकि 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 का इस्तेमाल करें.