Best practice per la migrazione del database in-app per Sqlite


94

Sto usando sqlite per il mio iphone e prevedo che lo schema del database potrebbe cambiare nel tempo. Quali sono i trucchi, le convenzioni sui nomi e le cose a cui prestare attenzione per eseguire ogni volta una migrazione di successo?

Ad esempio, ho pensato di aggiungere una versione al nome del database (es. Database_v1).

Risposte:


111

Mantengo un'applicazione che periodicamente deve aggiornare un database sqlite e migrare i vecchi database al nuovo schema ed ecco cosa faccio:

Per tenere traccia della versione del database, utilizzo la variabile della versione utente incorporata fornita da sqlite (sqlite non fa nulla con questa variabile, sei libero di usarla come preferisci). Inizia da 0 e puoi ottenere / impostare questa variabile con le seguenti istruzioni sqlite:

> PRAGMA user_version;  
> PRAGMA user_version = 1;

All'avvio dell'app, controllo la versione utente corrente, applico le modifiche necessarie per aggiornare lo schema, quindi aggiorno la versione utente. Inserisco gli aggiornamenti in una transazione in modo che, se qualcosa va storto, le modifiche non vengono applicate.

Per apportare modifiche allo schema, sqlite supporta la sintassi "ALTER TABLE" per determinate operazioni (ridenominazione della tabella o aggiunta di una colonna). Questo è un modo semplice per aggiornare sul posto le tabelle esistenti. Consulta la documentazione qui: http://www.sqlite.org/lang_altertable.html . Per eliminare colonne o altre modifiche che non sono supportate dalla sintassi "ALTER TABLE", creo una nuova tabella, eseguo la migrazione della data, rilascio la vecchia tabella e rinomino la nuova tabella con il nome originale.


2
Sto cercando di avere la stessa logica, ma per qualche motivo quando eseguo "pragma user_version =?" a livello di codice, non riesce ... qualche idea?
Unicorn

7
le impostazioni di pragma non supportano i parametri, dovrai fornire il valore effettivo: "pragma user_version = 1".
csgero

2
Ho una domanda. Supponiamo che tu sia una versione iniziale 1. E la versione attuale è 5. Ci sono alcuni aggiornamenti nella versione 2,3,4. L'utente finale ha scaricato solo la tua versione 1 e ora aggiorna alla versione 5. Cosa dovresti fare?
Bagusflyer

6
Aggiorna il database in più passaggi, applicando le modifiche necessarie per passare dalla versione 1 alla versione 2, quindi dalla versione 2 alla versione 3, ecc ... fino a quando non sarà aggiornato. Un modo semplice per farlo è avere un'istruzione switch in cui ogni istruzione "case" aggiorna il database di una versione. Si "passa" alla versione corrente del database e le istruzioni case cadono finché l'aggiornamento non è completo. Ogni volta che aggiorni il database, aggiungi semplicemente una nuova istruzione case. Vedi la risposta sotto di Billy Gray per un esempio dettagliato di questo.
Rngbus

1
@KonstantinTarkus, secondo la documentazione application_id è un bit in più per identificare il formato del file tramite l' fileutilità, ad esempio, non per le versioni del database.
xaizek

30

La risposta di Just Curious è morta (hai capito il mio punto!), Ed è ciò che usiamo per tenere traccia della versione dello schema del database che è attualmente nell'app.

Per eseguire le migrazioni che devono essere eseguite per ottenere user_version corrispondente alla versione dello schema prevista dell'app, usiamo un'istruzione switch. Ecco un piccolo esempio di come appare nella nostra app 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];
}

1
Beh, non ho visto dove usi toVersionnel tuo codice? Come viene gestito quando sei sulla versione 0 e dopo ci sono altre due versioni. Ciò significa che devi migrare da 0 a 1 e da 1 a 2. Come gestisci questo?
confile

1
@confile non ci sono breakistruzioni in switch, quindi verranno eseguite anche tutte le migrazioni successive.
opaco

Il collegamento Strip non esiste
Pedro Luz

20

Consentitemi di condividere un po 'di codice di migrazione con FMDB e MBProgressHUD.

Ecco come leggi e scrivi il numero di versione dello schema (questo è presumibilmente parte di una classe modello, nel mio caso è una classe singleton chiamata 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);
}

Ecco il [self database]metodo che apre pigramente il database:

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

Ed ecco i metodi di migrazione chiamati dal controller di visualizzazione:

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

Ed ecco il codice del controller della vista root che richiama la migrazione, utilizzando MBProgressHUD per visualizzare una cornice di avanzamento:

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

