Best Practices für die In-App-Datenbankmigration für Sqlite

94

Ich verwende SQLite für mein iPhone und gehe davon aus, dass sich das Datenbankschema im Laufe der Zeit ändern wird. Was sind die Fallstricke, Namenskonventionen und Dinge, auf die Sie achten müssen, um jedes Mal eine erfolgreiche Migration durchzuführen?

Ich habe beispielsweise darüber nachgedacht, eine Version an den Datenbanknamen anzuhängen (z. B. Database_v1).

Segen
quelle

Antworten:

110

Ich verwalte eine Anwendung, die regelmäßig eine SQLite-Datenbank aktualisieren und alte Datenbanken in das neue Schema migrieren muss.

Zum Verfolgen der Datenbankversion verwende ich die von sqlite bereitgestellte integrierte Benutzerversionsvariable (sqlite macht mit dieser Variablen nichts, Sie können sie nach Belieben verwenden). Es beginnt bei 0 und Sie können diese Variable mit den folgenden SQLite-Anweisungen abrufen / festlegen:

> PRAGMA user_version;  
> PRAGMA user_version = 1;

Wenn die App gestartet wird, überprüfe ich die aktuelle Benutzerversion, wende alle Änderungen an, die erforderlich sind, um das Schema auf den neuesten Stand zu bringen, und aktualisiere dann die Benutzerversion. Ich verpacke die Updates in eine Transaktion, damit die Änderungen nicht festgeschrieben werden, wenn etwas schief geht.

Zum Vornehmen von Schemaänderungen unterstützt sqlite die Syntax "ALTER TABLE" für bestimmte Vorgänge (Umbenennen der Tabelle oder Hinzufügen einer Spalte). Dies ist eine einfache Möglichkeit, vorhandene Tabellen direkt zu aktualisieren. Die Dokumentation finden Sie hier: http://www.sqlite.org/lang_altertable.html . Zum Löschen von Spalten oder anderen Änderungen, die von der Syntax "ALTER TABLE" nicht unterstützt werden, erstelle ich eine neue Tabelle, migriere das Datum hinein, lösche die alte Tabelle und benenne die neue Tabelle in den ursprünglichen Namen um.

Rngbus
quelle
2
Ich versuche, die gleiche Logik zu haben, aber aus irgendeinem Grund, wenn ich "pragma user_version =?" programmatisch scheitert es ... irgendeine Idee?
Einhorn
7
Pragma-Einstellungen unterstützen keine Parameter, Sie müssen den tatsächlichen Wert angeben: "pragma user_version = 1".
Csgero
2
Ich habe eine Frage. Nehmen wir an, Sie haben eine erste Version 1. Und die aktuelle Version ist 5. Es gibt einige Updates in Version 2,3,4. Der Endbenutzer hat nur Ihre Version 1 heruntergeladen und jetzt auf Version 5 aktualisiert. Was sollten Sie tun?
Bagusflyer
5
Aktualisieren Sie die Datenbank in mehreren Schritten und übernehmen Sie die Änderungen, die erforderlich sind, um von Version 1 auf Version 2, dann von Version 2 auf Version 3 usw. zu wechseln, bis sie auf dem neuesten Stand ist. Eine einfache Möglichkeit, dies zu tun, besteht darin, eine switch-Anweisung zu haben, bei der jede "case" -Anweisung die Datenbank um eine Version aktualisiert. Sie "wechseln" zur aktuellen Datenbankversion und die case-Anweisungen fallen durch, bis die Aktualisierung abgeschlossen ist. Wenn Sie die Datenbank aktualisieren, fügen Sie einfach eine neue case-Anweisung hinzu. Ein detailliertes Beispiel hierfür finden Sie in der Antwort von Billy Gray.
Rngbus
1
@KonstantinTarkus ist laut Dokumentation application_id ein zusätzliches Bit zum Identifizieren des Dateiformats nach fileDienstprogramm, nicht für Versionen der Datenbank.
Xaizek
30

Die Antwort von Just Curious ist absolut zutreffend (Sie haben es verstanden!) Und wir verwenden sie, um die Version des Datenbankschemas zu verfolgen, das derzeit in der App enthalten ist.

