- 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
219 lines
9.1 KiB
Markdown
219 lines
9.1 KiB
Markdown
---
|
||
name: android-db-migration
|
||
description: >
|
||
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`):
|
||
|
||
```kotlin
|
||
ksp { arg("room.schemaLocation", "$projectDir/schemas") }
|
||
```
|
||
|
||
```kotlin
|
||
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
|
||
|
||
1. **Niemals** `fallbackToDestructiveMigration()` verwenden – die App crasht bewusst, wenn kein Pfad definiert ist (Datenverlust bleibt sichtbar).
|
||
2. **Jede** Versionserhöhung braucht einen `@AutoMigration`- oder manuellen `Migration`-Eintrag.
|
||
3. **Immer** nach dem Schema-Bump bauen, damit die neue `N.json` generiert wird, bevor Tests geschrieben werden.
|
||
4. 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
|
||
|
||
```kotlin
|
||
@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
|
||
|
||
```kotlin
|
||
// 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
|
||
|
||
```kotlin
|
||
// 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
|
||
|
||
```kotlin
|
||
@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):
|
||
|
||
```kotlin
|
||
// 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
|
||
|
||
```kotlin
|
||
@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
|
||
|
||
```sql
|
||
-- 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.
|