Raumdatenbank migrieren

Wenn Sie Features in Ihrer Anwendung hinzufügen und ändern, müssen Sie die Entitätsklassen für Zimmer und die zugrunde liegenden Datenbanktabellen anpassen, um diese Änderungen widerzuspiegeln. Wenn durch ein App-Update das Datenbankschema geändert wird, müssen die bereits in der On-Device-Datenbank vorhandenen Nutzerdaten beibehalten werden.

Room unterstützt sowohl automatisierte als auch manuelle Optionen für die inkrementelle Migration. Automatische Migrationen funktionieren bei den meisten grundlegenden Schemaänderungen. Für komplexere Änderungen müssen Sie jedoch möglicherweise Migrationspfade manuell definieren.

Automatisierte Migrationen

Wenn Sie eine automatisierte Migration zwischen zwei Datenbankversionen deklarieren möchten, fügen Sie dem Attribut autoMigrations in @Database eine @AutoMigration-Annotation hinzu:

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

Spezifikationen für die automatische Migration

Wenn Room mehrdeutige Schemaänderungen erkennt und ohne weitere Eingaben keinen Migrationsplan generieren kann, wird ein Kompilierungszeitfehler ausgelöst und Sie werden aufgefordert, einen AutoMigrationSpec zu implementieren. Dies tritt am häufigsten auf, wenn eine Migration einen der folgenden Schritte umfasst:

  • Tabelle löschen oder umbenennen
  • Löschen oder Umbenennen einer Spalte

Sie können AutoMigrationSpec verwenden, um Room die zusätzlichen Informationen bereitzustellen, die zum korrekten Generieren von Migrationspfaden erforderlich sind. Definieren Sie eine statische Klasse, die AutoMigrationSpec in Ihrer RoomDatabase-Klasse implementiert, und annotieren Sie sie mit einem oder mehreren der folgenden Elemente:

Wenn Sie die AutoMigrationSpec-Implementierung für eine automatisierte Migration verwenden möchten, legen Sie das Attribut spec in der entsprechenden @AutoMigration-Annotation fest:

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

Wenn Ihre Anwendung nach Abschluss der automatischen Migration noch mehr Arbeit erledigen muss, können Sie onPostMigrate() implementieren. Wenn Sie diese Methode in AutoMigrationSpec implementieren, wird sie von Room nach Abschluss der automatisierten Migration aufgerufen.

Manuelle Migrationen

Wenn eine Migration komplexe Schemaänderungen beinhaltet, kann Room möglicherweise nicht automatisch einen geeigneten Migrationspfad generieren. Wenn Sie beispielsweise die Daten in einer Tabelle in zwei Tabellen aufteilen, weiß Room nicht, wie diese Aufteilung erfolgen soll. In solchen Fällen müssen Sie einen Migrationspfad manuell definieren, indem Sie eine Migration-Klasse implementieren.

Eine Migration-Klasse definiert explizit einen Migrationspfad zwischen einer startVersion und einem endVersion, indem die Methode Migration.migrate() überschrieben wird. Fügen Sie dem Datenbank-Builder Ihre Migration-Klassen mit der Methode addMigrations() hinzu:

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

Beim Definieren der Migrationspfade können Sie für einige Versionen automatisierte und für andere manuelle Migrationen verwenden. Wenn Sie für dieselbe Version sowohl eine automatisierte als auch eine manuelle Migration definieren, verwendet Room die manuelle Migration.

Migrationen testen

Migrationen sind oft komplex und eine falsch definierte Migration kann zum Absturz Ihrer Anwendung führen. Testen Sie Ihre Migrationen, um die Stabilität Ihrer App zu erhalten. Room stellt ein Maven-Artefakt room-testing zur Verfügung, um den Testprozess für automatisierte und manuelle Migrationen zu unterstützen. Damit dieses Artefakt funktioniert, müssen Sie zuerst das Schema Ihrer Datenbank exportieren.

Schemas exportieren