Um die Migrationen durchzuführen, die auftreten müssen, damit user_version mit der erwarteten Schemaversion der App übereinstimmt, verwenden wir eine switch-Anweisung. Hier ist ein geschnittenes Beispiel dafür, wie dies in unserer App Strip aussieht :

- (void) migrateToSchemaFromVersion:(NSInteger)fromVersion toVersion:(NSInteger)toVersion { 
    // allow migrations to fall thru switch cases to do a complete run
    // start with current version + 1
    [self beginTransaction];
    switch (fromVersion + 1) {
        case 3:
            // change pin type to mode 'pin' for keyboard handling changes
            // removing types from previous schema
            sqlite3_exec(db, "DELETE FROM types;", NULL, NULL, NULL);
            NSLog(@"installing current types");
            [self loadInitialData];
        case 4:
            //adds support for recent view tracking
            sqlite3_exec(db, "ALTER TABLE entries ADD COLUMN touched_at TEXT;", NULL, NULL, NULL);
        case 5:
            {
                sqlite3_exec(db, "ALTER TABLE categories ADD COLUMN image TEXT;", NULL, NULL, NULL);
                sqlite3_exec(db, "ALTER TABLE categories ADD COLUMN entry_count INTEGER;", NULL, NULL, NULL);
                sqlite3_exec(db, "CREATE INDEX IF NOT EXISTS categories_id_idx ON categories(id);", NULL, NULL, NULL);
                sqlite3_exec(db, "CREATE INDEX IF NOT EXISTS categories_name_id ON categories(name);", NULL, NULL, NULL);
                sqlite3_exec(db, "CREATE INDEX IF NOT EXISTS entries_id_idx ON entries(id);", NULL, NULL, NULL);

               // etc...
            }
    }

    [self setSchemaVersion];
    [self endTransaction];
}
Billy Gray
quelle
1
Nun, ich habe nicht gesehen, wo Sie toVersionin Ihrem Code verwenden? Wie wird damit umgegangen, wenn Sie sich in Version 0 befinden und danach zwei weitere Versionen verfügbar sind? Dies bedeutet, dass Sie von 0 auf 1 und von 1 auf 2 migrieren müssen. Wie gehen Sie damit um?
Confile
1
@confile Es gibt keine breakAnweisungen in der switch, daher werden auch alle nachfolgenden Migrationen stattfinden.
Matt
Die Strip-Verbindung existiert nicht
Pedro Luz
20

Lassen Sie mich einen Migrationscode mit FMDB und MBProgressHUD teilen.

So lesen und schreiben Sie die Versionsnummer des Schemas (dies ist vermutlich Teil einer Modellklasse, in meinem Fall eine Singleton-Klasse namens Database):

- (int)databaseSchemaVersion {
    FMResultSet *resultSet = [[self database] executeQuery:@"PRAGMA user_version"];
    int version = 0;
    if ([resultSet next]) {
        version = [resultSet intForColumnIndex:0];
    }
    return version;
}

- (void)setDatabaseSchemaVersion:(int)version {
    // FMDB cannot execute this query because FMDB tries to use prepared statements
    sqlite3_exec([self database].sqliteHandle, [[NSString stringWithFormat:@"PRAGMA user_version = %d", DatabaseSchemaVersionLatest] UTF8String], NULL, NULL, NULL);
}

Hier ist eine [self database]Methode, mit der die Datenbank träge geöffnet wird:

- (FMDatabase *)database {
    if (!_databaseOpen) {
        _databaseOpen = YES;

        NSString *documentsDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
        NSString *databaseName = [NSString stringWithFormat:@"userdata.sqlite"];

        _database = [[FMDatabase alloc] initWithPath:[documentsDir stringByAppendingPathComponent:databaseName]];
        _database.logsErrors = YES;

        if (![_database openWithFlags:SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FILEPROTECTION_COMPLETE]) {
            _database = nil;
        } else {
            NSLog(@"Database schema version is %d", [self databaseSchemaVersion]);
        }
    }
    return _database;
}

Und hier sind Migrationsmethoden, die vom View Controller aufgerufen werden:

