Meilleures pratiques pour la migration de base de données dans l'application pour Sqlite

94

J'utilise sqlite pour mon iPhone et je prévois que le schéma de la base de données pourrait changer avec le temps. Quels sont les pièges, les conventions de dénomination et les éléments à surveiller pour réussir à chaque fois une migration?

Par exemple, j'ai pensé à ajouter une version au nom de la base de données (par exemple Database_v1).

Aubaine
la source

Réponses:

111

Je maintiens une application qui a périodiquement besoin de mettre à jour une base de données sqlite et de migrer les anciennes bases de données vers le nouveau schéma et voici ce que je fais:

Pour suivre la version de la base de données, j'utilise la variable de version utilisateur intégrée fournie par sqlite (sqlite ne fait rien avec cette variable, vous êtes libre de l'utiliser comme bon vous semble). Il commence à 0, et vous pouvez obtenir / définir cette variable avec les instructions sqlite suivantes:

> PRAGMA user_version;  
> PRAGMA user_version = 1;

Lorsque l'application démarre, je vérifie la version actuelle de l'utilisateur, j'applique les modifications nécessaires pour mettre à jour le schéma, puis je mets à jour la version de l'utilisateur. J'emballe les mises à jour dans une transaction afin qu'en cas de problème, les modifications ne soient pas validées.

Pour apporter des modifications de schéma, sqlite prend en charge la syntaxe "ALTER TABLE" pour certaines opérations (renommer la table ou ajouter une colonne). C'est un moyen simple de mettre à jour les tables existantes sur place. Consultez la documentation ici: http://www.sqlite.org/lang_altertable.html . Pour supprimer des colonnes ou d'autres modifications qui ne sont pas prises en charge par la syntaxe "ALTER TABLE", je crée une nouvelle table, j'y migre la date, je supprime l'ancienne table et je renomme la nouvelle table avec son nom d'origine.

Rngbus
la source
2
J'essaie d'avoir la même logique, mais pour une raison quelconque lorsque j'exécute "pragma user_version =?" programmatiquement, ça échoue ... une idée?
Licorne
7
Les paramètres pragma ne prennent pas en charge les paramètres, vous devrez fournir la valeur réelle: "pragma user_version = 1".
csgero
2
J'ai une question. Disons que si vous avez une première version 1. Et la version actuelle est 5. Il y a quelques mises à jour dans la version 2,3,4. L'utilisateur final a uniquement téléchargé votre version 1, et maintenant la mise à niveau vers la version 5. Que devez-vous faire?
Bagusflyer
6
Mettre à jour la base de données en plusieurs étapes, en appliquant les modifications nécessaires pour passer de la version 1 à la version 2, puis de la version 2 à la version 3, etc ... jusqu'à ce qu'elle soit à jour. Une manière simple de faire ceci est d'avoir une instruction switch où chaque instruction "case" met à jour la base de données d'une version. Vous «basculez» vers la version actuelle de la base de données et les instructions de cas échouent jusqu'à ce que la mise à jour soit terminée. Chaque fois que vous mettez à jour la base de données, ajoutez simplement une nouvelle déclaration de cas. Voir la réponse ci-dessous par Billy Gray pour un exemple détaillé de cela.
Rngbus
1
@KonstantinTarkus, selon la documentation, application_id est un bit supplémentaire pour identifier le format de fichier par fileutilitaire par exemple, pas pour les versions de base de données.
xaizek
30

La réponse de Just Curious est sans appel (vous avez compris mon point!), Et c'est ce que nous utilisons pour suivre la version du schéma de base de données actuellement dans l'application.

Pour exécuter les migrations qui doivent se produire pour obtenir la version user_version correspondant à la version de schéma attendue de l'application, nous utilisons une instruction switch. Voici un exemple de ce à quoi cela ressemble dans notre bande d' applications :

- (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
la source
1
Eh bien, je n'ai pas vu où vous utilisez toVersiondans votre code? Comment est-il géré lorsque vous êtes sur la version 0 et qu'il y a deux autres versions après cela. Cela signifie que vous devez migrer de 0 à 1 et de 1 à 2. Comment gérez-vous cela?
confile le
1
@confile il n'y a aucune breakinstruction dans le switch, donc toutes les migrations ultérieures auront également lieu.
matte le
Le lien Strip n'existe pas
Pedro Luz
20

Permettez-moi de partager un code de migration avec FMDB et MBProgressHUD.

Voici comment vous lisez et écrivez le numéro de version du schéma (cela fait probablement partie d'une classe de modèle, dans mon cas, c'est une classe singleton appelée 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);
}

Voici la [self database]méthode qui ouvre paresseusement la base de données:

- (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;
}

Et voici les méthodes de migration appelées à partir du contrôleur de vue:

- (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]);
}

Et voici le code du contrôleur de vue racine qui appelle la migration, en utilisant MBProgressHUD pour afficher une lunette de progression:

