Praktik terbaik untuk migrasi database dalam aplikasi untuk Sqlite

94

Saya menggunakan sqlite untuk iphone saya dan saya mengantisipasi skema database dapat berubah seiring waktu. Apa saja gotcha, konvensi penamaan, dan hal-hal yang harus diperhatikan untuk setiap migrasi yang berhasil?

Misalnya, saya berpikir untuk menambahkan versi ke nama database (misalnya Database_v1).

Anugerah
sumber

Jawaban:

111

Saya memelihara aplikasi yang secara berkala perlu memperbarui database sqlite dan memigrasi database lama ke skema baru dan inilah yang saya lakukan:

Untuk melacak versi database, saya menggunakan variabel versi pengguna bawaan yang disediakan sqlite (sqlite tidak melakukan apa pun dengan variabel ini, Anda bebas menggunakannya sesuka Anda). Ini dimulai dari 0, dan Anda bisa mendapatkan / mengatur variabel ini dengan pernyataan sqlite berikut:

> PRAGMA user_version;  
> PRAGMA user_version = 1;

Saat aplikasi dimulai, saya memeriksa versi pengguna saat ini, menerapkan perubahan apa pun yang diperlukan untuk memperbarui skema, dan kemudian memperbarui versi pengguna. Saya membungkus pembaruan dalam transaksi sehingga jika ada yang salah, perubahan tidak dilakukan.

Untuk membuat perubahan skema, sqlite mendukung sintaks "ALTER TABLE" untuk operasi tertentu (mengganti nama tabel atau menambahkan kolom). Ini adalah cara mudah untuk memperbarui tabel yang sudah ada. Lihat dokumentasinya di sini: http://www.sqlite.org/lang_altertable.html . Untuk menghapus kolom atau perubahan lain yang tidak didukung oleh sintaks "ALTER TABLE", saya membuat tabel baru, memindahkan tanggal ke dalamnya, melepaskan tabel lama, dan mengganti nama tabel baru ke nama asli.

Rngbus
sumber
2
Saya mencoba untuk memiliki logika yang sama, tetapi untuk beberapa alasan ketika saya menjalankan "pragma user_version =?" secara terprogram, gagal ... ada ide?
Unicorn
7
pengaturan pragma tidak mendukung parameter, Anda harus memberikan nilai sebenarnya: "pragma user_version = 1".
csgero
2
Saya mempunyai satu pertanyaan. Katakanlah jika Anda versi awal 1. Dan versi saat ini adalah 5. Ada beberapa pembaruan di versi 2,3,4. Pengguna akhir hanya mendownload versi 1 Anda, dan sekarang mengupgrade ke versi 5. Apa yang harus Anda lakukan?
Bagusflyer
6
Perbarui basis data dalam beberapa langkah, terapkan perubahan yang diperlukan untuk beralih dari versi 1 ke versi 2, lalu versi 2 ke versi 3, dll ... hingga mutakhir. Cara mudah untuk melakukannya adalah dengan memiliki pernyataan switch di mana setiap pernyataan "case" memperbarui database dengan satu versi. Anda "beralih" ke versi database saat ini, dan pernyataan kasus tetap berlaku hingga pembaruan selesai. Setiap kali Anda memperbarui database, cukup tambahkan pernyataan kasus baru. Lihat jawaban di bawah oleh Billy Gray untuk contoh detailnya.
Rngbus
1
@KonstantinTarkus, menurut dokumentasi application_id adalah bit ekstra untuk mengidentifikasi format file berdasarkan fileutilitas misalnya, bukan untuk versi database.
xaizek
30

Jawaban dari Just Curious sudah mati (Anda mengerti maksud saya!), Dan itulah yang kami gunakan untuk melacak versi skema database yang saat ini ada di aplikasi.

Untuk menjalankan migrasi yang perlu terjadi agar user_version cocok dengan versi skema aplikasi yang diharapkan, kami menggunakan pernyataan switch. Berikut adalah contoh potongan dari tampilan ini di Strip aplikasi kami :

- (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];
}
Billy Gray
sumber
1
Nah, saya tidak melihat di mana Anda menggunakan toVersionkode Anda? Bagaimana penanganannya ketika Anda menggunakan versi 0 dan ada dua versi lagi setelah itu. Ini berarti Anda harus bermigrasi dari 0 ke 1 dan dari 1 ke 2. Bagaimana Anda menangani ini?
confile
1
@confile tidak ada breakpernyataan di dalam switch, jadi semua migrasi selanjutnya juga akan terjadi.
matte
Tautan Jalur tidak ada
Pedro Luz
20

Izinkan saya membagikan beberapa kode migrasi dengan FMDB dan MBProgressHUD.

Inilah cara Anda membaca dan menulis nomor versi skema (ini mungkin bagian dari kelas model, dalam kasus saya ini adalah kelas tunggal yang disebut 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);
}

Berikut [self database]metode yang membuka database dengan malas:

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

Dan berikut adalah metode migrasi yang dipanggil dari pengontrol tampilan:

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

Dan inilah kode pengontrol tampilan root yang memanggil migrasi, menggunakan MBProgressHUD untuk menampilkan bezel kemajuan:

- (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
sumber
Catatan: Saya tidak sepenuhnya puas dengan cara kode diatur (saya lebih suka membuka dan migrasi menjadi bagian dari operasi tunggal, sebaiknya dipanggil oleh delegasi aplikasi), tetapi berfungsi, dan saya pikir saya akan tetap membagikannya .
Andrey Tarantsov
Mengapa Anda menggunakan metode "setDatabaseSchemaVersion" untuk mengembalikan "user_version"? "user_version" dan "schema_version" adalah dua pragma yang berbeda menurut saya.
Paul Brewczynski
@PaulBrewczynski Karena saya lebih suka istilah yang umum digunakan, bukan istilah SQLite, dan saya juga menyebutnya dengan apa adanya (versi skema database saya). Saya tidak peduli dengan istilah khusus SQLite dalam kasus ini, dan schema_versionpragma biasanya bukan sesuatu yang ditangani orang juga.
Andrey Tarantsov
Anda telah menulis: // FMDB tidak dapat menjalankan kueri ini karena FMDB mencoba menggunakan pernyataan yang telah disiapkan. Apa yang Anda maksud dengan ini? Ini seharusnya berfungsi: NSString * query = [NSString stringWithFormat: @ "PRAGMA USER_VERSION =% i", userVersion]; [_db executeUpdate: query]; Seperti disebutkan di sini: stackoverflow.com/a/21244261/1364174
Paul Brewczynski
1
(terkait dengan komentar saya di atas) CATATAN: Perpustakaan FMDB sekarang memiliki fitur: userVersion dan setUserVersion: metode! Jadi, Anda tidak perlu menggunakan metode verbose @Andrey Tarantsov: - (int) databaseSchemaVersion! dan (void) setDatabaseSchemaVersion: (int) version. Dokumentasi FMDB: ccgus.github.io/fmdb/html/Categories/… :
Paul Brewczynski
4

Solusi terbaik IMO adalah membangun kerangka kerja peningkatan SQLite. Saya memiliki masalah yang sama (di dunia C #) dan saya membangun kerangka kerja saya sendiri. Anda dapat membacanya di sini . Ini bekerja dengan sempurna dan membuat upgrade saya (sebelumnya mimpi buruk) bekerja dengan sedikit usaha di pihak saya.

Meskipun perpustakaan diimplementasikan dalam C #, ide-ide yang disajikan di sana juga akan berfungsi dengan baik dalam kasus Anda.

Liron Levi
sumber
Itu alat yang bagus; sayang sekali ini tidak gratis
Mihai Damian
3

1. Buat /migrationsfolder dengan daftar migrasi berbasis SQL, di mana setiap migrasi terlihat seperti ini:

/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. Buat tabel db yang berisi daftar migrasi yang diterapkan, misalnya:

CREATE TABLE Migration (name TEXT);

3. Perbarui logika bootstrap aplikasi sehingga sebelum dimulai, ia mengambil daftar migrasi dari /migrationsfolder dan menjalankan migrasi yang belum diterapkan.

Berikut ini contoh yang diterapkan dengan JavaScript: Klien SQLite untuk Aplikasi Node.js

Konstantin Tarkus
sumber
2

Beberapa tips ...

1) Saya sarankan untuk meletakkan semua kode untuk memigrasi database Anda ke dalam NSOperation dan menjalankannya di thread latar belakang. Anda bisa menampilkan UIAlertView kustom dengan spinner saat database sedang dimigrasi.

2) Pastikan Anda menyalin database Anda dari bundel ke dalam dokumen aplikasi dan menggunakannya dari lokasi itu, jika tidak, Anda hanya akan menimpa seluruh database dengan setiap pembaruan aplikasi, lalu memigrasi database baru yang kosong.

3) FMDB bagus, tetapi metode executeQuery-nya tidak dapat melakukan kueri PRAGMA karena alasan tertentu. Anda harus menulis metode Anda sendiri yang menggunakan sqlite3 secara langsung jika Anda ingin memeriksa versi skema menggunakan PRAGMA user_version.

4) Struktur kode ini akan memastikan bahwa pembaruan Anda dijalankan secara berurutan, dan semua pembaruan dijalankan, tidak peduli berapa lama pengguna berjalan di antara pembaruan aplikasi. Ini bisa difaktorisasi lebih jauh, tapi ini adalah cara yang sangat sederhana untuk melihatnya. Metode ini dapat dijalankan dengan aman setiap kali data tunggal Anda dibuat, dan hanya memerlukan biaya satu kueri db kecil yang hanya terjadi sekali per sesi jika Anda menyiapkan data tunggal dengan benar.

- (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];
    }
}
Joslin kaya
sumber
1

Jika Anda mengubah skema database dan semua kode yang menggunakannya di lockstep, seperti yang mungkin terjadi di aplikasi yang disematkan dan yang terletak di ponsel, masalahnya sebenarnya terkendali dengan baik (tidak ada yang sebanding dengan mimpi buruk yaitu migrasi skema pada DB perusahaan yang mungkin melayani ratusan aplikasi - tidak semuanya di bawah kendali DBA juga ;-).

Alex Martelli
sumber
0

Untuk .net Anda dapat menggunakan lib:

EntityFrameworkCore.Sqlite.Migrations

Ini sederhana, jadi untuk platform lain Anda dapat dengan mudah menerapkan perilaku yang sama seperti di lib.

ichensky
sumber