Các phương pháp hay nhất để di chuyển cơ sở dữ liệu trong ứng dụng cho Sqlite


94

Tôi đang sử dụng sqlite cho iphone của mình và tôi dự đoán giản đồ cơ sở dữ liệu có thể thay đổi theo thời gian. Các gotchas, quy ước đặt tên và những điều cần chú ý để thực hiện di chuyển thành công mỗi lần là gì?

Ví dụ: tôi đã nghĩ đến việc thêm một phiên bản vào tên cơ sở dữ liệu (ví dụ: Database_v1).

Câu trả lời:


111

Tôi duy trì một ứng dụng cần cập nhật định kỳ cơ sở dữ liệu sqlite và di chuyển cơ sở dữ liệu cũ sang giản đồ mới và đây là những gì tôi làm:

Để theo dõi phiên bản cơ sở dữ liệu, tôi sử dụng biến phiên bản người dùng cài sẵn mà sqlite cung cấp (sqlite không làm gì với biến này, bạn có thể thoải mái sử dụng tùy ý). Nó bắt đầu từ 0 và bạn có thể lấy / đặt biến này bằng các câu lệnh sqlite sau:

> PRAGMA user_version;  
> PRAGMA user_version = 1;

Khi ứng dụng khởi động, tôi kiểm tra phiên bản người dùng hiện tại, áp dụng bất kỳ thay đổi nào cần thiết để cập nhật giản đồ, sau đó cập nhật phiên bản người dùng. Tôi gói các bản cập nhật trong một giao dịch để nếu có gì sai, các thay đổi sẽ không được cam kết.

Để thực hiện các thay đổi lược đồ, sqlite hỗ trợ cú pháp "ALTER TABLE" cho các hoạt động nhất định (đổi tên bảng hoặc thêm cột). Đây là một cách dễ dàng để cập nhật các bảng hiện có tại chỗ. Xem tài liệu tại đây: http://www.sqlite.org/lang_altertable.html . Để xóa các cột hoặc các thay đổi khác không được cú pháp "ALTER TABLE" hỗ trợ, tôi tạo một bảng mới, di chuyển ngày vào đó, bỏ bảng cũ và đổi tên bảng mới thành tên ban đầu.


2
Tôi đang cố gắng có cùng một logic, nhưng vì một số lý do khi tôi thực thi "pragma user_version =?" theo chương trình, nó không thành công ... bất kỳ ý tưởng?
Unicorn

7
cài đặt pragma không hỗ trợ tham số, bạn sẽ phải cung cấp giá trị thực: "pragma user_version = 1".
csgero

2
Tôi có một câu hỏi. Giả sử nếu bạn là phiên bản ban đầu 1. Và phiên bản hiện tại là 5. Có một số cập nhật trong phiên bản 2,3,4. Người dùng cuối chỉ tải xuống phiên bản 1 của bạn và bây giờ nâng cấp lên phiên bản 5. Bạn nên làm gì?
Bagusflyer

6
Cập nhật cơ sở dữ liệu theo nhiều bước, áp dụng các thay đổi cần thiết để chuyển từ phiên bản 1 đến phiên bản 2, sau đó là phiên bản 2 đến phiên bản 3, v.v. cho đến khi nó được cập nhật. Một cách dễ dàng để làm điều này là có một câu lệnh switch trong đó mỗi câu lệnh "case" cập nhật cơ sở dữ liệu theo một phiên bản. Bạn "chuyển" sang phiên bản cơ sở dữ liệu hiện tại và các câu lệnh trường hợp sẽ được thông qua cho đến khi cập nhật hoàn tất. Bất cứ khi nào bạn cập nhật cơ sở dữ liệu, chỉ cần thêm một câu lệnh trường hợp mới. Hãy xem câu trả lời dưới đây của Billy Grey để biết ví dụ chi tiết về điều này.
Rngbus

1
@KonstantinTarkus, theo tài liệu application_id là một bit bổ sung để xác định định dạng tệp theo filetiện ích, chẳng hạn cho các phiên bản của cơ sở dữ liệu.
xaizek

30

Câu trả lời từ Just Curious là không có cơ sở (bạn hiểu ý tôi!), Và đó là những gì chúng tôi sử dụng để theo dõi phiên bản của lược đồ cơ sở dữ liệu hiện có trong ứng dụng.

Để thực hiện quá trình di chuyển cần diễn ra để khiến user_version khớp với phiên bản giản đồ mong đợi của ứng dụng, chúng tôi sử dụng câu lệnh switch. Dưới đây là một ví dụ tóm tắt về cái này trông như thế nào trong Dải ứng dụng của chúng tôi :

- (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
Tôi không biết bạn sử dụng toVersionmã ở đâu? Nó được xử lý như thế nào khi bạn đang ở phiên bản 0 và có hai phiên bản nữa sau đó. Điều này có nghĩa là bạn phải chuyển từ 0 sang 1 và từ 1 sang 2. Bạn xử lý việc này như thế nào?
confile

1
@confile không có breakcâu lệnh nào trong switch, vì vậy tất cả các lần di chuyển tiếp theo cũng sẽ xảy ra.
mờ

Các liên kết Strip không tồn tại
Pedro Luz

20

Hãy để tôi chia sẻ một số mã di chuyển với FMDB và MBProgressHUD.

Đây là cách bạn đọc và ghi số phiên bản lược đồ (đây có lẽ là một phần của lớp mô hình, trong trường hợp của tôi, đó là một lớp singleton được gọi là Cơ sở dữ liệu):

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

Đây là [self database]phương pháp mở cơ sở dữ liệu một cách lười biếng:

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

Và đây là các phương thức di chuyển được gọi từ bộ điều khiển chế độ xem:

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

Và đây là mã của bộ điều khiển chế độ xem gốc gọi quá trình di chuyển, sử dụng MBProgressHUD để hiển thị khung tiến trình:

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

Lưu ý: Tôi không hoàn toàn hài lòng với cách tổ chức mã (tôi muốn mở và di chuyển thành các phần của một thao tác duy nhất, tốt hơn là được gọi bởi người ủy quyền ứng dụng), nhưng nó hoạt động và tôi nghĩ tôi vẫn muốn chia sẻ .
Andrey Tarantsov

Tại sao bạn sử dụng phương thức "setDatabaseSchemaVersion" để trả về "user_version"? "user_version" và "schema_version" là hai thực dụng khác nhau mà tôi nghĩ.
Paul Brewczynski

@PaulBrewczynski Bởi vì tôi thích các thuật ngữ thường được sử dụng hơn, không phải các thuật ngữ SQLite và tôi cũng đang gọi nó bằng cách gọi nó là gì (phiên bản của lược đồ cơ sở dữ liệu của tôi). Tôi không quan tâm đến các thuật ngữ dành riêng cho SQLite trong trường hợp này và schema_versionpragma thường không phải là thứ mà mọi người xử lý.
Andrey Tarantsov

Bạn đã viết: // FMDB không thể thực hiện truy vấn này vì FMDB cố gắng sử dụng các câu lệnh đã chuẩn bị. Ý bạn là gì? Điều này sẽ hoạt động: NSString * query = [NSString stringWithFormat: @ "PRAGMA USER_VERSION =% i", userVersion]; [_db executeUpdate: query]; Như đã lưu ý ở đây: stackoverflow.com/a/21244261/1364174
Paul Brewczynski

1
(liên quan đến nhận xét của tôi ở trên) LƯU Ý: Thư viện FMDB hiện có các tính năng: phương thức userVersion và setUserVersion:! Vì vậy, bạn không cần phải sử dụng các phương thức của @Andrey Tarantsov dài dòng: - (int) databaseSchemaVersion! và (void) setDatabaseSchemaVersion: (int) phiên bản. Tài liệu FMDB: ccgus.github.io/fmdb/html/Categories/… :
Paul Brewczynski

4

Giải pháp tốt nhất IMO là xây dựng khung nâng cấp SQLite. Tôi đã gặp vấn đề tương tự (trong thế giới C #) và tôi đã xây dựng một khuôn khổ như vậy của riêng mình. Bạn có thể đọc về nó ở đây . Nó hoạt động hoàn hảo và làm cho các bản nâng cấp (trước đây là ban đêm) của tôi hoạt động với nỗ lực tối thiểu từ phía tôi.

Mặc dù thư viện được triển khai bằng C #, các ý tưởng được trình bày ở đó cũng sẽ hoạt động tốt trong trường hợp của bạn.


Đó là một công cụ tốt; quá xấu nó không miễn phí
Mihai Damian

3

1. Tạo /migrationsthư mục với danh sách các lần di chuyển dựa trên SQL, trong đó mỗi lần di chuyển trông giống như sau:

/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. Tạo bảng db chứa danh sách các di chuyển được áp dụng, ví dụ:

CREATE TABLE Migration (name TEXT);

3. Cập nhật logic bootstrap của ứng dụng để trước khi khởi động, nó lấy danh sách các di chuyển từ /migrationsthư mục và chạy các di chuyển chưa được áp dụng.

Đây là một ví dụ được triển khai với JavaScript: SQLite Client cho Node.js Apps


2

Một số lời khuyên...

1) Tôi khuyên bạn nên đặt tất cả mã để di chuyển cơ sở dữ liệu của bạn vào một NSOperation và chạy nó trong chuỗi nền. Bạn có thể hiển thị UIAlertView tùy chỉnh với một con quay trong khi cơ sở dữ liệu đang được di chuyển.

2) Đảm bảo rằng bạn đang sao chép cơ sở dữ liệu của mình từ gói vào tài liệu của ứng dụng và sử dụng nó từ vị trí đó, nếu không, bạn sẽ chỉ ghi đè toàn bộ cơ sở dữ liệu với mỗi bản cập nhật ứng dụng, rồi di chuyển cơ sở dữ liệu trống mới.

