העברת מסד הנתונים של החדרים

כשמוסיפים תכונות באפליקציה ומשנים אותן, צריך לשנות את הישות לחדר והטבלאות של מסדי הנתונים הבסיסיים כדי לשקף את השינויים האלה. חשוב כדי לשמור נתוני משתמשים שכבר נמצאים במסד הנתונים במכשיר כשאפליקציה משנה את הסכימה של מסד הנתונים.

Room תומך באפשרויות אוטומטיות וידניות להעברה מצטברת. העברות אוטומטיות פועלות עבור רוב השינויים הבסיסיים בסכימה, אבל ייתכן שיהיה עליך להגדיר באופן ידני נתיבי העברה לשינויים מורכבים יותר.

העברות אוטומטיות

כדי להצהיר על העברה אוטומטית בין שתי גרסאות של מסד נתונים, צריך להוסיף @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 {
  ...
}

מפרטי העברה אוטומטית

אם החדר מזהה שינויים לא ברורים בסכימה והוא לא יכול ליצור תוכנית למיגרציה ללא קלט נוסף, היא גורמת לשגיאה זמן הידור (compiler) ושואלת כדי ליישם AutoMigrationSpec בדרך כלל מצב כזה מתרחש כשהעברה כוללת אחד מהמצבים הבאים:

  • מחיקה או שינוי שם של טבלה.
  • מחיקה או שינוי שם של עמודה.

אפשר להשתמש באפליקציית AutoMigrationSpec כדי לספק לחדר את המידע הנוסף צריך ליצור נתיבי העברה בצורה תקינה. הגדירו מחלקה סטטית מטמיעים את 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, החדר יתקשר אליה אחרי תהליך ההעברה האוטומטי מסתיים.

העברות ידניות

במקרים שבהם ההעברה כרוכה בשינויים מורכבים בסכימה, יכול להיות שהחדר לא יהיה ליצור נתיב העברה מתאים באופן אוטומטי. לדוגמה, אם אתם מחליטים לפצל את הנתונים בטבלה לשתי טבלאות, 'חדר' לא יכול לדעת איך לבצע את הפיצול הזה. במקרים כאלה, צריך לבצע את הפעולות הבאות באופן ידני להגדיר נתיב העברה באמצעות Migration.

מחלקה Migration מגדירה באופן מפורש נתיב העברה בין startVersion ו-endVersion על ידי ביטול של Migration.migrate() . הוספת הכיתות של Migration לבונה מסדי נתונים באמצעות ה addMigrations() method:

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

כשמגדירים את נתיבי ההעברה, אפשר להשתמש בהעברות אוטומטיות והעברות ידניות לאחרים. אם מגדירים גם תבנית עריכה אוטומטית העברה ידנית והעברה ידנית עבור אותה גרסה, אז Room משתמש ב מיגרציה.

בדיקת העברות

העברות הן לעיתים קרובות מורכבות, והעברה שמוגדרת בצורה שגויה עלולה לגרום כדי שהאפליקציה תקרוס. כדי לשמור על היציבות של האפליקציה, כדאי לבדוק את והעברות. בחדר יש ארטיפקט של Maven מהroom-testing כדי לעזור תהליך בדיקה להעברות אוטומטיות וידניות. עבור פריט המידע הזה שנוצר בתהליך הפיתוח (Artifact) כדי צריך לייצא תחילה את הסכימה של מסד הנתונים.

ייצוא סכימות

חדר יכול לייצא את פרטי הסכימה של מסד הנתונים לקובץ JSON במהלך הידור בזמן האימון. קובצי ה-JSON שתייצאו מייצגים את היסטוריית הסכימה של מסד הנתונים. חנות קבצים אלה במערכת ניהול הגרסאות שלכם כדי שחדר יוכל ליצור גרסאות נמוכות יותר של מסד הנתונים למטרות בדיקה וכדי לאפשר יצירת העברה אוטומטית.

הגדרת מיקום הסכימה באמצעות הפלאגין Room Gradle

אם אתם משתמשים בגרסה 2.6.0 ומעלה של החדר, אתם יכולים להחיל את פלאגין Room Gradle ושימוש ב- התוסף room לציון ספריית הסכימות.

מגניב

plugins {
  id 'androidx.room'
}

room {
  schemaDirectory "$projectDir/schemas"
}

Kotlin

plugins {
  id("androidx.room")
}

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

אם הסכימה של מסד הנתונים משתנה בהתאם לווריאנט, לטעם או ל-build מהסוג הזה, יש לציין מיקומים שונים באמצעות הפרמטר schemaDirectory() כמה פעמים, כשכל אחת היא variantMatchName ארגומנט. כל הגדרה יכולה להתאים לווריאנט אחד או יותר לפי תקן להשוואה לשם הווריאנט.

