简体   繁体   English

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

[英]Best practices for in-app database migration for Sqlite

I am using sqlite for my iphone and I anticipate the database schema might change over time.我正在为我的 iphone 使用 sqlite,我预计数据库架构可能会随着时间而改变。 What are the gotchas, naming conventions and things to watch out for to do a successful migration each time?每次成功迁移时需要注意哪些问题、命名约定和注意事项?

For example, I have thought of appending a version to the database name (eg Database_v1).例如,我曾想过将版本附加到数据库名称(例如 Database_v1)。

I maintain an application that periodically needs to update a sqlite database and migrate old databases to the new schema and here's what I do:我维护一个应用程序,它需要定期更新 sqlite 数据库并将旧数据库迁移到新模式,这就是我所做的:

For tracking the database version, I use the built in user-version variable that sqlite provides (sqlite does nothing with this variable, you are free to use it however you please).为了跟踪数据库版本,我使用 sqlite 提供的内置用户版本变量(sqlite 对这个变量不做任何事情,你可以随意使用它)。 It starts at 0, and you can get/set this variable with the following sqlite statements:它从 0 开始,您可以使用以下 sqlite 语句获取/设置此变量:

> PRAGMA user_version;  
> PRAGMA user_version = 1;

When the app starts, I check the current user-version, apply any changes that are needed to bring the schema up to date, and then update the user-version.当应用程序启动时,我检查当前用户版本,应用更新架构所需的任何更改,然后更新用户版本。 I wrap the updates in a transaction so that if anything goes wrong, the changes aren't committed.我将更新包装在事务中,这样如果出现任何问题,就不会提交更改。

For making schema changes, sqlite supports "ALTER TABLE" syntax for certain operations (renaming the table or adding a column).为了进行模式更改,sqlite 支持某些操作的“ALTER TABLE”语法(重命名表或添加列)。 This is an easy way to update existing tables in-place.这是一种就地更新现有表的简单方法。 See the documentation here: http://www.sqlite.org/lang_altertable.html .请参阅此处的文档: http ://www.sqlite.org/lang_altertable.html。 For deleting columns or other changes that aren't supported by the "ALTER TABLE" syntax, I create a new table, migrate date into it, drop the old table, and rename the new table to the original name.为了删除“ALTER TABLE”语法不支持的列或其他更改,我创建了一个新表,将日期迁移到其中,删除旧表,并将新表重命名为原始名称。

The answer from Just Curious is dead-on (you got my point!), and it's what we use to track the version of the database schema that is currently in the app. Just Curious 的答案是肯定的(你明白我的意思!),我们用它来跟踪应用程序中当前数据库模式的版本。

To run through the migrations that need to occur to get user_version matching the app's expected schema version, we use a switch statement.为了运行需要发生的迁移以使 user_version 与应用程序的预期架构版本匹配,我们使用 switch 语句。 Here's a cut-up example of what this look like in our app Strip :这是我们的应用程序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];
}

Let me share some migration code with FMDB and MBProgressHUD.让我与 FMDB 和 MBProgressHUD 分享一些迁移代码。

Here's how you read and write the schema version number (this is presumably part of a model class, in my case it's a singleton class called Database):以下是您读取和写入模式版本号的方式(这可能是模型类的一部分,在我的情况下,它是一个名为 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);
}

Here's [self database] method that lazily opens the database:这是延迟打开数据库的[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;
}

And here are migration methods called from the view controller:以下是从视图控制器调用的迁移方法:

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

And here's the root view controller code that invokes the migration, using MBProgressHUD to display a progress bezel:这是调用迁移的根视图控制器代码,使用 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];
        }];
    }
}

1 . 1 . Create /migrations folder with the list of SQL-based migrations, where each migration looks something like this:使用基于 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 . 2 . Create db table containing the list of applied migrations, for example:创建包含应用迁移列表的 db 表,例如:

CREATE TABLE Migration (name TEXT);

3 . 3 . Update application bootstrap logic so that before it starts, it grabs the list of migrations from the /migrations folder and runs the migrations that have not yet been applied.更新应用程序引导逻辑,以便在启动之前从/migrations文件夹中获取迁移列表并运行尚未应用的迁移。