- (BOOL)databaseNeedsMigration {
    return [self databaseSchemaVersion] < databaseSchemaVersionLatest;
}

- (void)migrateDatabase {
    int version = [self databaseSchemaVersion];
    if (version >= databaseSchemaVersionLatest)
        return;

    NSLog(@"Migrating database schema from version %d to version %d", version, databaseSchemaVersionLatest);

    // ...the actual migration code...
    if (version < 1) {
        [[self database] executeUpdate:@"CREATE TABLE foo (...)"];
    }

    [self setDatabaseSchemaVersion:DatabaseSchemaVersionLatest];
    NSLog(@"Database schema version after migration is %d", [self databaseSchemaVersion]);
}

Und hier ist der Root View Controller-Code, der die Migration aufruft und MBProgressHUD verwendet, um eine Fortschrittsanzeige anzuzeigen:

- (void)viewDidAppear {
    [super viewDidAppear];
    if ([[Database sharedDatabase] userDatabaseNeedsMigration]) {
        MBProgressHUD *hud = [[MBProgressHUD alloc] initWithView:self.view.window];
        [self.view.window addSubview:hud];
        hud.removeFromSuperViewOnHide = YES;
        hud.graceTime = 0.2;
        hud.minShowTime = 0.5;
        hud.labelText = @"Upgrading data";
        hud.taskInProgress = YES;
        [[UIApplication sharedApplication] beginIgnoringInteractionEvents];

        [hud showAnimated:YES whileExecutingBlock:^{
            [[Database sharedDatabase] migrateUserDatabase];
        } onQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0) completionBlock:^{
            [[UIApplication sharedApplication] endIgnoringInteractionEvents];
        }];
    }
}
Andrey Tarantsov
quelle
Hinweis: Ich bin nicht ganz zufrieden mit der Organisation des Codes (ich würde es vorziehen, wenn das Öffnen und die Migration Teil eines einzelnen Vorgangs sind, der vorzugsweise vom App-Delegaten aufgerufen wird), aber es funktioniert, und ich dachte, ich würde es trotzdem teilen .
Andrey Tarantsov
Warum verwenden Sie die Methode "setDatabaseSchemaVersion", um "user_version" zurückzugeben? "user_version" und "schema_version" sind zwei verschiedene Pragmas, denke ich.
Paul Brewczynski
@PaulBrewczynski Weil ich die häufig verwendeten Begriffe bevorzuge, nicht SQLite-Begriffe, und ich nenne es auch so, wie es ist (die Version meines Datenbankschemas). In diesem Fall interessieren mich SQLite-spezifische Begriffe schema_versionnicht , und Pragma ist normalerweise auch nichts, mit dem sich die Leute befassen.
Andrey Tarantsov
Sie haben geschrieben: // FMDB kann diese Abfrage nicht ausführen, da FMDB versucht, vorbereitete Anweisungen zu verwenden. Was meinst du damit? Dies sollte funktionieren: NSString * query = [NSString stringWithFormat: @ "PRAGMA USER_VERSION =% i", userVersion]; [_db executeUpdate: query]; Wie hier angegeben: stackoverflow.com/a/21244261/1364174
Paul Brewczynski
1
(im Zusammenhang mit meinem obigen Kommentar) HINWEIS: Die FMDB-Bibliothek enthält jetzt folgende Funktionen: userVersion und setUserVersion: Methoden! Sie müssen also nicht die ausführlichen Methoden von @Andrey Tarantsov verwenden: - (int) databaseSchemaVersion! und (void) setDatabaseSchemaVersion: (int) version. FMDB Dokumentation: ccgus.github.io/fmdb/html/Categories/… :
Paul Brewczynski
4

