Кращі практики міграції баз даних у додатках для Sqlite


94

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

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

Відповіді:


111

Я підтримую програму, яка періодично потребує оновлення бази даних sqlite та міграції старих баз даних до нової схеми, і ось що я роблю:

Для відстеження версії бази даних я використовую вбудовану змінну користувальницької версії, яку надає 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 =?" програмно, це не вдається ... будь-яка ідея?
Єдиноріг,

7
налаштування прагми не підтримують параметри, вам доведеться вказати фактичне значення: "pragma user_version = 1".
csgero

2
У мене одне питання. Скажімо, якщо ви початкова версія 1. А поточна версія - 5. Оновлення у версії 2,3,4. Кінцевий користувач завантажив лише вашу версію 1, а тепер оновіть до версії 5. Що робити?
Bagusflyer

6
Оновіть базу даних у кілька кроків, застосувавши зміни, необхідні для переходу від версії 1 до версії 2, потім від версії 2 до версії 3 тощо тощо, поки вона не буде оновлена. Простий спосіб зробити це - мати оператор перемикання, де кожен випадок "case" оновлює базу даних на одну версію. Ви "переходите" на поточну версію бази даних, і виписки з справ пропадають до завершення оновлення. Щоразу, коли ви оновлюєте базу даних, просто додайте нову заяву справи. Дивіться відповідь Біллі Грея нижче для детального прикладу цього.
Rngbus

1
@KonstantinTarkus, згідно з документацією, application_id є додатковим бітом для ідентифікації формату файлу за допомогою fileутиліти, наприклад, а не для версій бази даних.
xaizek

30

Відповідь від Just Curious є мертвою (ви зрозуміли мою думку!), І саме це ми використовуємо для відстеження версії схеми бази даних, яка наразі є у додатку.

Щоб перейти через міграції, які повинні відбутися, щоб користувальницька версія перетворилася на очікувану версію схеми програми, ми використовуємо оператор перемикання. Ось розрізний приклад того, як це виглядає в нашому додатку 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 немає ніяких breakзаяв в ході switch, так що всі наступні міграції також відбуватимуться.
матовий

Посилання Стрип не існує
Педро Луз

20

Дозвольте мені поділитися деяким міграційним кодом з 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];
        }];
    }
}

Примітка. Я не повністю задоволений тим, як організовано код (я вважаю за краще, щоб відкриття та переміщення були частинами однієї операції, бажано, щоб її викликав делегат програми), але це працює, і я подумав, що все одно поділюсь .
Андрій Таранцов

Чому ви використовуєте метод "setDatabaseSchemaVersion" для повернення "user_version"? "user_version" та "schema_version" - це дві різні прагми, на мою думку.
Пол Бревчинський

@PaulBrewczynski Тому що я віддаю перевагу загальновживаним термінам, а не термінам SQLite, а також називаю його тим, що це (версія моєї схеми бази даних). Мені не байдуже терміни, що стосуються конкретного SQLite, в цьому випадку, і schema_versionпрагма, як правило, не з чим займається.
Андрій Таранцов

Ви написали: // FMDB не може виконати цей запит, оскільки FMDB намагається використовувати підготовлені оператори. Що ви маєте на увазі під цим? Це повинно працювати: NSString * query = [NSString stringWithFormat: @ "PRAGMA USER_VERSION =% i", userVersion]; [_db ExecuteUpdate: query]; Як було відзначено тут: stackoverflow.com/a/21244261/1364174
Пол Brewczynski

1
(пов'язано з моїм коментарем вище) ПРИМІТКА: Бібліотека FMDB тепер має функції: userVersion та setUserVersion: methods! Тож вам не доведеться використовувати багатослівні методи @Andrey Tarantsov: - (int) databaseSchemaVersion! та (void) setDatabaseSchemaVersion: (int) версія. Документація FMDB: ccgus.github.io/fmdb/html/Categories/… :
Paul Brewczynski

4

Найкраще рішення IMO - це побудувати структуру оновлення SQLite. У мене була така ж проблема (у світі C #), і я створив власну таку основу. Про це можна прочитати тут . Це прекрасно працює і змушує мої (раніше кошмарні) оновлення працювати з мінімальними зусиллями на моєму боці.

Хоча бібліотека реалізована на C #, представлені там ідеї повинні чудово працювати і у вашому випадку.


Це приємний інструмент; занадто погано, що це не безкоштовно
Mihai Damian

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

CREATE TABLE Migration (name TEXT);

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

Ось приклад, реалізований із JavaScript: Клієнт SQLite для Node.js Apps


2

Кілька порад ...

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

2) Переконайтесь, що ви копіюєте свою базу даних із пакета в документи програми та використовуєте її з цього місця, інакше ви просто перезаписуєте всю базу даних з кожним оновленням додатка, а потім перемістите нову порожню базу даних.

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

4) Ця структура коду забезпечить виконання ваших оновлень у порядку та виконання всіх оновлень, незалежно від того, як довго користувач переходить між оновленнями додатків. Це може бути відновлено далі, але це дуже простий погляд на це. Цей метод можна безпечно запускати щоразу, коли ваші синхронізовані дані, і коштує лише один крихітний 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 ;-).


Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.