3) FMDB rất tuyệt, nhưng phương thức executeQuery của nó không thể thực hiện các truy vấn PRAGMA vì một số lý do. Bạn sẽ cần phải viết phương thức của riêng mình sử dụng sqlite3 trực tiếp nếu bạn muốn kiểm tra phiên bản lược đồ bằng PRAGMA user_version.

4) Cấu trúc mã này sẽ đảm bảo rằng các bản cập nhật của bạn được thực thi theo thứ tự và tất cả các bản cập nhật đều được thực thi, bất kể người dùng trải qua bao lâu giữa các bản cập nhật ứng dụng. Nó có thể được tái cấu trúc thêm, nhưng đây là một cách rất đơn giản để xem xét nó. Phương pháp này có thể được chạy một cách an toàn mỗi khi singleton dữ liệu của bạn được khởi tạo và chỉ tốn một truy vấn db nhỏ chỉ xảy ra một lần mỗi phiên nếu bạn thiết lập singleton dữ liệu của mình đúng cách.

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

Nếu bạn thay đổi lược đồ cơ sở dữ liệu và tất cả mã đang sử dụng nó trong bước khóa, như trường hợp xảy ra trong các ứng dụng được nhúng và định vị trên điện thoại, vấn đề thực sự đã được kiểm soát (không gì có thể so sánh được với cơn ác mộng đó là di chuyển lược đồ trên DB doanh nghiệp có thể đang cung cấp hàng trăm ứng dụng - không phải tất cả đều nằm dưới sự kiểm soát của DBA ;-).


0

Đối với .net, bạn có thể sử dụng lib:

EntityFrameworkCore.Sqlite.Migrations

Nó đơn giản, vì vậy đối với bất kỳ nền tảng nào khác, bạn có thể dễ dàng thực hiện hành vi tương tự như trong lib.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.