Here is an example implemented with JavaScript: SQLite Client for Node.js Apps这是一个使用 JavaScript 实现的示例: Node.js 应用程序的 SQLite 客户端

The best solution IMO is to build a SQLite upgrade framework. IMO 最好的解决方案是构建 SQLite 升级框架。 I had the same problem (in the C# world) and I built my own such framework.我遇到了同样的问题(在 C# 世界中),我构建了自己的这样的框架。 You can read about it here .你可以在这里阅读。 It works perfectly and makes my (previously nightmarish) upgrades work with minimal effort on my side.它运行良好,使我的(以前是噩梦般的)升级工作在我身边以最小的努力工作。

Although the library is implemented in C#, the ideas presented there should work fine in your case also.尽管该库是用 C# 实现的,但那里提出的想法在您的情况下也应该可以正常工作。

For .net you can use lib:对于.net,您可以使用 lib:

EntityFrameworkCore.Sqlite.Migrations EntityFrameworkCore.Sqlite.Migrations

It is simple, so for any other platform you can easily implement the same behavior as in lib.这很简单,因此对于任何其他平台,您都可以轻松实现与 lib 中相同的行为。

Some tips...一些技巧...

1) I recommend putting all the code to migrate your database into an NSOperation and running it in the background thread. 1)我建议将所有代码将您的数据库迁移到一个 NSOperation 并在后台线程中运行它。 You can show a custom UIAlertView with a spinner while the database is being migrated.您可以在迁移数据库时显示带有微调器的自定义 UIAlertView。

2) Make sure you are copying your database from the bundle into the app's documents and using it from that location, otherwise you will just overwrite the whole database with each app update, and then migrate the new empty database. 2) 确保您将数据库从捆绑包复制到应用程序的文档中并从该位置使用它,否则您将在每次应用程序更新时覆盖整个数据库,然后迁移新的空数据库。

3) FMDB is great, but its executeQuery method can't do PRAGMA queries for some reason. 3) FMDB 很棒,但是它的 executeQuery 方法由于某种原因不能进行 PRAGMA 查询。 You'll need to write your own method that uses sqlite3 directly if you want to check the schema version using PRAGMA user_version.如果您想使用 PRAGMA user_version 检查架构版本,您需要编写自己的直接使用 sqlite3 的方法。

4) This code structure will ensure that your updates are executed in order, and that all updates are executed, no matter how long the user goes between app updates. 4)这种代码结构将确保您的更新按顺序执行,并且无论用户在应用程序更新之间间隔多长时间,都会执行所有更新。 It could be refactored further, but this is a very simple way to look at it.它可以进一步重构,但这是一种非常简单的看待它的方法。 This method can safely be run every time your data singleton is instantiated, and only costs one tiny db query that only happens once per session if you set up your data singleton properly.每次实例化数据单例时都可以安全地运行此方法,并且如果您正确设置数据单例,则只需花费一个微小的数据库查询,每个会话仅发生一次。

- (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 的控制之下;-)。

In my article Simple declarative schema migration for SQLite we work out the schema changes automatically by creating a pristine in-memory database, and comparing the schema against your current database by querying the "sqlite_schema" tables from both.在我的文章SQLite 的简单声明性架构迁移中,我们通过创建一个原始的内存数据库来自动计算架构更改,并通过从两者中查询“sqlite_schema”表来将​​架构与您当前的数据库进行比较。 Then we follow the 12 step procedure from the SQLite documentation to safely modify the tables.然后我们按照 SQLite 文档中的12 步过程来安全地修改表。

You can define the schema however you like (an ORM, or plain SQL "CREATE TABLE" statements, etc) as long as you can use it to create a new in-memory database.只要您可以使用它来创建新的内存数据库,您就可以根据自己的喜好定义架构(ORM 或普通 SQL“CREATE TABLE”语句等)。 This means that you only have to maintain the schema in one place, and changes are applied automatically when your application starts up.这意味着您只需在一个地方维护模式,并且在您的应用程序启动时自动应用更改。

Of course there are limitations — in particular this doesn't handle data migrations, only schema migrations;当然有限制——特别是它不处理数据迁移,只处理模式迁移; and new columns must allow null or have a default value specified.并且新列必须允许 null 或指定默认值。 But overall it's a joy to work with.但总的来说,与它一起工作很愉快。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM