Sqlite的应用程序内数据库迁移的最佳实践
我为我的iphone使用sqlite,我预计数据库模式可能会随着时间的推移而改变。 什么是每次成功迁移的细节,命名约定和注意事项?
例如,我曾想过将一个版本附加到数据库名称(例如Database_v1)。
我维护一个应用程序,它需要定期更新一个sqlite数据库,并将旧的数据库迁移到新的模式,这就是我所做的:
为了跟踪数据库版本,我使用sqlite提供的内置用户版本variables(sqlite对此variables不做任何处理,但是您可以自由使用它)。 它从0开始,你可以用下面的sqlite语句获得/设置这个variables:
> PRAGMA user_version; > PRAGMA user_version = 1;
当应用程序启动时,我检查当前的用户版本,应用所需的任何更改以使模式更新,然后更新用户版本。 我将更新包装在一个事务中,以便如果出现任何错误,则不会提交更改。
为了进行模式更改,sqlite支持某些操作(重命名表或添加列)的“ALTER TABLE”语法。 这是更新现有表格的简单方法。 请参阅此处的文档: http : //www.sqlite.org/lang_altertable.html 。 为了删除“ALTER TABLE”语法不支持的列或其他更改,我创build了一个新表,将date迁移到该表中,删除旧表,并将新表重命名为原始名称。
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]; }
让我与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]; }]; } }
IMO的最佳解决scheme是构build一个SQLite升级框架。 我有同样的问题(在C#世界),我build立了自己的这样的框架。 你可以在这里阅读。 它完美的工作,使我(以前噩梦)升级工作在我身边尽最大努力。
虽然图书馆是用C#实现的,但是在那里提出的想法也应该适用于你的情况。
如果您更改数据库模式以及所有使用锁模式的代码(在embedded式和手机应用程序中可能会遇到这种情况),问题实际上已经得到了很好的控制(与企业数据库上模式迁移的噩梦毫无媲美这可能正在服务数百个应用程序 – 并不都是在DBA的控制之下;-)。
一些技巧…
1)我build议把所有的代码迁移到一个NSOperation数据库,并在后台线程中运行它。 在迁移数据库时,您可以使用微调器显示自定义的UIAlertView。
2)确保您将数据库从包中复制到应用程序的文档中,并从该位置使用它,否则只需使用每个应用程序更新覆盖整个数据库,然后迁移新的空数据库。
3)FMDB是伟大的,但它的executeQuery方法由于某种原因不能做PRAGMA查询。 如果要使用PRAGMA user_version检查模式版本,则需要直接编写自己的使用sqlite3的方法。
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
。 使用基于SQL的迁移列表创build/migrations
文件夹,其中每个迁移看起来像这样:
/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
。 创build包含应用迁移列表的数据库表,例如:
CREATE TABLE Migration (name TEXT);
3
。 更新应用程序引导程序逻辑,以便在启动之前抓取/migrations
文件夹中的迁移列表,并运行尚未应用的迁移。
下面是一个用JavaScript实现的例子: Node.js应用程序的SQLite客户端