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

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

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

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.

вот как Вы читаете и записываете номер версии схемы (это, по-видимому, часть класса модели, в моем случае это одноэлементный класс под названием 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);
}

здесь [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 со спиннером.

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. Create /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 приложения