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

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

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

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

אם 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 קוראת לה אחרי שהמיגרציה האוטומטית מסתיימת.

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

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

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

בדיקת העברות

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

ייצוא סכימות

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

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

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

Groovy

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, ההגדרות הבאות תקינות:

Groovy

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

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

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

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

קודם מעתיקים את הכיתה RoomSchemaArgProvider שמופיעה בהמשך לקובץ ה-build של המודול ב-Gradle. השיטה asArguments() במחלקה לדוגמה מעבירה את room.schemaLocation=${schemaDir.path} אל KSP. אם אתם משתמשים ב-KAPT וב-javac, צריך לשנות את הערך הזה ל--Aroom.schemaLocation=${schemaDir.path} במקום זאת.

Groovy

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 עם ספריית הסכימה שצוינה:

Groovy

// 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 מ-Room ליחסי התלות של הבדיקות ולהוסיף את המיקום של הסכימה המיוצאת כספריית נכסים:

build.gradle

Groovy

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, שיכולה לקרוא קובצי סכימה שיוצאו. החבילה גם מיישמת את הממשק TestRule של JUnit4, כדי שתוכל לנהל מסדי נתונים שנוצרו.

הדוגמה הבאה ממחישה בדיקה של העברה יחידה:

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. אם אתם לא מתנגדים לאבד נתונים קיימים אם נתיב ההעברה חסר, תוכלו להפעיל את שיטת ה-builder‏ 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().

טיפול בערכי ברירת המחדל של עמודות כשמשדרגים ל-Room 2.2.0

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

בגרסאות של Room שקודמות ל-2.2.0, אי ההתאמה הזו לא מזיקה. עם זאת, אם האפליקציה תשודרג מאוחר יותר לשימוש ב-Room 2.2.0 ואילך, ותשנה את סיווג הישות Song כך שיכלול ערך ברירת מחדל ל-tag באמצעות ההערה @ColumnInfo, מערכת Room תוכל לראות את אי ההתאמה הזו. כתוצאה מכך, תהליכי אימות הסכימה נכשלים.

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