MWCodebymw.de ↗
Kotlin

Room-Migrationen – das Schema ändern, ohne Nutzerdaten zu verlieren

Eine neue Spalte in der Entity, und beim nächsten Start ist der Spielstand weg – weil fallbackToDestructiveMigration die Datenbank einfach löscht. So machst du es richtig, inklusive Test.

Room prüft beim Start, ob das Schema in der Datei zur Version im Code passt. Passt es nicht und gibt es keine Migration, wirft es eine IllegalStateException – oder, was schlimmer ist, du hast fallbackToDestructiveMigration() gesetzt und die Datenbank wird kommentarlos neu angelegt. Für den Nutzer heißt das: alles weg.

Der Ablauf

Beim Ändern einer Entity gilt: Version hochzählen und eine Migration schreiben.

@Database(entities = [Notiz::class], version = 2, exportSchema = true)
abstract class AppDb : RoomDatabase() {
    abstract fun notizen(): NotizDao
}

val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(db: SupportSQLiteDatabase) {
        db.execSQL("ALTER TABLE notiz ADD COLUMN farbe TEXT NOT NULL DEFAULT '#ffffff'")
    }
}

val db = Room.databaseBuilder(context, AppDb::class.java, "app.db")
    .addMigrations(MIGRATION_1_2)
    .build()

Wichtig beim Hinzufügen einer NOT NULL-Spalte: Ohne DEFAULT schlägt das ALTER TABLE bei vorhandenen Zeilen fehl. Und der Standardwert muss genau dem entsprechen, was die Entity deklariert – sonst meldet Room beim Start eine Schema-Abweichung.

Spalten umbauen: der Vier-Schritte-Tanz

SQLite kann Spalten nicht beliebig ändern. Für Typänderungen, neue Fremdschlüssel oder entfernte Spalten führt der Weg über eine neue Tabelle:

val MIGRATION_2_3 = object : Migration(2, 3) {
    override fun migrate(db: SupportSQLiteDatabase) {
        db.execSQL("""
            CREATE TABLE notiz_neu (
              id INTEGER PRIMARY KEY NOT NULL,
              titel TEXT NOT NULL,
              text TEXT NOT NULL,
              erstellt INTEGER NOT NULL DEFAULT 0
            )
        """.trimIndent())
        db.execSQL("INSERT INTO notiz_neu (id, titel, text, erstellt) SELECT id, titel, text, 0 FROM notiz")
        db.execSQL("DROP TABLE notiz")
        db.execSQL("ALTER TABLE notiz_neu RENAME TO notiz")
        db.execSQL("CREATE INDEX index_notiz_titel ON notiz (titel)")
    }
}

Indizes musst du dabei neu anlegen – sie hängen an der alten Tabelle und verschwinden mit ihr.

Das Schema exportieren (und einchecken)

// build.gradle.kts
ksp { arg("room.schemaLocation", "$projectDir/schemas") }

Room legt dort je Version eine JSON-Datei ab. Die gehört ins Git: Sie ist die Grundlage für automatische Migrationstests – und sie zeigt im Code-Review, was sich am Schema geändert hat.

Automatische Migrationen für einfache Fälle

@Database(
    entities = [Notiz::class],
    version = 4,
    autoMigrations = [AutoMigration(from = 3, to = 4)],
    exportSchema = true,
)
abstract class AppDb : RoomDatabase()

Für hinzugefügte Spalten und Tabellen reicht das. Beim Umbenennen hilfst du mit @RenameColumn nach; bei Typänderungen bleibt es bei Handarbeit.

Testen, bevor es Nutzer merken

@get:Rule
val helper = MigrationTestHelper(
    InstrumentationRegistry.getInstrumentation(),
    AppDb::class.java,
)

@Test
fun migriere1zu2() {
    helper.createDatabase(TEST_DB, 1).apply {
        execSQL("INSERT INTO notiz (id, titel, text) VALUES (1, 'Alt', 'Inhalt')")
        close()
    }
    val db = helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2)
    db.query("SELECT farbe FROM notiz WHERE id = 1").use {
        it.moveToFirst()
        assertEquals("#ffffff", it.getString(0))
    }
}

Das ist der eigentliche Punkt: Eine Migration, die nie gegen echte alte Daten gelaufen ist, ist eine Vermutung.

Fallstrick

fallbackToDestructiveMigration() gehört in keine Version, die im Store liegt – höchstens in Debug-Builds, und dann bewusst. Denk außerdem an den Sprung über mehrere Versionen: Ein Nutzer, der drei Updates übersprungen hat, startet mit Version 1 in eine App mit Version 4. Room kettet dafür deine Migrationen – aber nur, wenn sie lückenlos vorhanden sind.

Android-Apps mit lokaler Datenhaltung baue und pflege ich regelmäßig. Melde dich über bymw.de.

Quellen

#Kotlin#Room#Android#Datenbank#Migration

Du brauchst mehr als ein Snippet?

Ich entwickle Android-Apps in Kotlin und moderne Websites für Selbstständige und kleine Unternehmen — von der ersten Idee bis zum Release.

Projekt anfragen →