- fallbackToDestructiveMigration() aus DatabaseModule entfernt - BollwerkDatabase auf Version 7 gebumpt - AutoMigration(from=5, to=6) und (from=6, to=7) definiert - MigrationTestHelper-Test migrate6To7_preservesData implementiert - 7.json Schema-Export generiert - Server: Flyway 9.22.3 integriert (baselineOnMigrate=true) - V1__initial_schema.sql + V2__cleanup_user_id.sql angelegt - Skill android-db-migration erstellt - versionCode 5 / versionName 1.4
9.1 KiB
| name | description |
|---|---|
| android-db-migration | Room-Datenbankmigration in der Bollwerk-App: Schema-Version erhöhen, @AutoMigration hinzufügen, manuelle Migration schreiben, MigrationTestHelper-Tests erstellen, fallbackToDestructiveMigration entfernen/vermeiden. Nutze diesen Skill immer wenn es um Room-Schema-Änderungen, DB-Versionen, Migrationen, Spalten umbenennen/löschen, neue Tabellen oder Migrationstests geht. Trigger-Phrasen: "migration", "DB-Version", "Schema", "Room", "AutoMigration", "fallback", "Datenverlust", "Spalte hinzufügen/umbenennen", "BollwerkDatabase", "DatabaseModule". |
Skill: Android DB-Migration (Room)
Vollständige Anleitung zur sicheren Room-Migration in der Bollwerk-App – vom Schema-Bump über @AutoMigration bis zum MigrationTestHelper-Test.
Projektkontext
| Datei | Zweck |
|---|---|
app/src/main/java/de/bollwerk/app/data/db/BollwerkDatabase.kt |
@Database-Annotation, Versionsnummer, autoMigrations-Liste |
app/src/main/java/de/bollwerk/app/data/db/Migrations.kt |
Manuelle Migration(X, Y)-Objekte (nur bei AutoMigration-Grenzen nötig) |
app/src/main/java/de/bollwerk/app/di/DatabaseModule.kt |
Room Builder – enthält kein fallbackToDestructiveMigration() |
app/schemas/de.bollwerk.app.data.db.BollwerkDatabase/ |
Exportierte Schema-JSON-Dateien (auto-generiert via KSP) |
app/src/androidTest/.../BollwerkDatabaseMigrationTest.kt |
MigrationTestHelper-Tests |
KSP-Konfiguration (app/build.gradle.kts):
ksp { arg("room.schemaLocation", "$projectDir/schemas") }
sourceSets { getByName("androidTest").assets.srcDirs("$projectDir/schemas") }
→ Schema-JSONs werden bei jedem Build automatisch aktualisiert und sind als Assets für Migrationstests verfügbar.
Goldene Regeln
- Niemals
fallbackToDestructiveMigration()verwenden – die App crasht bewusst, wenn kein Pfad definiert ist (Datenverlust bleibt sichtbar). - Jede Versionserhöhung braucht einen
@AutoMigration- oder manuellenMigration-Eintrag. - Immer nach dem Schema-Bump bauen, damit die neue
N.jsongeneriert wird, bevor Tests geschrieben werden. - Für Spalten-Umbenennungen immer
@RenameColumn-Spec angeben (AutoMigration erkennt Rename nicht automatisch).
Checkliste: Schema-Änderung
[ ] 1. Entity-Datei anpassen (Spalte/Tabelle hinzufügen, umbenennen, löschen)
[ ] 2. BollwerkDatabase.kt: version auf N+1 erhöhen
[ ] 3. BollwerkDatabase.kt: autoMigrations um AutoMigration(from=N, to=N+1) ergänzen
→ Bei Rename: spec = MeineRenameSpec::class angeben
→ Bei komplexer Migration: manuelles Migration-Objekt in Migrations.kt
[ ] 4. Build ausführen → N+1.json wird generiert
[ ] 5. MigrationTestHelper-Test in BollwerkDatabaseMigrationTest.kt schreiben
[ ] 6. Build + Tests grün
BollwerkDatabase.kt – Muster
@Database(
entities = [CategoryEntity::class, LocationEntity::class, ItemEntity::class,
SettingsEntity::class, PendingSyncOpEntity::class, MessageEntity::class],
version = 8, // ← erhöhen
exportSchema = true,
autoMigrations = [
AutoMigration(from = 5, to = 6), // No-op (gleiche Schemas)
AutoMigration(from = 6, to = 7), // No-op (Infrastruktur-Baseline)
AutoMigration(from = 7, to = 8), // z.B. neue Spalte
// AutoMigration(from = 8, to = 9, spec = MeineSpec::class) ← bei Rename
]
)
@TypeConverters(LocalDateConverter::class)
internal abstract class BollwerkDatabase : RoomDatabase() { ... }
AutoMigration-Fälle
Fall 1: Neue Spalte mit Default
// Entity: neue Spalte mit @ColumnInfo(defaultValue = "")
@ColumnInfo(name = "barcode", defaultValue = "") val barcode: String = ""
// @Database:
AutoMigration(from = 7, to = 8)
// Kein Spec nötig – Room erkennt ADD COLUMN automatisch
Fall 2: Spalte löschen
// Spec-Klasse anlegen (in Migrations.kt oder eigenem File):
@DeleteColumn(tableName = "items", columnName = "legacy_field")
class Migration7To8Spec : AutoMigrationSpec
// @Database:
AutoMigration(from = 7, to = 8, spec = Migration7To8Spec::class)
Fall 3: Spalte umbenennen
@RenameColumn(tableName = "items", fromColumnName = "kcal_per_kg", toColumnName = "kcal_per_unit")
class Migration7To8Spec : AutoMigrationSpec
// @Database:
AutoMigration(from = 7, to = 8, spec = Migration7To8Spec::class)
Fall 4: Komplexe Migration (Daten transformieren)
Wenn AutoMigration nicht reicht (z. B. Daten kopieren, Tabellen zusammenführen):
// In Migrations.kt:
internal val MIGRATION_7_8 = object : Migration(7, 8) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE items ADD COLUMN quantity_unit TEXT NOT NULL DEFAULT 'Stück'")
db.execSQL("UPDATE items SET quantity_unit = unit WHERE unit IS NOT NULL")
}
}
// In DatabaseModule.kt:
Room.databaseBuilder(context, BollwerkDatabase::class.java, "bollwerk.db")
.addCallback(DefaultDataCallback)
.addMigrations(MIGRATION_7_8)
.build()
MigrationTestHelper-Test
@get:Rule
val migrationTestHelper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
BollwerkDatabase::class.java
)
@Test
fun migrate7To8_preservesData() {
// 1. DB im alten Zustand anlegen und Testdaten einfügen
migrationTestHelper.createDatabase(dbName, 7).use { db ->
db.execSQL("INSERT INTO categories (name) VALUES ('Lebensmittel')")
db.execSQL(
"INSERT INTO items (id, name, category_id, quantity, unit, unit_price, " +
"kcal_per_unit, expiry_date, location_id, notes, last_updated) " +
"VALUES ('item-1', 'Wasser', 1, 10.0, 'Liter', 0.5, NULL, NULL, 1, '', 0)"
)
}
// 2. Migration durchführen und Schema validieren
migrationTestHelper.runMigrationsAndValidate(dbName, 8, true)
.use { db ->
// 3. Datenpersistenz prüfen
db.query("SELECT id, name FROM items").use { cursor ->
assertTrue(cursor.moveToFirst())
assertEquals("item-1", cursor.getString(0))
}
}
}
Wichtig: Die Schema-JSON-Dateien aus app/schemas/ müssen als Assets im androidTest-Quellsatz liegen (bereits konfiguriert). Der MigrationTestHelper liest sie automatisch.
Häufige Fehler & Lösungen
| Fehler | Ursache | Lösung |
|---|---|---|
IllegalStateException: A migration from X to Y was required but not found |
Version erhöht ohne AutoMigration-Eintrag | @AutoMigration(from=X, to=Y) in @Database eintragen |
Cannot find implementation for AutoMigration |
Build noch nicht nach Schema-Änderung ausgeführt | ./gradlew assembleDebug ausführen, Y.json muss existieren |
Expected X migrations, found Y |
addMigrations() fehlt für manuelle Migration |
In DatabaseModule.kt .addMigrations(MIGRATION_X_Y) eintragen |
MigrationTestHelper: No schema file found for version X |
Schema-JSON für Version X existiert nicht in app/schemas/ |
Build mit Version X zuerst ausführen, JSON committen |
| Rename-Spalte → Datenverlust | AutoMigration interpretiert Rename als DROP+ADD | @RenameColumn-Spec zwingend angeben |
Aktuelle DB-Version
Aktuelle Produktionsversion: 7 (BollwerkDatabase.kt, version = 7)
Migrations-Pfade vorhanden:
- v5 → v6: No-op (identische Schemas,
@AutoMigration) - v6 → v7: No-op (Infrastruktur-Baseline,
@AutoMigration)
Nächste Version für neue Schema-Änderung: v8
Hinweis: Server-Migration (Flyway)
Für Schema-Änderungen auf dem Server (PostgreSQL + Exposed):
- SQL-Skripte unter
server/src/main/resources/db/migration/ - Namenskonvention:
V{N}__{beschreibung}.sql - Flyway ist integriert (
baselineOnMigrate = true) – läuft beim Server-Start automatisch - Bestehende Datenbanken werden auf V1 gebaselined; neue Skripte werden einmalig ausgeführt
-- Beispiel: server/src/main/resources/db/migration/V3__add_barcode_column.sql
ALTER TABLE items ADD COLUMN IF NOT EXISTS barcode VARCHAR(100);
IF NOT EXISTS / IF EXISTS in SQL verwenden, um Idempotenz zu gewährleisten.