Room veritabanınızı taşıyın

Uygulamanızda özellik ekleyip değiştirirken, Oda varlık sınıflarınızı ve temel veritabanı tablolarınızı bu değişiklikleri yansıtacak şekilde değiştirmeniz gerekir. Bir uygulama güncellemesi, veritabanı şemasını değiştirdiğinde cihaz üzerindeki veritabanında bulunan kullanıcı verilerinin korunması önemlidir.

Oda, artımlı taşıma için hem otomatik hem de manuel seçenekleri destekler. Otomatik taşıma işlemleri, temel şema değişikliklerinin çoğunda işe yarar ancak daha karmaşık değişiklikler için taşıma yollarını manuel olarak tanımlamanız gerekebilir.

Otomatik taşıma işlemleri

İki veritabanı sürümü arasında otomatik taşıma bildirmek için @Database içindeki autoMigrations özelliğine @AutoMigration ek açıklaması ekleyin:

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 {
  ...
}

Otomatik taşıma özellikleri

Oda, belirsiz şema değişiklikleri algılar ve daha fazla giriş olmadan bir taşıma planı oluşturamazsa derleme zamanı hatası verir ve sizden bir AutoMigrationSpec uygulamanızı ister. Bu durum genellikle taşıma işlemi aşağıdakilerden birini içerdiğinde gerçekleşir:

  • Tablo silme veya yeniden adlandırma.
  • Sütun silme veya yeniden adlandırma

Odaya, taşıma yollarını doğru şekilde oluşturmak için ihtiyaç duyduğu ek bilgileri vermek üzere AutoMigrationSpec kullanabilirsiniz. RoomDatabase sınıfınızda AutoMigrationSpec uygulayan statik bir sınıf tanımlayın ve aşağıdakilerden biri veya birkaçıyla ek açıklama ekleyin:

Otomatik taşıma işleminde AutoMigrationSpec uygulamasını kullanmak için ilgili @AutoMigration ek açıklamasında spec özelliğini ayarlayın:

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 { }
  ...
}

Otomatik taşıma işlemi tamamlandıktan sonra uygulamanızın daha fazla işlem yapması gerekiyorsa onPostMigrate() öğesini uygulayabilirsiniz. Bu yöntemi AutoMigrationSpec içinde uygularsanız Oda, otomatik taşıma işlemi tamamlandıktan sonra yöntemi çağırır.

Manuel taşıma işlemleri

Taşıma işleminin karmaşık şema değişiklikleri içerdiği durumlarda Room, otomatik olarak uygun bir taşıma yolu oluşturamayabilir. Örneğin, bir tablodaki verileri iki tabloya bölmeye karar verirseniz Oda, bu bölme işleminin nasıl yapılacağını söyleyemez. Bu gibi durumlarda, Migration sınıfı uygulayarak manuel olarak bir taşıma yolu tanımlamanız gerekir.

Migration sınıfı, Migration.migrate() yöntemini geçersiz kılarak startVersion ile endVersion arasındaki taşıma yolunu açıkça tanımlar. addMigrations() yöntemini kullanarak Migration sınıflarınızı veritabanı oluşturucunuza ekleyin:

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();

Taşıma yollarınızı tanımlarken bazı sürümler için otomatik taşımaları, bazıları için manuel taşımaları kullanabilirsiniz. Aynı sürüm için hem otomatik taşıma hem de manuel taşıma tanımlarsanız Oda, manuel taşıma işlemini kullanır.

Taşıma işlemlerini test etme

Taşıma işlemleri genellikle karmaşıktır ve yanlış tanımlanmış bir taşıma işlemi, uygulamanızın kilitlenmesine neden olabilir. Uygulamanızın kararlılığını korumak için taşıma işlemlerinizi test edin. Oda, hem otomatik hem de manuel taşıma işlemleri için test sürecine yardımcı olmak amacıyla bir room-testing Maven yapısı sağlar. Bu yapının çalışması için önce veritabanınızın şemasını dışa aktarmanız gerekir.

Şemaları dışa aktarma

Oda, derleme sırasında veritabanınızın şema bilgilerini bir JSON dosyasına aktarabilir. Dışa aktarılan JSON dosyaları, veritabanınızın şema geçmişini temsil eder. Odanın test amacıyla veritabanının daha düşük sürümlerini oluşturabilmesi ve otomatik taşıma oluşturmayı etkinleştirebilmesi için bu dosyaları sürüm kontrol sisteminizde depolayın.

Room Gradle eklentisini kullanarak şema konumunu ayarlama

Room 2.6.0 veya sonraki bir sürümü kullanıyorsanız Room Gradle Plugin'i uygulayabilir ve şema dizinini belirtmek için room uzantısını kullanabilirsiniz.

Eski

plugins {
  id 'androidx.room'
}

