Práticas recomendadas para migração de banco de dados no aplicativo para Sqlite

94

Estou usando o sqlite para o meu iphone e prevejo que o esquema do banco de dados pode mudar com o tempo. Quais são as pegadinhas, convenções de nomenclatura e coisas a serem observadas para sempre fazer uma migração bem-sucedida?

Por exemplo, pensei em acrescentar uma versão ao nome do banco de dados (por exemplo, Database_v1).

Boon
fonte

Respostas:

111

Eu mantenho um aplicativo que precisa atualizar periodicamente um banco de dados sqlite e migrar bancos de dados antigos para o novo esquema e aqui está o que eu faço:

Para rastrear a versão do banco de dados, eu uso a variável integrada de versão do usuário que o sqlite fornece (o sqlite não faz nada com essa variável, você pode usá-la como quiser). Ele começa em 0 e você pode obter / definir essa variável com as seguintes instruções sqlite:

> PRAGMA user_version;  
> PRAGMA user_version = 1;

Quando o aplicativo é iniciado, verifico a versão do usuário atual, aplico as alterações necessárias para atualizar o esquema e, em seguida, atualizo a versão do usuário. Eu envolvo as atualizações em uma transação para que, se algo der errado, as alterações não sejam confirmadas.

Para fazer alterações no esquema, o sqlite suporta a sintaxe "ALTER TABLE" para certas operações (renomear a tabela ou adicionar uma coluna). Esta é uma maneira fácil de atualizar as tabelas existentes no local. Consulte a documentação aqui: http://www.sqlite.org/lang_altertable.html . Para excluir colunas ou outras alterações que não são suportadas pela sintaxe "ALTER TABLE", eu crio uma nova tabela, migro a data para ela, elimino a tabela antiga e renomeio a nova tabela com o nome original.

Rngbus
fonte
2
Estou tentando ter a mesma lógica, mas por algum motivo quando executo "pragma user_version =?" programaticamente, ele falha ... alguma ideia?
Unicórnio
7
As configurações de pragma não oferecem suporte a parâmetros, você terá que fornecer o valor real: "pragma user_version = 1".
csgero
2
Eu tenho uma pergunta. Digamos que se você tem uma versão inicial 1. E a versão atual é 5. Existem algumas atualizações na versão 2,3,4. O usuário final baixou apenas sua versão 1 e agora atualize para a versão 5. O que você deve fazer?
Bagusflyer
6
Atualizar o banco de dados em várias etapas, aplicando as alterações necessárias para passar da versão 1 para a versão 2, depois da versão 2 para a versão 3, etc ... até que esteja atualizado. Uma maneira fácil de fazer isso é ter uma instrução switch em que cada instrução "case" atualiza o banco de dados em uma versão. Você "alterna" para a versão atual do banco de dados e as instruções do caso são perdidas até que a atualização seja concluída. Sempre que você atualizar o banco de dados, basta adicionar uma nova instrução de caso. Veja a resposta de Billy Gray abaixo para um exemplo detalhado disso.
Rngbus
1
@KonstantinTarkus, de acordo com a documentação, application_id é um bit extra para identificar o formato do arquivo por fileutilitário por exemplo, não para versões de banco de dados.
xaizek
30

A resposta de Just Curious é perfeita (você entendeu!) E é o que usamos para rastrear a versão do esquema de banco de dados que está atualmente no aplicativo.

Para executar as migrações que precisam ocorrer para obter user_version correspondente à versão de esquema esperada do aplicativo, usamos uma instrução switch. Aqui está um exemplo recortado de como isso se parece em nosso aplicativo 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 Gray
fonte
1
Bem, eu não vi onde você usa toVersionem seu código? Como isso é tratado quando você está na versão 0 e há mais duas versões depois disso. Isso significa que você deve migrar de 0 para 1 e de 1 para 2. Como você lida com isso?
confile em
1
@confile não há breakinstruções no switch, então todas as migrações subsequentes também acontecerão.
matte de
O link Strip não existe
Pedro Luz
20

Deixe-me compartilhar alguns códigos de migração com FMDB e MBProgressHUD.

Veja como você lê e escreve o número da versão do esquema (presumivelmente, faz parte de uma classe de modelo, no meu caso é uma classe singleton chamada 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);
}

Este é o [self database]método que abre lentamente o banco de dados:

- (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 aqui estão os métodos de migração chamados a partir do controlador de visualização:

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

E aqui está o código do controlador de visualização raiz que invoca a migração, usando MBProgressHUD para exibir um painel de progresso:

- (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
fonte
Observação: não estou totalmente satisfeito com a forma como o código está organizado (prefiro que a abertura e a migração sejam partes de uma única operação, de preferência invocada pelo delegado do aplicativo), mas funciona e pensei em compartilhar mesmo assim .
Andrey Tarantsov,
Por que você usa o método "setDatabaseSchemaVersion" para retornar "user_version"? "user_version" e "schema_version" são dois pragmas diferentes, eu acho.
Paul Brewczynski
@PaulBrewczynski Porque prefiro os termos comumente usados, não os termos SQLite, e também estou chamando pelo que é (a versão do meu esquema de banco de dados). Eu não me importo com termos específicos do SQLite neste caso, e schema_versionpragma normalmente não é algo com que as pessoas lidam.
Andrey Tarantsov
Você escreveu: // FMDB não pode executar esta consulta porque FMDB tenta usar instruções preparadas. O que você quer dizer com isso? Isso deve funcionar: NSString * query = [NSString stringWithFormat: @ "PRAGMA USER_VERSION =% i", userVersion]; [_db executeUpdate: query]; Conforme observado aqui: stackoverflow.com/a/21244261/1364174
Paul Brewczynski
1
(relacionado ao meu comentário acima) NOTA: A biblioteca FMDB agora apresenta: métodos userVersion e setUserVersion:! Assim, você não precisa usar os métodos detalhados de @Andrey Tarantsov: - (int) databaseSchemaVersion! e (void) setDatabaseSchemaVersion: (int) version. Documentação do FMDB: ccgus.github.io/fmdb/html/Categories/… :
Paul Brewczynski
4

A melhor solução da IMO é construir uma estrutura de atualização do SQLite. Eu tive o mesmo problema (no mundo C #) e construí meu próprio framework. Você pode ler sobre isso aqui . Ele funciona perfeitamente e faz com que minhas atualizações (antes terríveis) funcionem com o mínimo de esforço da minha parte.

Embora a biblioteca seja implementada em C #, as idéias apresentadas devem funcionar bem no seu caso também.

Liron Levi
fonte
Essa é uma boa ferramenta; pena que não é grátis
Mihai Damian
3

1. Crie uma /migrationspasta com a lista de migrações baseadas em SQL, onde cada migração se parece com isto:

/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. Crie uma tabela de banco de dados contendo a lista de migrações aplicadas, por exemplo:

CREATE TABLE Migration (name TEXT);

3. Atualize a lógica de bootstrap do aplicativo para que antes de iniciar, pegue a lista de migrações da /migrationspasta e execute as migrações que ainda não foram aplicadas.

Aqui está um exemplo implementado com JavaScript: Cliente SQLite para aplicativos Node.js

Konstantin Tarkus
fonte
2

Algumas dicas...

1) Eu recomendo colocar todo o código para migrar seu banco de dados em um NSOperation e executá-lo em segundo plano. Você pode mostrar um UIAlertView personalizado com um botão giratório enquanto o banco de dados está sendo migrado.

2) Certifique-se de copiar seu banco de dados do pacote para os documentos do aplicativo e usá-lo desse local, caso contrário, você apenas substituirá todo o banco de dados a cada atualização do aplicativo e, em seguida, migrará o novo banco de dados vazio.

3) FMDB é ótimo, mas seu método executeQuery não pode fazer consultas PRAGMA por algum motivo. Você precisará escrever seu próprio método que usa sqlite3 diretamente se quiser verificar a versão do esquema usando PRAGMA user_version.

4) Esta estrutura de código irá garantir que suas atualizações sejam executadas em ordem, e que todas as atualizações sejam executadas, não importa quanto tempo o usuário fica entre as atualizações do aplicativo. Ele poderia ser refatorado posteriormente, mas esta é uma maneira muito simples de olhar para ele. Esse método pode ser executado com segurança toda vez que seu singleton de dados é instanciado e custa apenas uma pequena consulta db que só acontece uma vez por sessão se você configurar seu singleton de dados corretamente.

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

Se você alterar o esquema do banco de dados e todo o código que o está usando em um lockstep, como é provável que seja o caso em aplicativos integrados e localizados por telefone, o problema está realmente bem sob controle (nada comparável ao pesadelo que é a migração do esquema em um banco de dados corporativo que pode servir centenas de aplicativos - nem todos sob o controle do DBA ;-).

Alex Martelli
fonte
0

Para .net, você pode usar lib:

EntityFrameworkCore.Sqlite.Migrations

É simples, portanto, para qualquer outra plataforma, você pode implementar facilmente o mesmo comportamento do lib.

Ichensky
fonte