Mejores prácticas para la migración de bases de datos en la aplicación para Sqlite

94

Estoy usando sqlite para mi iphone y anticipo que el esquema de la base de datos podría cambiar con el tiempo. ¿Cuáles son las trampas, las convenciones de nomenclatura y las cosas a tener en cuenta para realizar una migración exitosa cada vez?

Por ejemplo, he pensado en agregar una versión al nombre de la base de datos (por ejemplo, Database_v1).

Bendición
fuente

Respuestas:

111

Mantengo una aplicación que periódicamente necesita actualizar una base de datos sqlite y migrar bases de datos antiguas al nuevo esquema y esto es lo que hago:

Para rastrear la versión de la base de datos, utilizo la variable de versión de usuario incorporada que proporciona sqlite (sqlite no hace nada con esta variable, usted es libre de usarla como quiera). Comienza en 0 y puede obtener / establecer esta variable con las siguientes declaraciones sqlite:

> PRAGMA user_version;  
> PRAGMA user_version = 1;

Cuando se inicia la aplicación, verifico la versión de usuario actual, aplico los cambios necesarios para actualizar el esquema y luego actualizo 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 de esquema, sqlite admite la sintaxis "ALTER TABLE" para ciertas operaciones (cambiar el nombre de la tabla o agregar una columna). Esta es una forma sencilla 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 "ALTER TABLE", creo una nueva tabla, migro la fecha en ella, elimino la tabla anterior y cambio el nombre de la nueva tabla al nombre original.

Rngbus
fuente
2
Estoy tratando de tener la misma lógica, pero por alguna razón cuando ejecuto "pragma user_version =?" programáticamente, falla ... ¿alguna idea?
Unicornio
7
La configuración de pragma no admite parámetros, deberá proporcionar el valor real: "pragma user_version = 1".
csgero
2
Tengo una pregunta. Digamos si tienes 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 actualiza a la versión 5. ¿Qué debe hacer?
Bagusflyer
6
Actualice la base de datos en varios pasos, aplicando los cambios necesarios para pasar de la versión 1 a la versión 2, luego de la versión 2 a la versión 3, etc ... hasta que esté actualizada. Una manera fácil de hacer esto es tener una declaración de cambio donde cada declaración de "caso" actualice la base de datos en una versión. Usted "cambia" a la versión actual de la base de datos y las declaraciones del caso no funcionan hasta que se completa la actualización. Siempre que actualice la base de datos, simplemente agregue una nueva declaración de caso. Vea la respuesta de Billy Gray a continuación para obtener un ejemplo detallado de esto.
Rngbus
1
@KonstantinTarkus, de acuerdo con la documentación, application_id es un bit extra para identificar el formato de archivo por fileutilidad, por ejemplo, no para las versiones de la base de datos.
xaizek
30

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

Para ejecutar las migraciones que deben ocurrir para que user_version coincida con la versión de esquema esperada de la aplicación, usamos una declaración de cambio. Aquí hay un ejemplo cortado de cómo se ve esto 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];
}
Billy Grey
fuente
1
Bueno, no vi dónde lo usas toVersionen tu código. ¿Cómo se maneja cuando está 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
1
@confile no hay breakdeclaraciones en el switch, por lo que todas las migraciones posteriores también ocurrirán.
mate
El vínculo Strip no existe
Pedro Luz
20

Permítanme compartir un código de migración con FMDB y MBProgressHUD.

Así es como lee y escribe el número de versión del esquema (esto es presumiblemente parte de una clase modelo, en mi caso es 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í está el [self database]método que abre perezosamente 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í están los métodos de migración llamados 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 del controlador de vista raíz que invoca la migración, usando 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];
        }];
    }
}
Andrey Tarantsov
fuente
Nota: no estoy completamente satisfecho con la forma en que está organizado el código (preferiría 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é en compartirlo de todos modos. .
Andrey Tarantsov
¿Por qué utiliza el método "setDatabaseSchemaVersion" para devolver "user_version"? "user_version" y "schema_version" son dos pragmas diferentes, creo.
Paul Brewczynski
@PaulBrewczynski Porque prefiero los términos de uso común, no los términos SQLite, y también los llamo por lo que es (la versión del esquema de mi base de datos). En este caso, no me importan los términos específicos de SQLite, y el schema_versionpragma tampoco es algo con lo que la gente se enfrente normalmente.
Andrey Tarantsov
Ha escrito: // FMDB no puede ejecutar esta consulta porque FMDB intenta usar declaraciones preparadas. ¿Qué quiere decir con esto? Esto debería funcionar: NSString * query = [NSString stringWithFormat: @ "PRAGMA USER_VERSION =% i", userVersion]; [_db executeUpdate: consulta]; Como se indica aquí: stackoverflow.com/a/21244261/1364174
Paul Brewczynski
1
(relacionado con mi comentario anterior) NOTA: la biblioteca FMDB ahora presenta: userVersion y setUserVersion: methods! Por lo tanto, no tiene que usar los métodos detallados de @Andrey Tarantsov: - (int) databaseSchemaVersion! y (void) setDatabaseSchemaVersion: (int) versión. Documentación de FMDB: ccgus.github.io/fmdb/html/Categories/… :
Paul Brewczynski
4

En mi opinión, la mejor solución es crear un marco de actualización de SQLite. Tuve el mismo problema (en el mundo C #) y construí mi propio marco de trabajo. Puedes leer sobre esto aquí . Funciona perfectamente y hace que mis actualizaciones (que antes eran de pesadilla) funcionen con un mínimo esfuerzo de mi parte.

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

Liron Levi
fuente
Esa es una buena herramienta; lástima que no es gratis
Mihai Damian
3

1. Cree una /migrationscarpeta con la lista de migraciones basadas en SQL, donde cada migración se parece a 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. Cree una tabla de base de datos que contenga la lista de migraciones aplicadas, por ejemplo:

CREATE TABLE Migration (name TEXT);

3. Actualice la lógica de arranque de la aplicación para que, antes de que comience, tome la lista de migraciones de la /migrationscarpeta y ejecute las migraciones que aún no se han aplicado.

A continuación se muestra un ejemplo implementado con JavaScript: SQLite Client para aplicaciones Node.js

Konstantin Tarkus
fuente
2

Algunos consejos...

1) Recomiendo poner todo el código para migrar su base de datos a una NSOperation y ejecutarlo en segundo plano. Puede mostrar un UIAlertView personalizado con un control giratorio mientras se migra la base de datos.

2) Asegúrese de que está copiando su base de datos del paquete en los documentos de la aplicación y usándola desde esa ubicación; de lo contrario, simplemente sobrescribirá la base de datos completa con cada actualización de la aplicación y luego migrará la nueva base de datos vacía.

3) FMDB es genial, pero su método executeQuery no puede realizar consultas PRAGMA por alguna razón. Deberá escribir su propio método que use sqlite3 directamente si desea verificar la versión del esquema usando PRAGMA user_version.

4) Esta estructura de código asegurará que sus actualizaciones se ejecuten en orden y que todas las actualizaciones se ejecuten, sin importar cuánto tiempo pase el usuario entre actualizaciones de la aplicación. Podría refactorizarse aún más, pero esta es una forma muy simple de verlo. Este método se puede ejecutar de forma segura cada vez que se crea una instancia de su singleton de datos, y solo cuesta una pequeña consulta de 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];
    }
}
Rich Joslin
fuente
1

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

Alex Martelli
fuente