Рекомендации по миграции базы данных в приложении для Sqlite


Я использую sqlite для моего iphone, и я ожидаю, что схема базы данных может измениться с течением времени. Каковы gotchas, соглашения об именах и вещи, чтобы следить за тем, чтобы сделать успешную миграцию каждый раз?

например, я подумал о добавлении версии к имени базы данных (например, Database_v1).

7 75

7 ответов:

Я поддерживаю приложение, которое периодически должно обновлять базу данных sqlite и переносить старые базы данных в новую схему, и вот что я делаю:

для отслеживания версии базы данных я использую встроенную переменную user-version, которую предоставляет sqlite (sqlite ничего не делает с этой переменной, вы можете использовать ее, как вам угодно). Он начинается с 0, и вы можете получить/установить эту переменную со следующими инструкциями sqlite:

> PRAGMA user_version;  
> PRAGMA user_version = 1;

когда приложение запускается, я проверяю текущего пользователя-версия, применить все изменения, которые необходимы, чтобы довести схему до дата, а затем обновить пользователя-версия. Я упаковываю обновления в транзакцию, чтобы, если что-то пойдет не так, изменения не были зафиксированы.

для внесения изменений в схему sqlite поддерживает синтаксис "ALTER TABLE" для определенных операций (переименование таблицы или добавление столбца). Это простой способ обновить существующие таблицы на месте. Смотрите документацию здесь: http://www.sqlite.org/lang_altertable.html. для удаления столбцов или других изменений, которые не поддерживаются синтаксисом "ALTER TABLE", я создаю новую таблицу, переношу в нее дату, удаляю старую таблицу и переименовываю новую таблицу в исходное имя.

ответ от Just Curious мертв (вы поняли мою точку зрения!), и это то, что мы используем, чтобы отслеживать версии схемы базы данных, которая в настоящее время находится в приложении.

чтобы выполнить миграции, которые должны произойти, чтобы получить user_version, соответствующий ожидаемой версии схемы приложения, мы используем оператор switch. Вот пример того, как это выглядит в нашем приложении прокладки:

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

позвольте мне поделиться некоторым кодом миграции с FMDB и MBProgressHUD.

вот как Вы читаете и записываете номер версии схемы (это, по-видимому, часть класса модели, в моем случае это одноэлементный класс, называемый базой данных):

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

здесь [self 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;
}

и вот методы миграции, вызываемые из контроллера вида:

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

и вот корневой код контроллера вида, который вызывает миграция, используя MBProgressHUD для отображения панели прогресса:

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

лучшим решением IMO является создание платформы обновления SQLite. У меня была такая же проблема (в мире C#) и я построил свою собственную такие рамки. Вы можете прочитать об этом здесь. Он отлично работает и заставляет мои (ранее кошмарные) обновления работать с минимальными усилиями на моей стороне.

хотя библиотека реализована в C#, идеи, представленные там, должны работать нормально и в вашем случае.

Если вы измените схему базы данных и весь код, который использует ее в lockstep, как это, вероятно, будет иметь место во встроенных и телефонных приложениях, проблема на самом деле хорошо контролируется (ничто не сравнимо с кошмаром миграции схемы на корпоративной БД, которая может обслуживать сотни приложений-не все под контролем DBA;-).

некоторые советы...

1) я рекомендую поместить весь код для переноса вашей базы данных в NSOperation и запустить его в фоновом потоке. Вы можете показать пользовательский UIAlertView с помощью spinner во время миграции базы данных.

2) Убедитесь, что вы копируете свою базу данных из пакета в документы приложения и используете ее из этого места, иначе вы просто перезапишете всю базу данных с каждым обновлением приложения, а затем перенесете новый пустой база данных.

3) FMDB отлично, но его метод executeQuery по какой-то причине не может выполнять запросы PRAGMA. Вам нужно будет написать свой собственный метод, который использует sqlite3 напрямую, если вы хотите проверить версию схемы с помощью PRAGMA user_version.

4) эта структура кода гарантирует, что ваши обновления выполняются по порядку, и что все обновления выполняются, независимо от того, как долго пользователь переходит между обновлениями приложения. Это может быть рефакторинг дальше, но это очень простой способ посмотреть на него. Этот метод можно безопасно запускать каждый раз, когда создается экземпляр вашего синглтона данных, и стоит только один крошечный запрос БД, который выполняется только один раз за сеанс, если вы правильно настроили свой синглтон данных.

- (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. Создать /migrations папка со списком миграций на основе SQL, где каждая миграция выглядит примерно так:

/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. Создайте таблицу БД, содержащую список применяемых миграций, например:

CREATE TABLE Migration (name TEXT);

3. Обновите логику начальной загрузки приложения, чтобы перед ее запуском он захватил список миграций из /migrations папка и запускает миграции, которые имеют пока не применялся.

вот пример, реализованный с помощью JavaScript: клиент SQLite для узла.js Apps