Die beste Lösung IMO ist das Erstellen eines SQLite-Upgrade-Frameworks. Ich hatte das gleiche Problem (in der C # -Welt) und habe mein eigenes solches Framework erstellt. Sie können darüber lesen Sie hier . Es funktioniert perfekt und lässt meine (bisher albtraumhaften) Upgrades mit minimalem Aufwand auf meiner Seite funktionieren.

Obwohl die Bibliothek in C # implementiert ist, sollten die dort vorgestellten Ideen auch in Ihrem Fall gut funktionieren.

Liron Levi
quelle
Das ist ein schönes Werkzeug; Schade, dass es nicht kostenlos ist
Mihai Damian
3

1. Erstellen Sie einen /migrationsOrdner mit der Liste der SQL-basierten Migrationen, wobei jede Migration ungefähr so ​​aussieht:

/migrations/001-categories.sql

-- Up
CREATE TABLE Category (id INTEGER PRIMARY KEY, name TEXT);
INSERT INTO Category (id, name) VALUES (1, 'Test');

-- Down
DROP TABLE User;

/migrations/002-posts.sql

-- Up
CREATE TABLE Post (id INTEGER PRIMARY KEY, categoryId INTEGER, text TEXT);

-- Down
DROP TABLE Post;

2. Erstellen Sie eine DB-Tabelle mit der Liste der angewendeten Migrationen. Beispiel:

CREATE TABLE Migration (name TEXT);

3. Aktualisieren Sie die Bootstrap-Logik der Anwendung so, dass sie vor dem Start die Liste der Migrationen aus dem /migrationsOrdner abruft und die noch nicht angewendeten Migrationen ausführt.

Hier ist ein mit JavaScript implementiertes Beispiel: SQLite Client für Node.js Apps

Konstantin Tarkus
quelle
2

Einige Hinweise...

1) Ich empfehle, den gesamten Code für die Migration Ihrer Datenbank in eine NSOperation einzufügen und im Hintergrundthread auszuführen. Sie können eine benutzerdefinierte UIAlertView mit einem Drehfeld anzeigen, während die Datenbank migriert wird.

2) Stellen Sie sicher, dass Sie Ihre Datenbank aus dem Bundle in die Dokumente der App kopieren und von diesem Speicherort aus verwenden. Andernfalls überschreiben Sie bei jedem App-Update einfach die gesamte Datenbank und migrieren dann die neue leere Datenbank.

3) FMDB ist großartig, aber seine executeQuery-Methode kann aus irgendeinem Grund keine PRAGMA-Abfragen durchführen. Sie müssen Ihre eigene Methode schreiben, die sqlite3 direkt verwendet, wenn Sie die Schemaversion mit PRAGMA user_version überprüfen möchten.

4) Diese Codestruktur stellt sicher, dass Ihre Updates in der richtigen Reihenfolge ausgeführt werden und dass alle Updates ausgeführt werden, unabhängig davon, wie lange der Benutzer zwischen den App-Updates liegt. Es könnte weiter überarbeitet werden, aber dies ist eine sehr einfache Sichtweise. Diese Methode kann jedes Mal sicher ausgeführt werden, wenn Ihr Daten-Singleton instanziiert wird, und kostet nur eine winzige Datenbank-Abfrage, die nur einmal pro Sitzung ausgeführt wird, wenn Sie Ihren Daten-Singleton ordnungsgemäß eingerichtet haben.

- (void)upgradeDatabaseIfNeeded {
    if ([self databaseSchemaVersion] < 3)
    {
        if ([self databaseSchemaVersion] < 2)
        {
            if ([self databaseSchemaVersion] < 1)
            {
                // run statements to upgrade from 0 to 1
            }
            // run statements to upgrade from 1 to 2
        }
        // run statements to upgrade from 2 to 3

        // and so on...

        // set this to the latest version number
        [self setDatabaseSchemaVersion:3];
    }
}
Rich Joslin
quelle
1

Wenn Sie das Datenbankschema und den gesamten Code, der es verwendet, im Gleichschritt ändern, wie dies wahrscheinlich bei eingebetteten und am Telefon befindlichen Apps der Fall ist, ist das Problem tatsächlich gut unter Kontrolle (nichts Vergleichbares zu dem Albtraum, der die Schemamigration in einer Unternehmensdatenbank darstellt das kann Hunderte von Apps bedienen - auch nicht alle unter der Kontrolle des DBA ;-).

Alex Martelli
quelle
0

Für .net können Sie lib verwenden:

EntityFrameworkCore.Sqlite.Migrations

Es ist einfach, sodass Sie für jede andere Plattform problemlos das gleiche Verhalten wie in lib implementieren können.

ichensky
quelle