- (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
la source
Remarque: je ne suis pas entièrement satisfait de l'organisation du code (je préférerais que l'ouverture et la migration fassent partie d'une seule opération, de préférence invoquée par le délégué de l'application), mais cela fonctionne, et j'ai pensé partager quand même .
Andrey Tarantsov
Pourquoi utilisez-vous la méthode "setDatabaseSchemaVersion" pour renvoyer "user_version"? "user_version" et "schema_version" sont deux pragmas différents, je pense.
Paul Brewczynski
@PaulBrewczynski Parce que je préfère les termes couramment utilisés, pas les termes SQLite, et aussi je l'appelle par ce qu'il est (la version de mon schéma de base de données). Je ne me soucie pas des termes spécifiques à SQLite dans ce cas, et schema_versionpragma n'est normalement pas quelque chose que les gens traitent non plus.
Andrey Tarantsov
Vous avez écrit: // FMDB ne peut pas exécuter cette requête car FMDB essaie d'utiliser des instructions préparées. Que veux-tu dire par là? Cela devrait fonctionner: NSString * query = [NSString stringWithFormat: @ "PRAGMA USER_VERSION =% i", userVersion]; [_db executeUpdate: query]; Comme indiqué ici: stackoverflow.com/a/21244261/1364174
Paul Brewczynski
1
(lié à mon commentaire ci-dessus) REMARQUE: la bibliothèque FMDB comprend désormais: userVersion et setUserVersion: méthodes! Vous n'avez donc pas besoin d'utiliser les méthodes verbeuses de @Andrey Tarantsov: - (int) databaseSchemaVersion! et (void) setDatabaseSchemaVersion: version (int). Documentation FMDB: ccgus.github.io/fmdb/html/Categories/… :
Paul Brewczynski
4

La meilleure solution IMO est de construire un framework de mise à niveau SQLite. J'ai eu le même problème (dans le monde C #) et j'ai construit mon propre framework. Vous pouvez en savoir plus ici . Cela fonctionne parfaitement et fait fonctionner mes mises à niveau (auparavant cauchemardesques) avec un minimum d'effort de ma part.

Bien que la bibliothèque soit implémentée en C #, les idées qui y sont présentées devraient également fonctionner correctement dans votre cas.

Liron Levi
la source
C'est un bel outil; dommage que ce ne soit pas gratuit
Mihai Damian
3

1. Créez un /migrationsdossier avec la liste des migrations basées sur SQL, où chaque migration ressemble à ceci:

/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. Créez une table de base de données contenant la liste des migrations appliquées, par exemple:

CREATE TABLE Migration (name TEXT);

3. Mettez à jour la logique d'amorçage de l'application afin qu'avant son démarrage, elle récupère la liste des migrations du /migrationsdossier et exécute les migrations qui n'ont pas encore été appliquées.

Voici un exemple implémenté avec JavaScript: Client SQLite pour les applications Node.js

Konstantin Tarkus
la source
2

Quelques conseils...

1) Je recommande de mettre tout le code pour migrer votre base de données dans une NSOperation et de l'exécuter dans le thread d'arrière-plan. Vous pouvez afficher un UIAlertView personnalisé avec une double flèche pendant la migration de la base de données.

2) Assurez-vous que vous copiez votre base de données du bundle dans les documents de l'application et que vous l'utilisez à partir de cet emplacement, sinon vous allez simplement écraser toute la base de données avec chaque mise à jour de l'application, puis migrer la nouvelle base de données vide.

3) FMDB est excellent, mais sa méthode executeQuery ne peut pas faire de requêtes PRAGMA pour une raison quelconque. Vous devrez écrire votre propre méthode qui utilise directement sqlite3 si vous souhaitez vérifier la version du schéma à l'aide de PRAGMA user_version.

4) Cette structure de code garantira que vos mises à jour sont exécutées dans l'ordre et que toutes les mises à jour sont exécutées, quel que soit le temps que l'utilisateur passe entre les mises à jour de l'application. Il pourrait être remanié davantage, mais c'est une façon très simple de l'examiner. Cette méthode peut être exécutée en toute sécurité chaque fois que votre singleton de données est instancié et ne coûte qu'une minuscule requête de base de données qui ne se produit qu'une fois par session si vous configurez correctement votre singleton de données.

- (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
la source
1

Si vous modifiez le schéma de la base de données et tout le code qui l'utilise de manière synchronisée, comme c'est probablement le cas dans les applications intégrées et localisées sur le téléphone, le problème est en fait bien sous contrôle (rien de comparable au cauchemar qu'est la migration de schéma sur une base de données d'entreprise qui peuvent servir des centaines d'applications - pas toutes non plus sous le contrôle du DBA ;-).

Alex Martelli
la source
0

Pour .net, vous pouvez utiliser lib:

EntityFrameworkCore.Sqlite.Migrations

C'est simple, donc pour toute autre plate-forme, vous pouvez facilement implémenter le même comportement que dans lib.

Ichensky
la source