আপনার Room ডেটাবেস মাইগ্রেট করা

আপনার অ্যাপে ফিচার যোগ ও পরিবর্তন করার সাথে সাথে, এইসব পরিবর্তন দেখানোর জন্য আপনাকে Room এন্টিটি ক্লাস ও অন্তর্নিহিত ডেটাবেস টেবিল পরিবর্তন করতে হবে। কোনও অ্যাপ আপডেট করার ফলে ডেটাবেস স্কিমা পরিবর্তন হলে, ডিভাইসে আগে থেকেই থাকা ব্যবহারকারীর ডেটা সংরক্ষণ করা গুরুত্বপূর্ণ।

ইনক্রিমেন্টাল মাইগ্রেশনের জন্য রুম অটোমেটিক ও ম্যানুয়াল, দুটি বিকল্পেই কাজ করে। বেশিরভাগ সাধারণ স্কিমা পরিবর্তনের ক্ষেত্রে অটোমেটিক মাইগ্রেশন কাজ করে, তবে আরও জটিল পরিবর্তনের জন্য আপনাকে ম্যানুয়ালি মাইগ্রেশন পাথ নির্ধারণ করতে হতে পারে।

অটোমেটেড মাইগ্রেশন

দুটি ডেটাবেস ভার্সনের মধ্যে অটোমেটিক মাইগ্রেশন ঘোষণা করতে, @AutoMigration অ্যানোটেশনকে @Database-এর মধ্যে থাকা autoMigrations প্রপার্টিতে যোগ করুন:

// 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 ইমপ্লিমেন্টেশন প্রদান করতে হবে। সাধারণত, এটি তখনই ঘটে যখন মাইগ্রেশনে নিম্নলিখিতগুলির মধ্যে একটি থাকে:

  • টেবিল মুছে দেওয়া বা নাম পরিবর্তন করা।
  • কলাম মুছে দেওয়া বা নাম পরিবর্তন করা।

মাইগ্রেশন পাথ সঠিকভাবে জেনারেট করার জন্য Room-এর প্রয়োজনীয় অতিরিক্ত তথ্য দিতে আপনি AutoMigrationSpec ব্যবহার করতে পারেন। আপনার 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 ক্লাস প্রয়োগ করে মাইগ্রেশন পাথ ম্যানুয়ালি নির্ধারণ করতে হবে।

migrate ফাংশন ওভাররাইড করার মাধ্যমে Migration ক্লাসটি 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 Plugin ব্যবহার করে স্কিমার লোকেশন সেট করা

স্কিমা ডিরেক্টরি নির্দিষ্ট করতে, 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 ফাংশনটি KSP-এ room.schemaLocation=${schemaDir.path} পাস করে। আপনি 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.3"
}

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.3")
}

টেস্টিং প্যাকেজে 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 ব্যবহার করুন।