room {
  schemaDirectory "$projectDir/schemas"
}

Kotlin

plugins {
  id("androidx.room")
}

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

Veritabanı şemanız varyant, aroma veya derleme türüne göre farklılık gösteriyorsa schemaDirectory() yapılandırmasını birden fazla kez kullanarak farklı konumlar belirtmeniz gerekir. Bu yapılandırmaların her birinde ilk bağımsız değişken olarak variantMatchName yer alır. Her yapılandırma, varyant adıyla yapılan basit karşılaştırmaya dayanarak bir veya daha fazla varyantı eşleştirebilir.

Bu koşulların kapsamlı olduğundan ve tüm varyantları kapsadığından emin olun. Diğer yapılandırmalardan hiçbiriyle eşleşmeyen varyantları işlemek için variantMatchName içermeyen bir schemaDirectory() de ekleyebilirsiniz. Örneğin, demo ve full iki derleme türü ile debug ve release derleme türüne sahip bir uygulamada aşağıdakiler geçerli yapılandırmalardır:

Eski

room {
  // Applies to 'demoDebug' only
  schemaLocation "demoDebug", "$projectDir/schemas/demoDebug"

  // Applies to 'demoDebug' and 'demoRelease'
  schemaLocation "demo", "$projectDir/schemas/demo"

  // Applies to 'demoDebug' and 'fullDebug'
  schemaLocation "debug", "$projectDir/schemas/debug"

  // Applies to variants that aren't matched by other configurations.
  schemaLocation "$projectDir/schemas"
}

Kotlin

room {
  // Applies to 'demoDebug' only
  schemaLocation("demoDebug", "$projectDir/schemas/demoDebug")

  // Applies to 'demoDebug' and 'demoRelease'
  schemaLocation("demo", "$projectDir/schemas/demo")

  // Applies to 'demoDebug' and 'fullDebug'
  schemaLocation("debug", "$projectDir/schemas/debug")

  // Applies to variants that aren't matched by other configurations.
  schemaLocation("$projectDir/schemas")
}

Ek açıklama işlemci seçeneğini kullanarak şema konumunu ayarlama

Room'un 2.5.2 veya daha eski bir sürümünü kullanıyorsanız ya da Room Gradle eklentisini kullanmıyorsanız room.schemaLocation ek açıklama işlemci seçeneğini kullanarak şema konumunu ayarlayın.

Bu dizindeki dosyalar bazı Gradle görevleri için giriş ve çıkış olarak kullanılır. Artımlı ve önbelleğe alınmış derlemelerin doğruluğu ve performansı için Gradle'ı bu dizin hakkında bilgilendirmek amacıyla Gradle'ın CommandLineArgumentProvider hizmetini kullanmanız gerekir.

İlk olarak aşağıda gösterilen RoomSchemaArgProvider sınıfını modülünüzün Gradle derleme dosyasına kopyalayın. Örnek sınıftaki asArguments() yöntemi, KSP sınıfına room.schemaLocation=${schemaDir.path} iletir. KAPT ve javac kullanıyorsanız bu değeri -Aroom.schemaLocation=${schemaDir.path} olarak değiştirin.

Eski

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

Ardından, RoomSchemaArgProvider öğesini belirtilen şema diziniyle kullanmak için derleme seçeneklerini yapılandırın:

Eski

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

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

Kotlin

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

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

Tek bir taşıma işlemini test etme

Taşıma işlemlerinizi test etmeden önce, Room'daki androidx.room:room-testing Maven yapısını test bağımlılıklarınıza ve dışa aktarılan şemanın konumunu öğe klasörü olarak ekleyin:

derleme.gradle

Eski

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

Test paketi, dışa aktarılan şema dosyalarını okuyabilen bir MigrationTestHelper sınıfı sağlar. Paket, oluşturulan veritabanlarını yönetebilmek için JUnit4 TestRule arayüzünü de uygular.

Aşağıdaki örnekte tek bir taşıma işlemine yönelik bir test gösterilmektedir:

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.
    }
}

Tüm taşıma işlemlerini test et

Tek bir artımlı taşımayı test etmek mümkün olsa da uygulamanızın veritabanı için tanımlanan tüm taşıma işlemlerini kapsayan bir test eklemenizi öneririz. Bu, yakın zamanda oluşturulmuş bir veritabanı örneği ile tanımlanan taşıma yollarını izleyen eski bir örnek arasında fark olmamasını sağlar.

Aşağıdaki örnekte tanımlanan tüm taşıma işlemlerine yönelik bir test gösterilmektedir:

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};
}

Eksik taşıma yollarının sorunsuz bir şekilde ele alınması

