نقل قاعدة بيانات الغرفة

عند إضافة الميزات وتغييرها في تطبيقك، ستحتاج إلى تعديل فئات كيانات الغرفة وجداول قاعدة البيانات الأساسية لتعكس هذه التغييرات. من المهم الاحتفاظ ببيانات المستخدمين المتوفرة في قاعدة البيانات على الجهاز فقط عندما يغيّر تحديث التطبيق مخطط قاعدة البيانات.

تتيح الغرفة كلاً من الخيارات المبرمَجة واليدوية لعملية النقل المتزايد. تتوافق عمليات نقل البيانات التلقائية مع معظم التغييرات الأساسية في المخططات، ولكن قد تحتاج إلى تحديد مسارات نقل البيانات يدويًا لإجراء تغييرات أكثر تعقيدًا.

عمليات نقل البيانات المبرمَجة

لإعلان عملية نقل بيانات مُبرمَجة بين إصدارَين من قاعدة البيانات، أضِف تعليقًا توضيحيًا على @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 من إنشاء مسار مناسب لنقل البيانات تلقائيًا. على سبيل المثال، إذا قررت تقسيم البيانات في جدول إلى جدولين، فلا يمكن للغرفة معرفة كيفية إجراء هذا التقسيم. في مثل هذه الحالات، عليك تحديد مسار نقل البيانات يدويًا من خلال تنفيذ فئة 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();

عند تحديد مسارات نقل البيانات، يمكنك استخدام عمليات نقل البيانات المُبرمَجة لبعض الإصدارات وعمليات نقل البيانات اليدوية لبعض الإصدارات الأخرى. في حال تحديد كل من النقل التلقائي والنقل اليدوي للإصدار نفسه، ستستخدم الغرفة عملية النقل اليدوي.

اختبار عمليات نقل البيانات

غالبًا ما تكون عمليات نقل البيانات معقدة، ويمكن أن تؤدي عملية النقل المحددة بشكل غير صحيح إلى تعطُّل تطبيقك. للحفاظ على استقرار تطبيقك، عليك اختبار عمليات نقل البيانات. توفّر الغرفة أداة Maven من room-testing للمساعدة في عملية اختبار كل من عمليات نقل البيانات التلقائية واليدوية. لكي تعمل هذه الأداة، يجب عليك أولاً تصدير مخطط قاعدة البيانات لديك.

تصدير المخططات

يمكن لتطبيق Room تصدير معلومات مخطط قاعدة البيانات إلى ملف JSON في وقت التجميع.

ضبط موقع المخطط باستخدام المكوّن الإضافي Room Gradle

لتصدير المخطط باستخدام الإصدار 2.6.0 من الغرفة أو الإصدارات الأحدث، طبِّق المكوِّن الإضافي Room Gradle واستخدِم الإضافة room لتحديد دليل المخطط.

في ملف إصدار Gradle ذو المستوى الأعلى، حدِّد المكوِّن الإضافي وإصداره:

رائع

plugins {
  id 'androidx.room' version '2.6.0' apply false
}

Kotlin

plugins {
  id("androidx.room") version "2.6.0" apply false
}

في ملف إصدار Gradle للمشروع باستخدام Room، طبِّق المكوِّن الإضافي. يمكنك استخدام الإضافة room لضبط الموقع الجغرافي الذي تريد من Room كتابة مخططات قاعدة البيانات فيه.

رائع

plugins {
  id 'androidx.room'
}

room {
  schemaDirectory "$projectDir/schemas"
}

Kotlin

plugins {
  id("androidx.room")
}

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

تمثّل ملفات JSON التي تم تصديرها سجلّ مخطّط قاعدة البيانات. يُرجى تخزين هذه الملفات في نظام التحكم في الإصدارات كي يتمكن Room من إنشاء نُسخ أدنى من قاعدة البيانات لأغراض الاختبار ولتفعيل عملية نقل البيانات التلقائية.

ضبط موقع المخطط باستخدام خيار معالج التعليقات التوضيحية

لتصدير المخطط عند استخدام الإصدار 2.5.2 أو الإصدارات الأقدم من غرفة، يجب ضبط وسيطة معالج التعليقات التوضيحية room.schemaLocation باستخدام CommandLineArgumentProvider في ملف app/build.gradle.

أولاً، حدِّد السمة CommandLineArgumentProvider التي توفِّر السمة room.schemaLocation للمعالج، كما هو موضّح في المثال التالي. تجدر الإشارة إلى أنّ طريقة asArguments() في الفئة تؤدّي إلى تمرير -Aroom.schemaLocation=${schemaDir.path} إلى javac أو KAPT. إذا كنت تستخدم نظام أسماء النطاقات (KSP)، عليك تغييره إلى room.schemaLocation=${schemaDir.path}.

