Sqlite의 앱 내 데이터베이스 마이그레이션 모범 사례


94

나는 내 아이폰에 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 =?"를 실행할 때 프로그래밍 방식으로 실패합니다 ... 어떤 아이디어?
Unicorn

7
pragma 설정은 매개 변수를 지원하지 않으므로 실제 값인 "pragma user_version = 1"을 제공해야합니다.
csgero

2
질문이 하나 있습니다. 초기 버전 1이라고 가정 해 보겠습니다. 현재 버전은 5입니다. 버전 2,3,4에는 몇 가지 업데이트가 있습니다. 최종 사용자는 버전 1 만 다운로드했고 이제 버전 5로 업그레이드합니다. 어떻게해야합니까?
Bagusflyer 2014

6
최신 상태가 될 때까지 버전 1에서 버전 2로, 버전 2에서 버전 3으로 이동하는 데 필요한 변경 사항을 적용하여 여러 단계로 데이터베이스를 업데이트합니다. 이를 수행하는 쉬운 방법은 각 "case"문이 데이터베이스를 한 버전 씩 업데이트하는 switch 문을 사용하는 것입니다. 현재 데이터베이스 버전으로 "전환"하면 업데이트가 완료 될 때까지 case 문이 통과됩니다. 데이터베이스를 업데이트 할 때마다 새 case 문을 추가하면됩니다. 이에 대한 자세한 예는 Billy Gray의 아래 답변을 참조하십시오.
Rngbus 2014

1
문서 에 따르면 @KonstantinTarkus 는 데이터베이스 버전이 아닌 유틸리티로 application_id파일 형식을 식별하기위한 추가 비트입니다 file.
자이 젝

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로 마이그레이션해야 함을 의미합니다. 어떻게 처리합니까?
confile

1
@confile에는 break문 이 없으므로 switch모든 후속 마이그레이션도 발생합니다.
Matte

스트립 링크가 존재하지 않습니다
페드로 루즈

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

"user_version"을 반환하기 위해 "setDatabaseSchemaVersion"메서드를 사용하는 이유는 무엇입니까? "user_version"과 "schema_version"은 두 개의 다른 pragma라고 생각합니다.
Paul Brewczynski 2015 년

@PaulBrewczynski SQLite 용어가 아닌 일반적으로 사용되는 용어를 선호하고 그것이 무엇인지 (데이터베이스 스키마 버전)로 부르기 때문입니다. 이 경우 SQLite 관련 용어에 대해서는 신경 쓰지 않으며 schema_versionpragma는 일반적으로 사람들이 다루는 것이 아닙니다.
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 #으로 구현되지만 여기에 제시된 아이디어는 귀하의 경우에도 잘 작동합니다.


그것은 좋은 도구입니다. 너무 나쁘다 그것은 자유롭지 않다
Mihai Damian

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)이 코드 구조는 사용자가 앱 업데이트간에 이동하는 시간에 관계없이 업데이트가 순서대로 실행되고 모든 업데이트가 실행되도록합니다. 추가로 리팩토링 할 수 있지만 이것은 매우 간단한 방법입니다. 이 방법은 데이터 싱글 톤이 인스턴스화 될 때마다 안전하게 실행할 수 있으며 데이터 싱글 톤을 올바르게 설정 한 경우 세션 당 한 번만 발생하는 하나의 작은 db 쿼리 비용 만 발생합니다.

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