Nota: non sono completamente soddisfatto di come è organizzato il codice (preferirei che l'apertura e la migrazione facessero parte di una singola operazione, preferibilmente invocata dal delegato dell'app), ma funziona e ho pensato di condividere comunque .
Andrey Tarantsov

Perché utilizzi il metodo "setDatabaseSchemaVersion" per restituire "user_version"? "user_version" e "schema_version" sono due pragmi diversi, credo.
Paul Brewczynski

@PaulBrewczynski Perché preferisco i termini comunemente usati, non i termini SQLite, e lo chiamo anche per quello che è (la versione dello schema del mio database). Non mi interessano i termini specifici di SQLite in questo caso e il schema_versionpragma non è normalmente qualcosa di cui le persone si occupano.
Andrey Tarantsov

Hai scritto: // FMDB non può eseguire questa query perché FMDB cerca di utilizzare istruzioni preparate. Cosa intendi con questo? Questo dovrebbe funzionare: NSString * query = [NSString stringWithFormat: @ "PRAGMA USER_VERSION =% i", userVersion]; [_db executeUpdate: query]; Come notato qui: stackoverflow.com/a/21244261/1364174
Paul Brewczynski

1
(correlato al mio commento sopra) NOTA: la libreria FMDB ora include: userVersion e setUserVersion: metodi! Quindi non devi usare i metodi dettagliati di @Andrey Tarantsov: - (int) databaseSchemaVersion! e (void) setDatabaseSchemaVersion: (int) versione. Documentazione FMDB: ccgus.github.io/fmdb/html/Categories/… :
Paul Brewczynski

4

La migliore soluzione IMO è creare un framework di aggiornamento SQLite. Ho avuto lo stesso problema (nel mondo C #) e ho costruito il mio framework di questo tipo. Puoi leggerlo qui . Funziona perfettamente e fa funzionare i miei aggiornamenti (precedentemente da incubo) con il minimo sforzo da parte mia.

Sebbene la libreria sia implementata in C #, le idee presentate dovrebbero funzionare bene anche nel tuo caso.


Questo è uno strumento carino; peccato che non sia gratis
Mihai Damian

3

1. Crea una /migrationscartella con l'elenco delle migrazioni basate su SQL, dove ogni migrazione ha un aspetto simile a questo:

/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. Crea una tabella db contenente l'elenco delle migrazioni applicate, ad esempio:

CREATE TABLE Migration (name TEXT);

3. Aggiorna la logica di bootstrap dell'applicazione in modo che, prima dell'avvio, acquisisca l'elenco delle migrazioni dalla /migrationscartella ed esegua le migrazioni che non sono state ancora applicate.

Di seguito è riportato un esempio implementato con JavaScript: SQLite Client for Node.js Apps


2

Alcuni suggerimenti...

1) Consiglio di inserire tutto il codice per migrare il database in una NSOperation e di eseguirlo nel thread in background. È possibile mostrare un UIAlertView personalizzato con una casella di selezione durante la migrazione del database.

2) Assicurati di copiare il tuo database dal bundle nei documenti dell'app e di utilizzarlo da quella posizione, altrimenti sovrascriverai semplicemente l'intero database con ogni aggiornamento dell'app, quindi migrerai il nuovo database vuoto.

3) FMDB è ottimo, ma il suo metodo executeQuery non può eseguire query PRAGMA per qualche motivo. Dovrai scrivere il tuo metodo che utilizza direttamente sqlite3 se vuoi controllare la versione dello schema usando PRAGMA user_version.

4) Questa struttura di codice garantirà che gli aggiornamenti vengano eseguiti in ordine e che tutti gli aggiornamenti vengano eseguiti, indipendentemente dal tempo che l'utente trascorre tra gli aggiornamenti dell'app. Potrebbe essere ulteriormente modificato, ma questo è un modo molto semplice per vederlo. Questo metodo può essere eseguito in modo sicuro ogni volta che viene creata un'istanza del singleton dei dati e costa solo una piccola query di database che viene eseguita solo una volta per sessione se si imposta correttamente il singleton dei dati.

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

Se modifichi lo schema del database e tutto il codice che lo utilizza in blocco, come è probabile che sia il caso delle app incorporate e localizzate sul telefono, il problema è effettivamente ben sotto controllo (niente di paragonabile all'incubo che è la migrazione dello schema su un DB aziendale potrebbe servire centinaia di app, non tutte nemmeno sotto il controllo del DBA ;-).


Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.