web-dev-qa-db-fra.com

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

J'utilise sqlite pour mon iphone et je prévois que le schéma de 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 une migration à chaque fois?

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

78
Boon

Je gère une application qui doit périodiquement mettre à jour une base de données sqlite et migrer d'anciennes bases de données vers le nouveau schéma. Voici ce que je fais:

Pour suivre la version de la base de données, j'utilise la variable utilisateur intégrée à la version 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, applique les modifications nécessaires à la mise à jour du schéma, puis met à 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 effectuer 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 facile de mettre à jour les tables existantes sur place. Voir la documentation ici: http://www.sqlite.org/lang_altertable.html . Pour supprimer des colonnes ou d'autres modifications non prises en charge par la syntaxe "ALTER TABLE", je crée une nouvelle table, migre la date dans celle-ci, supprime l'ancienne table et renomme la nouvelle table avec le nom d'origine.

96
Rngbus

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 avoir lieu pour que version_utilisateur corresponde à la version de schéma attendue de l'application, nous utilisons une instruction switch. Voici un exemple détaillé de ce à quoi cela ressemble dans notre application Strip :

- (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];
}
28
Billy Gray

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 méthode [self database] 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 un cadre 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];
        }];
    }
}
19
Andrey Tarantsov

La meilleure solution IMO consiste à créer un framework de mise à niveau SQLite. J'ai eu le même problème (dans le monde C #) et j'ai construit mon propre cadre. Vous pouvez lire à ce sujet ici . Cela fonctionne parfaitement et fait fonctionner mes mises à niveau (auparavant cauchemardesques) avec un minimum d'effort de mon côté.

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

4
Liron Levi

1. Créez un dossier /migrations 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 de démarrage de l'application afin qu'avant son démarrage, elle récupère la liste des migrations à partir du dossier /migrations et exécute les migrations qui n'ont pas encore été appliquées.

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

2

Si vous modifiez le schéma de base de données et tout le code qui l'utilise en bloc, comme c'est probablement le cas dans les applications intégrées et localisées, le problème est en réalité bien maîtrisé (rien de comparable au cauchemar que constitue la migration de schéma sur une base de données d'entreprise cela peut servir des centaines d'applications - pas tous sous le contrôle de la DBA non plus ;-).

1
Alex Martelli

Quelques conseils...

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

2) Assurez-vous de copier votre base de données de l'ensemble dans les documents de l'application et de l'utiliser depuis cet emplacement, sinon vous écraserez toute la base de données à chaque mise à jour de l'application, puis migrez la nouvelle base de données vide.

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

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 refait plus avant, mais c'est une façon très simple de voir les choses. 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 petite requête de base de données qui ne se produit qu'une fois par session si vous configurez votre singleton de données correctement.

- (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];
    }
}
1
Rich Joslin

Pour .net, vous pouvez utiliser lib: 

EntityFrameworkCore.Sqlite.Migrations

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

0
ichensky