Room kann die Schemainformationen Ihrer Datenbank bei der Kompilierung in eine JSON-Datei exportieren. Die exportierten JSON-Dateien stellen den Schemaverlauf Ihrer Datenbank dar. Speichern Sie diese Dateien in Ihrem Versionsverwaltungssystem, damit Room zu Testzwecken und um die automatische Migration zu generieren.

Schemaspeicherort mit dem Room Gradle-Plug-in festlegen

Wenn Sie die Room-Version 2.6.0 oder höher verwenden, können Sie das Room Gradle-Plug-in anwenden und das Schemaverzeichnis mit der Erweiterung room angeben.

Groovig

plugins {
  id 'androidx.room'
}

room {
  schemaDirectory "$projectDir/schemas"
}

Kotlin

plugins {
  id("androidx.room")
}

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

Wenn sich das Datenbankschema nach Variante, Flavor oder Build-Typ unterscheidet, müssen Sie verschiedene Speicherorte angeben. Verwenden Sie dazu mehrmals die Konfiguration schemaDirectory() mit jeweils einem variantMatchName als erstes Argument. Jede Konfiguration kann einer oder mehreren Varianten basierend auf einem einfachen Vergleich mit dem Variantennamen entsprechen.

Achten Sie darauf, dass diese vollständig sind und alle Varianten abdecken. Sie können auch einen schemaDirectory() ohne variantMatchName einfügen, um Varianten zu verarbeiten, die mit keiner der anderen Konfigurationen übereinstimmen. In einer Anwendung mit den beiden Build-Varianten demo und full sowie den beiden Build-Typen debug und release sind beispielsweise die folgenden Konfigurationen gültig:

Groovig

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

Schemaspeicherort mithilfe der Annotationsprozessor-Option festlegen

Wenn Sie die Version 2.5.2 oder niedriger von Room oder das Room Gradle-Plug-in nicht verwenden, legen Sie den Schemaspeicherort mit der Option für den Annotationsprozessor room.schemaLocation fest.

Dateien in diesem Verzeichnis werden als Ein- und Ausgaben für einige Gradle-Aufgaben verwendet. Damit die Genauigkeit und Leistung von inkrementellen und im Cache gespeicherten Builds gewährleistet ist, müssen Sie Gradle mit dem CommandLineArgumentProvider von Gradle über dieses Verzeichnis informieren.

Kopieren Sie zuerst die unten gezeigte RoomSchemaArgProvider-Klasse in die Gradle-Build-Datei Ihres Moduls. Die Methode asArguments() in der Beispielklasse übergibt room.schemaLocation=${schemaDir.path} an KSP. Wenn Sie KAPT und javac verwenden, ändern Sie diesen Wert stattdessen in -Aroom.schemaLocation=${schemaDir.path}.

Groovig

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

Konfigurieren Sie dann die Kompilierungsoptionen so, dass RoomSchemaArgProvider mit dem angegebenen Schemaverzeichnis verwendet wird:

Groovig

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

Einzelne Migration testen

Bevor Sie Ihre Migrationen testen können, fügen Sie das Maven-Artefakt androidx.room:room-testing aus Room in die Testabhängigkeiten ein und fügen Sie den Speicherort des exportierten Schemas als Asset-Ordner hinzu:

build.gradle

Groovig

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

Das Testpaket enthält eine MigrationTestHelper-Klasse, die exportierte Schemadateien lesen kann. Mit dem Paket wird auch die JUnit4-Schnittstelle TestRule implementiert, sodass erstellte Datenbanken verwaltet werden können.

Das folgende Beispiel zeigt einen Test für eine einzelne Migration:

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

Alle Migrationen testen

Obwohl es möglich ist, eine einzelne inkrementelle Migration zu testen, empfehlen wir, einen Test einzubeziehen, der alle für die Datenbank Ihrer Anwendung definierten Migrationen abdeckt. Dadurch wird sichergestellt, dass es keine Diskrepanz zwischen einer kürzlich erstellten Datenbankinstanz und einer älteren Instanz gibt, die den definierten Migrationspfaden folgt.

Das folgende Beispiel zeigt einen Test für alle definierten Migrationen:

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

Fehlende Migrationspfade reibungslos handhaben

