Sqlite应用内数据库迁移的最佳实践


94

我正在为iPhone使用sqlite,并且我预计数据库架构可能会随着时间而改变。每次成功迁移需要注意哪些陷阱,命名约定和注意事项?

例如,我曾考虑过将一个版本附加到数据库名称(例如Database_v1)上。

Answers:


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 2011年

2
我有一个问题。假设您的初始版本为1,当前版本为5。版本2、3、4中有一些更新。最终用户仅下载了版本1,现在又升级到版本5。您应该怎么做?
Bagusflyer 2014年

6
分几个步骤更新数据库,应用从版本1到版本2,然后从版本2到版本3等等所必需的更改,直到更新为止。一种简单的方法是使用switch语句,其中每个“ case”语句均通过一个版本更新数据库。您“切换”到当前数据库版本,case语句会一直执行到更新完成为止。每当您更新数据库时,只需添加一个新的case语句。有关此示例的详细示例,请参见下面的Billy Gray的答案。
Rngbus 2014年

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。如何处理?

1
@confile中没有break语句switch,因此所有后续迁移也将发生。
磨砂

带链接不存在
Pedro Luz

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];
        }];
    }
}

注意:我对代码的组织方式不完全满意(我更喜欢将打开和迁移作为单个操作的一部分,最好由应用程序委托调用),但是它可以正常工作,并且我想还是应该共享。
Andrey Tarantsov 2012年

为什么使用“ setDatabaseSchemaVersion”方法返回“ user_version”?我认为“ user_version”和“ schema_version”是两种不同的编译指示。
Paul Brewczynski,2015年

@PaulBrewczynski因为我更喜欢常用术语,而不是SQLite术语,所以我也用它的名字(数据库模式的版本)来称呼它。在这种情况下,我不在乎特定于SQLite的术语,schema_version通常人们也不会处理杂物。
安德烈·塔兰佐夫

您已编写:// FMDB无法执行此查询,因为FMDB尝试使用准备好的语句。这是什么意思 这应该起作用:NSString * query = [NSString stringWithFormat:@“ PRAGMA USER_VERSION =%i”,userVersion];[_db executeUpdate:query]; 如此处所述:stackoverflow.com/a/21244261/1364174
Paul Brewczynski 2015年

1
(与上面的评论有关)注意:FMDB库现在具有:userVersion和setUserVersion:方法!因此,您不必使用冗长的@Andrey Tarantsov的方法:-(int)databaseSchemaVersion!和(void)setDatabaseSchemaVersion:(int)版本。FMDB文档:ccgus.github.io/fmdb/html/Categories/…
Paul Brewczynski 2015年

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。创建包含已应用迁移列表的数据库表,例如:

CREATE TABLE Migration (name TEXT);

3。更新应用程序引导程序逻辑,以便在它启动之前,它会从/migrations文件夹中获取迁移列表并运行尚未应用的迁移。

这是使用JavaScript实现的示例:适用于Node.js应用程序的SQLite客户端


2

一些技巧...

1)我建议放置所有代码以将数据库迁移到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

如果您更改数据库架构以及所有在锁步中使用它的代码,就像嵌入式和电话定位应用中可能发生的情况一样,问题实际上已经得到了很好的控制(这与企业数据库上架构迁移的噩梦无法相比)可能正在服务数百个应用程序-也不都是在DBA的控制下;-)。


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.