Відповіді:
Я підтримую програму, яка періодично потребує оновлення бази даних sqlite та міграції старих баз даних до нової схеми, і ось що я роблю:
Для відстеження версії бази даних я використовую вбудовану змінну користувальницької версії, яку надає sqlite (sqlite нічого не робить з цією змінною, ви можете вільно користуватися нею, як завгодно). Він починається з 0, і ви можете отримати / встановити цю змінну за допомогою наступних операторів sqlite:
> PRAGMA user_version;
> PRAGMA user_version = 1;
Коли програма запускається, я перевіряю поточну версію користувача, застосовую будь-які зміни, необхідні для оновлення схеми, а потім оновлюю версію користувача. Я обгортаю оновлення транзакцією, щоб, якщо щось піде не так, зміни не було здійснено.
Для внесення змін у схему sqlite підтримує синтаксис "ALTER TABLE" для певних операцій (перейменування таблиці або додавання стовпця). Це простий спосіб оновити наявні таблиці на місці. Дивіться документацію тут: http://www.sqlite.org/lang_altertable.html . Для видалення стовпців або інших змін, які не підтримуються синтаксисом "ALTER TABLE", я створюю нову таблицю, переміщую дату в неї, скидаю стару таблицю і перейменую нову таблицю на оригінальну назву.
application_id
є додатковим бітом для ідентифікації формату файлу за допомогою file
утиліти, наприклад, а не для версій бази даних.
Відповідь від 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];
}
toVersion
у своєму коді? Як це обробляється, коли ви користуєтеся версією 0, а після цього є ще дві версії. Це означає, що вам потрібно мігрувати від 0 до 1 та від 1 до 2. Як ви впораєтеся з цим?
break
заяв в ході switch
, так що всі наступні міграції також відбуватимуться.
Дозвольте мені поділитися деяким міграційним кодом з 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];
}];
}
}
schema_version
прагма, як правило, не з чим займається.
Найкраще рішення IMO - це побудувати структуру оновлення SQLite. У мене була така ж проблема (у світі C #), і я створив власну таку основу. Про це можна прочитати тут . Це прекрасно працює і змушує мої (раніше кошмарні) оновлення працювати з мінімальними зусиллями на моєму боці.
Хоча бібліотека реалізована на C #, представлені там ідеї повинні чудово працювати і у вашому випадку.
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) Я рекомендую помістити весь код для перенесення вашої бази даних у 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];
}
}
Якщо ви зміните схему бази даних та весь код, який використовує її в режимі блокування, як це, швидше за все, у вбудованих і розміщених на телефоні додатках, проблема насправді знаходиться під контролем (нічого подібного до кошмару, який є міграцією схеми у корпоративній БД які можуть обслуговувати сотні програм - не всі також під контролем DBA ;-).
Для .net ви можете використовувати lib:
EntityFrameworkCore.Sqlite.Migrations
Це просто, тому для будь-якої іншої платформи ви можете легко застосувати таку ж поведінку, як у lib.