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


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, convenciones de nombres y cosas de las que hay que tener cuidado para hacer 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).


75
2018-06-13 00:02


origen


Respuestas:


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

Para rastrear la versión de la base de datos, uso la variable de versión de usuario incorporada que proporciona sqlite (sqlite no hace nada con esta variable, puede usarla libremente). 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, compruebo la versión de usuario actual, aplico los cambios que sean 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 de "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. Vea 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.


89
2018-06-15 22:04



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 que user_version coincida con la versión de esquema esperada de la aplicación, utilizamos una declaración de cambio. Aquí hay un ejemplo fragmentado de cómo se ve esto en nuestra aplicación Tira:

- (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
2018-06-29 14:15



Permítanme compartir algunos códigos de migración con FMDB y MBProgressHUD.

A continuación, le mostramos cómo leer y escribir el número de versión del esquema (esto es, presumiblemente, parte de una clase de modelo, en mi caso se trata de una clase única 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á [self database] método que 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í 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 de 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];
        }];
    }
}

18
2017-09-17 23:24



La mejor solución IMO es construir un marco de actualización de SQLite. Tuve el mismo problema (en el mundo C #) y construí mi propio marco. Puedes leer sobre eso aquí. 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.


4
2017-07-30 07:30



Si cambia el esquema de la base de datos y todo el código que lo usa en bloque, como probablemente sea el caso en aplicaciones incrustadas y ubicadas en el teléfono, el problema está realmente 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 ;-).


1
2018-06-13 00:08



Algunos consejos...

1) Recomiendo poner todo el código para migrar su base de datos a 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 la 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 asegurará que sus actualizaciones se ejecuten en orden y que todas las actualizaciones se ejecuten, sin importar cuánto tiempo pase 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];
    }
}

1
2017-09-28 19:54



1. Crear /migrations carpeta con la lista de migraciones basadas en SQL, donde cada migración se ve así:

/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 db 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, capte la lista de migraciones del /migrations carpeta y ejecuta las migraciones que aún no se han aplicado.

Aquí hay un ejemplo implementado con JavaScript: Cliente SQLite para aplicaciones Node.js


1
2018-05-20 18:45