Лучшие практики для миграции базы данных в приложении для Sqlite

75

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

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

Теги:
iphone

8 ответов

97
Лучший ответ

Я поддерживаю приложение, которое периодически необходимо обновить базу данных 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", я создаю новую таблицу, переношу дату в нее, удаляю старую таблицу и переименовываю новую таблицу в исходное имя.

  • 2
    Я пытаюсь иметь ту же логику, но по какой-то причине, когда я выполняю "pragma user_version =?" программно, это терпит неудачу ... любая идея?
  • 6
    Настройки прагмы не поддерживают параметры, вам нужно будет указать фактическое значение: "прагма user_version = 1".
Показать ещё 5 комментариев
28

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

Чтобы выполнить миграцию, которая должна произойти, чтобы получить user_version, соответствующую ожидаемой версии схемы приложения, мы используем оператор switch. Здесь приведенный пример того, как это выглядит в нашем приложении 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];
}
  • 1
    Ну, я не видел, где вы используете toVersion в вашем коде? Как это обрабатывается, когда вы находитесь на версии 0, и после этого есть еще две версии. Это означает, что вам нужно перейти с 0 на 1 и с 1 на 2. Как вы справляетесь с этим?
  • 1
    @confile в switch нет операторов break , поэтому все последующие миграции также будут происходить.
19

Позвольте мне поделиться некоторым кодом миграции с 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];
        }];
    }
}
  • 0
    Примечание: я не полностью удовлетворен тем, как организован код (я бы предпочел, чтобы открытие и перенос были частями одной операции, предпочтительно вызываемой делегатом приложения), но это работает, и я решил поделиться в любом случае ,
  • 0
    Почему вы используете метод setDatabaseSchemaVersion для возврата «user_version»? «user_version» и «schema_version» - две разные прагмы, я думаю.
Показать ещё 4 комментария
4

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

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

  • 0
    Это хороший инструмент; Жаль, что это не бесплатно
  • 5
    Черт возьми, это 9,95.
2

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

CREATE TABLE Migration (name TEXT);

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

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

1

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

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

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

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

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

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

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

0

Для .net вы можете использовать lib:

EntityFrameworkCore.Sqlite.Migrations

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню