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

96

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

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

Благо
источник

Ответы:

115

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

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

> PRAGMA user_version;  
> PRAGMA user_version = 1;

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

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

Rngbus
источник
2
Я пытаюсь использовать ту же логику, но по какой-то причине, когда я выполняю «pragma user_version =?» программно, это не работает ... любая идея?
Unicorn
7
Настройки pragma не поддерживают параметры, вам нужно будет указать фактическое значение: "pragma user_version = 1".
csgero
2
У меня есть один вопрос. Допустим, у вас начальная версия 1. А текущая версия 5. Есть некоторые обновления в версии 2,3,4. Конечный пользователь загрузил только вашу версию 1, а теперь обновился до версии 5. Что делать?
Bagusflyer
6
Обновите базу данных в несколько этапов, применяя изменения, необходимые для перехода от версии 1 к версии 2, затем версии 2 к версии 3 и т. Д., Пока она не станет актуальной. Самый простой способ сделать это - иметь оператор switch, в котором каждый оператор case обновляет базу данных на одну версию. Вы «переключаетесь» на текущую версию базы данных, и операторы case пропадают, пока обновление не будет завершено. Каждый раз, когда вы обновляете базу данных, просто добавляйте новый оператор case. См. Подробный пример этого в ответе Билли Грея ниже.
Rngbus
1
@KonstantinTarkus, согласно документации, application_id - это дополнительный бит для определения формата файла, fileнапример, утилитой, а не для версий базы данных.
xaizek
30

Ответ 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. Как вы с этим справляетесь?
confile
1
@confile в файле нет breakоператоров switch, поэтому все последующие миграции также будут происходить.
матовый
Ссылка на Стрип не существует
Педро Луз
20

Позвольте мне поделиться кодом миграции с 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];
        }];
    }
}
Андрей Таранцов
источник
Примечание: я не полностью удовлетворен тем, как организован код (я бы предпочел, чтобы открытие и миграция были частями одной операции, предпочтительно вызываемой делегатом приложения), но это работает, и я подумал, что все равно поделюсь .
Андрей Таранцов
Почему вы используете метод setDatabaseSchemaVersion для возврата user_version? Я думаю, что user_version и schema_version - это две разные прагмы.
Paul Brewczynski
@PaulBrewczynski Потому что я предпочитаю часто используемые термины, а не термины SQLite, а также называю это тем, что есть (версия моей схемы базы данных). В данном случае меня не интересуют термины, специфичные для SQLite, да и schema_versionпрагма обычно не является тем, с чем люди имеют дело.
Андрей Таранцов
Вы написали: // FMDB не может выполнить этот запрос, потому что FMDB пытается использовать подготовленные операторы. Что вы под этим подразумеваете? Это должно работать: NSString * query = [NSString stringWithFormat: @ "PRAGMA USER_VERSION =% i", userVersion]; [_db executeUpdate: запрос]; Как отмечено здесь: stackoverflow.com/a/21244261/1364174
Пол Брючинский,
1
(относится к моему комментарию выше) ПРИМЕЧАНИЕ. В библиотеке FMDB теперь есть: userVersion и setUserVersion: методы! Так что вам не обязательно использовать подробные методы @Andrey Tarantsov: - (int) databaseSchemaVersion! и (void) setDatabaseSchemaVersion: (int) версия. Документация FMDB: ccgus.github.io/fmdb/html/Categories/… :
Paul Brewczynski
4

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

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

Лирон Леви
источник
Это хороший инструмент; жаль, что это не бесплатно
Михай Дамиан
4

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

Константин Таркус
источник
2

Несколько советов ...

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

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

Алекс Мартелли
источник
0

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

EntityFrameworkCore.Sqlite.Migrations

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

Иченский
источник