bollwerk/.github/skills/android-db-migration/SKILL.md
Jens Reinemann 045a4b7674 feat: Migration-Safety – Room v7, AutoMigration, Flyway, kein fallbackToDestructiveMigration (#99)
- 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
2026-05-17 21:17:24 +02:00

9.1 KiB
Raw Permalink Blame History

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

  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

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