Oda, bir cihazdaki mevcut veritabanını geçerli sürüme yükseltmek için bir taşıma yolu bulamazsa IllegalStateException işlemi gerçekleşir. Taşıma yolu eksik olduğunda mevcut verilerin kaybedilmesi kabul edilebilir bir durumsa veritabanını oluştururken fallbackToDestructiveMigration() oluşturucu yöntemini çağırın:

Kotlin

Room.databaseBuilder(applicationContext, MyDb::class.java, "database-name")
        .fallbackToDestructiveMigration()
        .build()

Java

Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name")
        .fallbackToDestructiveMigration()
        .build();

Bu yöntem, odanın artımlı bir taşıma yapması gerektiğinde ve tanımlanmış bir taşıma yolu olmadığında uygulamanızın veritabanındaki tabloları yıkıcı bir şekilde yeniden oluşturmasını bildirir.

Oda modunu yalnızca belirli durumlarda yıkıcı eğlence faaliyetlerine geri dönmek istiyorsanız fallbackToDestructiveMigration() yerine kullanabileceğiniz birkaç alternatif vardır:

  • Şema geçmişinizin belirli sürümleri, taşıma yollarıyla çözemediğiniz hatalara neden oluyorsa bunun yerine fallbackToDestructiveMigrationFrom() değerini kullanın. Bu yöntem, Room'un yalnızca belirli sürümlerden taşıma yaparken zararlı yeniden oluşturma işlevini kullanmayı istediğinizi belirtir.
  • Room'un yalnızca daha yüksek bir veritabanı sürümünden daha düşük bir sürüme geçiş yaparken yıkıcı yeniden oluşturma işlemlerine yedeklenmesini istiyorsanız bunun yerine fallbackToDestructiveMigrationOnDowngrade() kullanın.

Oda 2.2.0'a yükseltme yaparken sütun varsayılan değerlerini işleme

Oda 2.2.0 ve sonraki sürümlerde, @ColumnInfo(defaultValue = "...") ek açıklamasını kullanarak bir sütun için varsayılan bir değer tanımlayabilirsiniz. 2.2.0'dan düşük sürümlerde, bir sütun için varsayılan değer tanımlamanın tek yolu, sütunu doğrudan yürütülen bir SQL ifadesinde tanımlamaktır. Bu işlem Oda'nın bilmediği bir varsayılan değer oluşturur. Bu, veritabanının orijinal olarak Odanın 2.2.0'dan daha eski bir sürümüyle oluşturulduğu durumlarda, uygulamanızı Oda 2.2.0'ı kullanacak şekilde yükselttiğinizde, Room API'leri kullanmadan tanımladığınız mevcut varsayılan değerler için özel bir taşıma yolu sağlamanız gerekebileceği anlamına gelir.

Örneğin, veritabanı sürüm 1'in bir Song varlığını tanımladığını varsayalım:

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;
}

Ayrıca, aynı veritabanına ait sürüm 2'nin yeni bir NOT NULL sütunu eklediğini ve sürüm 1'den sürüm 2'ye bir taşıma yolu tanımladığını varsayalım:

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 ''");
    }
};

Bu durum, temel tabloda uygulamanın güncellemeleri ile yeni yüklemeleri arasında tutarsızlığa neden olur. tag sütununun varsayılan değeri yalnızca sürüm 1 ile sürüm 2 arasındaki taşıma yolunda belirtildiğinden, uygulamayı sürüm 2'den başlayarak yükleyen kullanıcılar veritabanı şemasında varsayılan tag değerine sahip değildir.

Odanın 2.2.0'dan önceki sürümlerinde, bu tutarsızlık zararsızdır. Bununla birlikte, uygulama daha sonra Oda 2.2.0 veya sonraki bir sürümü kullanacak şekilde yükseltilir ve @ColumnInfo ek açıklamasını kullanarak Song varlık sınıfını tag için varsayılan bir değer içerecek şekilde değiştirirse Oda bu tutarsızlığı görebilir. Bu durum, şema doğrulamalarının başarısız olmasına neden olur.

Önceki taşıma yollarınızda sütun varsayılan değerleri tanımlandığında veritabanı şemasının tüm kullanıcılar arasında tutarlı olmasını sağlamak için uygulamanızı Oda 2.2.0 veya sonraki bir sürümü kullanacak şekilde ilk kez yükselttiğinizde aşağıdaki adımları uygulayın:

  1. @ColumnInfo ek açıklamasını kullanarak, ilgili varlık sınıflarındaki sütun varsayılan değerlerini bildirin.
  2. Veritabanı sürüm numarasını 1 artırın.
  3. Gerekli varsayılan değerleri mevcut sütunlara eklemek için bırak ve yeniden oluşturma stratejisini uygulayan yeni sürüme bir taşıma yolu tanımlayın.

Aşağıdaki örnekte bu işlem gösterilmektedir:

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");
    }
};