Sqliteのアプリ内データベース移行のベストプラクティス


94

私はiphoneにsqliteを使用していますが、データベーススキーマが時間の経過とともに変化する可能性があります。毎回、移行を成功させるための注意点、命名規則、および注意点は何ですか?

たとえば、データベース名にバージョンを追加することを考えました(例:Database_v1)。

回答:


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に移行するために必要な変更を適用して、最新の状態にします。これを行う簡単な方法は、各 "case"ステートメントがデータベースを1つのバージョンだけ更新するswitchステートメントを作成することです。現在のデータベースバージョンに「切り替える」と、更新が完了するまでcaseステートメントが失敗します。データベースを更新するときはいつでも、新しいケースステートメントを追加してください。この詳細な例については、ビリー・グレイによる以下の回答を参照してください。
Rngbus 2014

1
@KonstantinTarkusは、に応じてマニュアルする application_idことにより、ファイル形式を識別するための余分なビットでfileないデータベースのバージョンについては、例えばユーティリティ。
xaizek 16

30

Just Curiousからの答えは完全に正解です(あなたは私のポイントを手に入れました!)。それは、現在アプリにあるデータベーススキーマのバージョンを追跡するために使用するものです。

アプリの予期されるスキーマバージョンと一致するuser_versionを取得するために必要な移行を実行するには、switchステートメントを使用します。これが私たちのアプリストリップでこれがどのように見えるかのカットアップ例です:

- (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を使用していて、その後にさらに2つのバージョンがある場合の処理​​方法。つまり、0から1および1から2に移行する必要があります。これをどのように処理しますか?
21:09にファイル

1
@confileにはbreakステートメントがないswitchため、後続のすべての移行も行われます。
マット

ストリップのリンクが存在しません
ペドロ・ルス

20

いくつかの移行コードを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;
}

そして、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]);
}

次に、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」は、私が考える2つの異なるプラグマです。
Paul Brewczynski、2015年

@PaulBrewczynski SQLite用語ではなく、一般的に使用される用語を好むため、それを何であるか(データベーススキーマのバージョン)と呼んでいます。この場合、SQLite固有の用語は気にしません。また、schema_versionプラグマは通常、人々が扱うものではありません。
Andrey Tarantsov、2015年

あなたは書きました:// FMDBは準備済みステートメントを使用しようとするため、FMDBはこのクエリを実行できません。これはどういう意味ですか?これは機能するはずです:NSString * query = [NSString stringWithFormat:@ "PRAGMA USER_VERSION =%i"、userVersion]; [_db executeUpdate:query]; ここで述べたように:stackoverflow.com/a/21244261/1364174
Paul Brewczynski

1
(上記の私のコメントに関連して)注:FMDBライブラリは次の機能を備えています:userVersionおよびsetUserVersion:メソッド!したがって、詳細な@Andrey Tarantsovのメソッドを使用する必要はありません。-(int)databaseSchemaVersion!および(void)setDatabaseSchemaVersion:(int)version。FMDBドキュメント:ccgus.github.io/fmdb/html/Categories/…
Paul Brewczynski

4

最良のソリューションIMOは、SQLiteアップグレードフレームワークを構築することです。同じ問題が(C#の世界でも)あり、自分でそのようなフレームワークを構築しました。あなたはそれについてここで読むことができます。それは完全に機能し、私の(以前は悪夢のような)アップグレードを最小限の労力で動作させます。

ライブラリはC#で実装されていますが、そこに提示されているアイデアは、あなたのケースでもうまくいくはずです。


それは素晴らしいツールです。残念ながら無料ではありません
ミハイダミアン

3

1/migrationsSQLベースの移行のリストを含むフォルダーを作成します。各移行は次のようになります。

/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で実装した例を次に示します。Node.jsアプリ用のSQLiteクライアント


2

いくつかのヒント...

1)データベースをNSOperationに移行するためのすべてのコードを配置し、バックグラウンドスレッドで実行することをお勧めします。データベースの移行中に、スピナーを使用してカスタムUIAlertViewを表示できます。

2)バンドルからアプリのドキュメントにデータベースをコピーし、その場所からそれを使用していることを確認してください。それ以外の場合は、アプリの更新ごとにデータベース全体を上書きし、新しい空のデータベースを移行します。

3)FMDBは素晴らしいですが、そのexecuteQueryメソッドは何らかの理由でPRAGMAクエリを実行できません。PRAGMA user_versionを使用してスキーマバージョンを確認する場合は、sqlite3を直接使用する独自のメソッドを作成する必要があります。

4)このコード構造により、ユーザーがアプリの更新をどの程度行っても、更新が順番に実行され、すべての更新が実行されます。さらにリファクタリングすることもできますが、これは非常に簡単な方法です。このメソッドは、データシングルトンがインスタンス化されるたびに安全に実行でき、データシングルトンを適切に設定した場合、セッションごとに1回だけ発生する小さなdbクエリが1つだけかかります。

- (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

データベーススキーマと、それをロックステップで使用しているすべてのコードを変更すると、組み込みアプリや電話に配置されたアプリの場合のように、問題は実際にはうまく制御されます(エンタープライズDBでのスキーマの移行という悪夢に匹敵するものはありません)それは何百ものアプリにサービスを提供しているかもしれません-DBAの制御下にあるすべてではありません;-)。


弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.