رائع

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 KSP, change the line below to return
    // ["room.schemaLocation=${schemaDir.path}".toString()].
    return ["-Aroom.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 KSP, change the line below to return
    // listOf("room.schemaLocation=${schemaDir.path}").
    return listOf("-Aroom.schemaLocation=${schemaDir.path}")
  }
}

بعد ذلك، يمكنك ضبط خيارات التجميع لاستخدام RoomSchemaArgProvider مع دليل المخطط المحدَّد، والتأكّد من توفّر هذه الخيارات أولاً:

رائع

// For javac or KAPT, configure using android DSL:
android {
  ...
  defaultConfig {
    javaCompileOptions {
      annotationProcessorOptions {
        compilerArgumentProviders(
          new RoomSchemaArgProvider(new File(projectDir, "schemas"))
        )
      }
    }
  }
}

// For KSP, configure using KSP extension:
ksp {
  arg(new RoomSchemaArgProvider(new File(projectDir, "schemas")))
}

Kotlin

// For javac or KAPT, configure using android DSL:
android {
  ...
  defaultConfig {
    javaCompileOptions {
      annotationProcessorOptions {
        compilerArgumentProviders(
          RoomSchemaArgProvider(File(projectDir, "schemas"))
        )
      }
    }
  }
}

// For KSP, configure using KSP extension:
ksp {
  arg(RoomSchemaArgProvider(File(projectDir, "schemas")))
}

تمثّل ملفات JSON التي تم تصديرها سجلّ مخطّط قاعدة البيانات. يُرجى تخزين هذه الملفات في نظام التحكم في الإصدارات كي يتمكن Room من إنشاء إصدارات قديمة من قاعدة البيانات لأغراض الاختبار ولتفعيل النقل التلقائي للبيانات.

اختبار عملية نقل بيانات واحدة

قبل اختبار عمليات نقل البيانات، أضِف androidx.room:room-testing أداة Maven من Room إلى العناصر التابعة للاختبار وأضِف موقع المخطط الذي تم تصديره كمجلد مواد عرض:

Build.grale

رائع

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 يمكنها قراءة ملفات المخططات التي تم تصديرها. تنفِّذ الحزمة أيضًا واجهة JUnit4 TestRule، حتى تتمكن من إدارة قواعد البيانات التي تم إنشاؤها.

يوضِّح المثال التالي اختبارًا لعملية نقل بيانات واحدة:

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() بدلاً من ذلك.

التعامل مع القيم التلقائية للأعمدة عند الترقية إلى الغرفة 2.2.0

في الغرفة 2.2.0 والإصدارات الأحدث، يمكنك تحديد قيمة تلقائية لعمود باستخدام التعليق التوضيحي @ColumnInfo(defaultValue = "..."). في الإصدارات الأقدم من 2.2.0، تكون الطريقة الوحيدة لتحديد قيمة تلقائية لعمود هي تعريفها مباشرةً في عبارة SQL تم تنفيذها، ما يؤدي إلى إنشاء قيمة تلقائية لا تعرفها Room. يعني هذا أنّه إذا تم إنشاء قاعدة البيانات في الأصل باستخدام إصدار من Room أقدم من الإصدار 2.2.0، قد تتطلّب ترقية تطبيقك لاستخدام Room 2.2.0 توفير مسار خاص لنقل البيانات للقيم التلقائية الحالية التي حدّدتها بدون استخدام واجهات برمجة تطبيقات الغرف.

على سبيل المثال، لنفترض أنّ الإصدار 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، لن تكون القيمة التلقائية لـ tag لدى أي مستخدم يثبّت التطبيق بدءًا من الإصدار 2 في مخطط قاعدة البيانات.

وفي إصدارات الغرفة الأقل من 2.2.0، لا تسبب هذا التناقض في حدوث أي ضرر. ومع ذلك، إذا تمت ترقية التطبيق لاحقًا لاستخدام الغرفة 2.2.0 أو إصدار أحدث وغيّر فئة الكيان Song لتضمين قيمة تلقائية في tag باستخدام التعليق التوضيحي @ColumnInfo، يمكن أن تلاحظ الغرفة عندها هذا التناقض. ويؤدي هذا إلى فشل تحققات المخطط.

للمساعدة في ضمان اتساق مخطط قاعدة البيانات على مستوى جميع المستخدمين عند الإعلان عن القيم التلقائية للأعمدة في مسارات نقل البيانات السابقة، يمكنك إجراء ما يلي في المرة الأولى التي تتم فيها ترقية تطبيقك لاستخدام الغرفة 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");
    }
};