חשוב לוודא שכל המסמכים מפורטים ומכסים את כל הווריאציות. אפשר גם לכלול schemaDirectory() בלי variantMatchName כדי לטפל בווריאציות שלא נמצאה להן התאמה כל הגדרה אחרת. לדוגמה, באפליקציה שיש בה שני גרסאות build demo ו-full ושני סוגי build debug ו-release, אלה ההגדרות החוקיות:

מגניב

room {
  // 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

room {
  // 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")
}

הגדרת מיקום הסכימה באמצעות אפשרות של מעבד הערות

אם אתם משתמשים בגרסה 2.5.2 ומטה של 'חדר', או אם אתם לא משתמשים פלאגין Room Gradle, הגדרת מיקום הסכימה באמצעות room.schemaLocation לעיבוד הערות.

הקבצים בספרייה הזו משמשים כקלט וכפלט למשימות מסוימות של Gradle. כדי לבדוק את הנכונות והביצועים של גרסאות build מצטברות ושמורות במטמון, צריך להשתמש של Gradle CommandLineArgumentProvider כדי לעדכן את Gradle לגבי הספרייה הזו.

קודם כול, מעתיקים את הכיתה RoomSchemaArgProvider שמוצגת למטה לתוך המודול קובץ build של 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() {
    // 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}")
  }
}

לאחר מכן צריך להגדיר את אפשרויות ההידור כך שישתמשו ב-RoomSchemaArgProvider עם ספריית הסכימה שצוינה:

מגניב

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

בדיקת העברה יחידה

כדי לבדוק את ההעברות, צריך להוסיף את androidx.room:room-testing ארטיפקט של Maven מהחדר לבדיקה של יחסי התלות ומוסיפים את המיקום של הסכימה המיוצאת כתיקיית נכסים:

build.gradle

מגניב

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 ליצור מחדש באופן הרסני את הטבלאות באפליקציה כשצריך לבצע מיגרציה מצטברת ואין נתיב העברה מוגדר.

אם מעדיפים להשתמש ב'חדר' רק כדי לחזור וליהנות מפעילויות הרסניות מסוגים מסוימים במצבים מסוימים, יש כמה חלופות ל-fallbackToDestructiveMigration():

  • אם גרסאות ספציפיות של היסטוריית הסכימות גורמות לשגיאות שאין לכם אפשרות לפתור עם נתיבי העברה, משתמשים fallbackToDestructiveMigrationFrom() במקום זאת. השיטה הזו מציינת שאתם רוצים שהחדר יחזור להיות הרסני רק כשעוברים מגרסאות ספציפיות.
  • אם רוצים שחדר הרחצה יוכל לחזור וליהנות מפעילויות הרסניות רק לאחר ההעברה מגרסה גבוהה יותר של מסד נתונים לגרסה נמוכה יותר, משתמשים fallbackToDestructiveMigrationOnDowngrade() במקום זאת.

כשמשדרגים לחדר 2.2.0, צריך לטפל בערכי ברירת המחדל של העמודות

בחדר 2.2.0 ואילך אפשר להגדיר ערך ברירת מחדל לעמודה באמצעות ההערה @ColumnInfo(defaultValue = "...") בגרסאות שקודמות לגרסה 2.2.0, הדרך היחידה להגדיר ערך ברירת מחדל היא להגדיר אותה ישירות בהצהרת SQL שמופעלת, ותיצור ערך ברירת המחדל שהחדר לא מכיר. כלומר, אם מסד נתונים נוצרה במקור על ידי גרסה של 'חדר' ישנה יותר מ-2.2.0, ושדרגתי את האפליקציה שלך יכול להיות שתצטרכו לספק נתיב העברה מיוחד כדי להשתמש ב'חדר 2.2.0'. ערכי ברירת מחדל קיימים שהגדרתם בלי להשתמש בממשקי API של Room.

לדוגמה, נניח שגרסה 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, כל המשתמשים להתקין את האפליקציה החל מגרסה 2 לא כוללים את ערך ברירת המחדל עבור tag בסכימה של מסד הנתונים.

בגרסאות של 'חדר' שקודמות לגרסה 2.2.0, הפער הזה לא מזיק. אבל אם האפליקציה תשדרג מאוחר יותר לשימוש בחדר 2.2.0 ואילך ותשנה את הישות Song class כדי לכלול ערך ברירת מחדל עבור 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");
    }
};