【问题标题】:Best practices for in-app database migration for SqliteSqlite 应用内数据库迁移的最佳实践
【发布时间】:2010-11-02 15:19:14
【问题描述】:

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

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

【问题讨论】:

    标签: iphone sqlite


    【解决方案1】:

    我维护一个应用程序,该应用程序需要定期更新 sqlite 数据库并将旧数据库迁移到新架构,这就是我所做的:

    为了跟踪数据库版本,我使用 sqlite 提供的内置用户版本变量(sqlite 对这个变量没有任何作用,您可以随意使用它)。它从 0 开始,您可以使用以下 sqlite 语句获取/设置此变量:

    > PRAGMA user_version;  
    > PRAGMA user_version = 1;
    

    当应用启动时,我会检查当前的用户版本,应用更新架构所需的任何更改,然后更新用户版本。我将更新包装在事务中,这样如果出现任何问题,就不会提交更改。

    为了进行架构更改,sqlite 支持某些操作(重命名表或添加列)的“ALTER TABLE”语法。这是一种就地更新现有表的简单方法。请参阅此处的文档:http://www.sqlite.org/lang_altertable.html。为了删除“ALTER TABLE”语法不支持的列或其他更改,我创建了一个新表,将日期迁移到其中,删除旧表,并将新表重命名为原始名称。

    【讨论】:

    • 我正在尝试使用相同的逻辑,但由于某种原因,当我执行“pragma user_version = ?”时以编程方式,它失败了......有什么想法吗?
    • pragma 设置不支持参数,您必须提供实际值:“pragma user_version = 1”。
    • 我有一个问题。假设您是初始版本 1。当前版本是 5。版本 2、3、4 中有一些更新。最终用户只下载了您的版本 1,现在升级到版本 5。您该怎么办?
    • 分几个步骤更新数据库,应用必要的更改从版本 1 到版本 2,然后从版本 2 到版本 3,等等......直到它是最新的。一个简单的方法是使用一个 switch 语句,其中每个“case”语句将数据库更新一个版本。您“切换”到当前的数据库版本,case 语句将通过,直到更新完成。每当您更新数据库时,只需添加一个新的 case 语句。有关此问题的详细示例,请参见下面 Billy Gray 的回答。
    【解决方案2】:

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

    【讨论】:

    • 好吧,我没有看到您在代码中使用toVersion的位置?当您使用版本 0 并且之后还有两个版本时,它是如何处理的。这意味着您必须从 0 迁移到 1,从 1 迁移到 2。您如何处理?
    • @confile 在switch 中没有break 语句,因此所有后续迁移也会发生。
    • Strip 链接不存在
    【解决方案3】:

    让我分享一些 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”是两个不同的编译指示。
    • @PaulBrewczynski 因为我更喜欢常用术语,而不是 SQLite 术语,而且我用它是什么来称呼它(我的数据库模式的版本)。在这种情况下,我不关心特定于 SQLite 的术语,schema_version pragma 通常也不是人们处理的东西。
    • 您已经写过: // FMDB 无法执行此查询,因为 FMDB 尝试使用准备好的语句。你这是什么意思?这应该工作: NSString *query = [NSString stringWithFormat:@"PRAGMA USER_VERSION = %i", userVersion]; [_db 执行更新:查询];如此处所述:stackoverflow.com/a/21244261/1364174
    • (与我上面的评论相关)注意:FMDB 库现在具有:userVersion 和 setUserVersion:方法!因此,您不必使用冗长的@Andrey Tarantsov 方法:- (int)databaseSchemaVersion!和 (void)setDatabaseSchemaVersion:(int)version。 FMDB 文档:ccgus.github.io/fmdb/html/Categories/…:
    【解决方案4】:

    1。使用基于 SQL 的迁移列表创建 /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。创建包含应用迁移列表的 db 表,例如:

    CREATE TABLE Migration (name TEXT);
    

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

    这是一个使用 JavaScript 实现的示例:SQLite Client for Node.js Apps

    【讨论】:

      【解决方案5】:

      IMO 最好的解决方案是构建 SQLite 升级框架。我遇到了同样的问题(在 C# 世界中),我构建了自己的这样的框架。你可以阅读它here。它运行良好,让我(以前是噩梦般的)升级工作对我来说几乎不需要付出任何努力。

      虽然该库是用 C# 实现的,但那里提出的想法在您的情况下也应该可以正常工作。

      【讨论】:

      • 这是一个不错的工具;太糟糕了,它不是免费的
      【解决方案6】:

      一些提示...

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

      【讨论】:

        【解决方案7】:

        对于 .net,您可以使用 lib:

        EntityFrameworkCore.Sqlite.Migrations

        这很简单,因此对于任何其他平台,您都可以轻松实现与 lib 中相同的行为。

        【讨论】:

          【解决方案8】:

          如果您以同步方式更改数据库架构和所有使用它的代码,嵌入式和手机定位的应用程序很可能就是这种情况,那么问题实际上已经得到了很好的控制(没有什么能比得上架构迁移的噩梦了)一个可能为数百个应用程序提供服务的企业数据库——也不是所有应用程序都在 DBA 的控制之下;-)。

          【讨论】:

            猜你喜欢
            • 2023-03-19
            • 1970-01-01
            • 2016-10-05
            • 2019-01-01
            • 1970-01-01
            • 1970-01-01
            • 2023-03-10
            • 1970-01-01
            • 2020-08-28
            相关资源
            最近更新 更多