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

Sto usando sqlite per il mio iphone e prevedo che lo schema del database potrebbe cambiare nel tempo. Quali sono i trucchi, le convenzioni di denominazione 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 (ad es. Database_v1).

Gestisco un’applicazione che richiede periodicamente l’aggiornamento di un database SQLite e la migrazione di vecchi database nel nuovo schema e ecco cosa faccio:

Per rintracciare la versione del database, utilizzo la variabile incorporata della versione utente 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; 

Quando l’app si avvia, controllo la versione utente corrente, applico tutte le modifiche necessarie per aggiornare lo schema e poi aggiorno la versione utente. Racchiudo gli aggiornamenti in una transazione in modo che se qualcosa va storto, le modifiche non vengono commesse.

Per apportare modifiche allo schema, sqlite supporta la syntax “ALTER TABLE” per determinate operazioni (rinominando la tabella o aggiungendo una colonna). Questo è un modo semplice per aggiornare le tabelle esistenti sul posto. Vedere la documentazione qui: http://www.sqlite.org/lang_altertable.html . Per eliminare colonne o altre modifiche che non sono supportate dalla syntax “ALTER TABLE”, creo una nuova tabella, trasferisco la data in essa, rilascia la vecchia tabella e rinomina la nuova tabella con il nome originale.

La risposta di Just Curious è perfetta (hai capito il mio punto!), Ed è quello che usiamo per tracciare la versione dello schema del database che è attualmente nell’app.

Per eseguire le migrazioni che devono verificarsi per ottenere user_version che corrisponde alla versione dello schema previsto dell’app, utilizziamo un’istruzione switch. Ecco un esempio di ciò che 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]; } 

Permettetemi di condividere qualche codice di migrazione con FMDB e MBProgressHUD.

Ecco come leggi e scrivi il numero di versione dello schema (questo è presumibilmente parte di una class del modello, nel mio caso è una class 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 metodo [self database] 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; } 

E qui ci sono 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 radice che richiama la migrazione, utilizzando MBProgressHUD per visualizzare una mascherina 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]; }]; } } 

La soluzione migliore IMO è creare un framework di aggiornamento SQLite. Ho avuto lo stesso problema (nel mondo C #) e ho costruito il mio framework. Puoi leggere a riguardo qui . Funziona perfettamente e rende il mio (in precedenza incubo) gli aggiornamenti funzionano con il minimo sforzo da parte mia.

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

Se si modifica lo schema del database e tutto il codice che lo sta usando in blocco, come è probabile che si verifichi nelle app incorporate e localizzate sul telefono, il problema è in realtà ben controllato (niente paragonabile all’incubo della migrazione dello schema su un DB aziendale potrebbe servire centinaia di app, non tutte sotto il controllo del DBA ;-).

Alcuni suggerimenti…

1) Raccomando di inserire tutto il codice per migrare il tuo database in una NSOperation ed eseguirlo nel thread in background. È ansible mostrare un UIAlertView personalizzato con uno spinner durante la migrazione del database.

2) Assicurati di copiare il tuo database dal pacchetto nei documenti dell’app e usarlo da quella posizione, altrimenti sovrascrivi l’intero database con ogni aggiornamento di app, quindi esegui la migrazione del nuovo database vuoto.

3) FMDB è ottimo, ma il suo metodo executeQuery non può eseguire query PRAGMA per qualche motivo. Avrai bisogno di scrivere il tuo metodo che usa sqlite3 direttamente 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 dalla durata dell’intervallo tra gli aggiornamenti delle app. Potrebbe essere ulteriormente rifattorizzato, ma questo è un modo molto semplice per guardarlo. Questo metodo può essere eseguito in modo sicuro ogni volta che viene creata un’istanza del data singleton e costa solo una piccola query in formato db che si verifica 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 . Crea /migrations cartella con l’elenco delle migrazioni basate su SQL, dove ogni migrazione è simile a questa:

/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 . Aggiornare la logica di bootstrap dell’applicazione in modo che prima di iniziare, recuperi l’elenco delle migrazioni dalla cartella /migrations ed esegua le migrazioni che non sono ancora state applicate.

Ecco un esempio implementato con JavaScript: Client SQLite per le app Node.js