Wenn Room keinen Migrationspfad für das Upgrade einer vorhandenen Datenbank auf einem Gerät auf die aktuelle Version findet, wird ein IllegalStateException ausgelöst. Wenn vorhandene Daten verloren gehen können, wenn ein Migrationspfad fehlt, rufen Sie beim Erstellen der Datenbank die Builder-Methode fallbackToDestructiveMigration() auf:

Kotlin

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

Java

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

Mit dieser Methode wird Room angewiesen, die Tabellen in der Datenbank Ihrer Anwendung destruktiv neu zu erstellen, wenn eine inkrementelle Migration erforderlich ist und es keinen definierten Migrationspfad gibt.

Wenn Room nur in bestimmten Situationen eine destruktive Wiederherstellung nutzen soll, gibt es Alternativen zu fallbackToDestructiveMigration():

  • Wenn bestimmte Versionen des Schemaverlaufs Fehler verursachen, die Sie mit Migrationspfaden nicht beheben können, verwenden Sie stattdessen fallbackToDestructiveMigrationFrom(). Diese Methode gibt an, dass Room nur bei der Migration von bestimmten Versionen auf destruktive Neuerstellung zurückgreift.
  • Wenn Room nur bei der Migration von einer höheren Datenbankversion zu einer niedrigeren eine destruktive Neuerstellung wiederherstellen soll, verwenden Sie stattdessen fallbackToDestructiveMigrationOnDowngrade().

Spaltenstandardwerte beim Upgrade auf Room 2.2.0 verarbeiten

In Room 2.2.0 und höher können Sie mit der Annotation @ColumnInfo(defaultValue = "...") einen Standardwert für eine Spalte definieren. In Versionen vor 2.2.0 kann ein Standardwert für eine Spalte nur definiert werden, indem sie direkt in einer ausgeführten SQL-Anweisung definiert wird. Dadurch wird ein Standardwert erstellt, den Room nicht kennt. Wenn also eine Datenbank ursprünglich von einer Room-Version als 2.2.0 erstellt wurde, müssen Sie für das Upgrade Ihrer Anwendung auf Room 2.2.0 möglicherweise einen speziellen Migrationspfad für vorhandene Standardwerte angeben, die Sie ohne Room APIs definiert haben.

Angenommen, Version 1 einer Datenbank definiert eine Song-Entität:

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

Nehmen wir außerdem an, dass Version 2 derselben Datenbank eine neue Spalte NOT NULL hinzufügt und einen Migrationspfad von Version 1 zu Version 2 definiert:

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

Dies führt zu einer Diskrepanz in der zugrunde liegenden Tabelle zwischen Aktualisierungen und Neuinstallationen der Anwendung. Da der Standardwert für die Spalte tag nur im Migrationspfad von Version 1 zu Version 2 deklariert wird, haben Nutzer, die die Anwendung ab Version 2 installieren, in ihrem Datenbankschema nicht den Standardwert für tag.

In früheren Versionen als 2.2.0 ist diese Diskrepanz harmlos. Wenn die Anwendung jedoch später auf Raum 2.2.0 oder höher aktualisiert und die Entitätsklasse Song so ändert, dass sie mithilfe der Annotation @ColumnInfo einen Standardwert für tag enthält, kann Room diese Abweichung sehen. Dies führt zu fehlgeschlagenen Schemavalidierungen.

Damit das Datenbankschema für alle Nutzer einheitlich ist, wenn in früheren Migrationspfaden Spaltenstandardwerte deklariert werden, gehen Sie beim ersten Upgrade Ihrer Anwendung auf Raum 2.2.0 oder höher so vor:

  1. Verwenden Sie die Annotation @ColumnInfo, um Spaltenstandardwerte in den jeweiligen Entitätsklassen zu deklarieren.
  2. Erhöhen Sie die Versionsnummer der Datenbank um 1.
  3. Definieren Sie einen Migrationspfad zur neuen Version, der die Strategie zum Löschen und Neuerstellen implementiert, um den vorhandenen Spalten die erforderlichen Standardwerte hinzuzufügen.

Das folgende Beispiel veranschaulicht diesen Vorgang:

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