2009-06-13 14 views

Respuesta

89

que mantienen una aplicación que necesita periódicamente para actualizar una base de datos SQLite y migrar bases de datos antiguos al nuevo esquema y esto es lo que hago:

para el seguimiento de la versión de la base de datos, yo uso el construido en la variable de usuario, versión que sqlite proporciona (sqlite no hace nada con esta variable, puedes usarla como quieras). Se inicia en 0, y se puede obtener/establecer esta variable con las siguientes afirmaciones sqlite:

> PRAGMA user_version; 
> PRAGMA user_version = 1; 

Cuando se inicia la aplicación, puedo comprobar la facilidad de uso versión actual, aplicar los cambios que son necesarios para llevar el esquema de arriba hasta la fecha, y luego actualiza la versión de usuario. Envuelvo las actualizaciones en una transacción para que, si algo sale mal, los cambios no se confirmen.

Para realizar cambios en el esquema, sqlite admite la sintaxis "ALTER TABLE" para ciertas operaciones (cambiar el nombre de la tabla o agregar una columna). Esta es una manera fácil de actualizar las tablas existentes en el lugar. Consulte la documentación aquí: http://www.sqlite.org/lang_altertable.html. Para eliminar columnas u otros cambios que no son compatibles con la sintaxis de "ALTER TABLE", creo una nueva tabla, migro la fecha, dejo caer la tabla anterior y renombro la nueva tabla con el nombre original.

+1

Estoy tratando de tener misma lógica, pero por alguna razón cuando ejecuto "user_version pragma =?" programáticamente, falla ... ¿alguna idea? – Unicorn

+6

La configuración de pragma no admite parámetros; deberá proporcionar el valor real: "pragma user_version = 1". – csgero

+1

Tengo una pregunta. Digamos si tiene una versión inicial 1. Y la versión actual es 5. Hay algunas actualizaciones en la versión 2,3,4. El usuario final solo descargó su versión 1 y ahora la actualiza a la versión 5. ¿Qué debe hacer? –

1

Si cambia el esquema de la base de datos y todo el código que lo usa en bloque, como es probable en las aplicaciones integradas y ubicadas en el teléfono, el problema está bajo control (nada comparable a la pesadilla que es la migración de esquema en una base de datos empresarial que puede estar sirviendo cientos de aplicaciones, no todas bajo el control del DBA ;-).

28

La respuesta de Just Curious es irrelevante (ya entendiste mi punto), y es lo que usamos para rastrear la versión del esquema de la base de datos que se encuentra actualmente en la aplicación.

Para ejecutar las migraciones que deben producirse para obtener la versión de esquema de la aplicación esperada por user_version, utilizamos una declaración de cambio. He aquí un ejemplo de cut-up de lo que esto parezca en nuestra aplicación 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

Bueno, no vi dónde usas '' toVersion'' en tu código? ¿Cómo se maneja cuando estás en la versión 0 y hay dos versiones más después de eso? Esto significa que debe migrar de 0 a 1 y de 1 a 2. ¿Cómo maneja esto? – confile

+0

@confile no hay sentencias 'break' en el' switch', por lo que todas las migraciones posteriores también ocurrirán. – matte

4

La mejor solución es la OMI para construir un marco de actualización SQLite. Tuve el mismo problema (en el mundo C#) y construí mi propio marco. Puede leer al respecto here. Funciona perfectamente y hace que mis actualizaciones (previamente de pesadilla) funcionen con el mínimo esfuerzo de mi parte.

Aunque la biblioteca se implementa en C#, las ideas presentadas allí también deberían funcionar bien en su caso.

+0

Esa es una buena herramienta; Lástima que no es gratis –

+4

Vaya hombre, es 9.95. – cheez

1

Algunos consejos ...

1) Recomiendo poner todo el código para migrar su base de datos en un NSOperation y ejecutarlo en el hilo de fondo. Puede mostrar un UIAlertView personalizado con un spinner mientras se está migrando la base de datos.

2) Asegúrese de copiar su base de datos del paquete en los documentos de la aplicación y usarla desde esa ubicación; de lo contrario, sobrescribirá toda la base de datos con cada actualización de aplicación y luego migrará la nueva base de datos vacía.

3) FMDB es genial, pero su método executeQuery no puede hacer consultas PRAGMA por algún motivo. Tendrá que escribir su propio método que use sqlite3 directamente si desea verificar la versión del esquema utilizando PRAGMA user_version.

4) Esta estructura de código se asegurará de que las actualizaciones se ejecuten en orden y de que se ejecuten todas las actualizaciones, sin importar cuánto tiempo pase el usuario entre las actualizaciones de la aplicación. Podría ser refactorizado aún más, pero esta es una manera muy simple de verlo. Este método se puede ejecutar de manera segura cada vez que se crea una instancia de su singleton de datos, y solo le cuesta una pequeña consulta db que solo ocurre una vez por sesión si configura su singleton de datos correctamente.

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

Déjame compartir un código de migración con FMDB y MBProgressHUD.

Así es como leer y escribir el número de versión de esquema (esto es, presumiblemente, parte de una clase de modelo, en mi caso se trata de una clase Singleton llamada base de datos):

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

Aquí es [self database] método que perezosamente se abre la base de datos :

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

Y aquí son llamados métodos de migración desde el controlador de vista:

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

Y aquí está el código de controlador de vista raíz que invoca la migración, utilizando MBProgressHUD para mostrar un bisel de progreso:

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

Nota: no estoy completamente satisfecho con la forma en que está organizado el código (prefiero que la apertura y la migración sean parte de una sola operación, preferiblemente invocada por el delegado de la aplicación), pero funciona, y pensé que compartiría de todas formas. –

+0

¿Por qué usa el método "setDatabaseSchemaVersion" para devolver "user_version"? "user_version" y "schema_version" son dos pragmas diferentes, creo. –

+0

@PaulBrewczynski Porque prefiero los términos comúnmente utilizados, no los términos SQLite, y también los llamo por lo que es (la versión de mi esquema de base de datos). No me importan los términos específicos de SQLite en este caso, y el pragma 'schema_version' tampoco es algo con lo que la gente se relacione. –

1

1. Crear /migrations carpeta con la lista de las migraciones basadas en SQL, donde cada migración se ve algo como esto:

/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. Crear db tabla que contiene la lista de las migraciones aplicados, por ejemplo:

CREATE TABLE Migration (name TEXT); 

3. Actualice la lógica de arranque de la aplicación para que, antes de comenzar, capture la lista de migraciones de la carpeta /migrations y ejecute las migraciones que aún no se hayan aplicado.

Aquí es un ejemplo implementado con JavaScript: SQLite Client for Node.js Apps

Cuestiones relacionadas