Najlepsze rozwiązania dotyczące migracji bazy danych w aplikacji dla Sqlite

95

Używam sqlite dla mojego iPhone'a i spodziewam się, że schemat bazy danych może z czasem ulec zmianie. Jakie są pułapki, konwencje nazewnictwa i rzeczy, na które należy uważać, aby za każdym razem przeprowadzić udaną migrację?

Na przykład pomyślałem o dołączeniu wersji do nazwy bazy danych (np. Database_v1).

Dar
źródło

Odpowiedzi:

113

Utrzymuję aplikację, która musi okresowo aktualizować bazę danych sqlite i migrować stare bazy danych do nowego schematu, a oto co robię:

Do śledzenia wersji bazy danych używam wbudowanej zmiennej wersji użytkownika, którą zapewnia sqlite (sqlite nic nie robi z tą zmienną, możesz jej używać w dowolny sposób). Zaczyna się od 0 i możesz pobrać / ustawić tę zmienną za pomocą następujących instrukcji sqlite:

> PRAGMA user_version;  
> PRAGMA user_version = 1;

Po uruchomieniu aplikacji sprawdzam bieżącą wersję użytkownika, wprowadzam wszelkie zmiany potrzebne do zaktualizowania schematu, a następnie aktualizuję wersję użytkownika. Zawijam aktualizacje w transakcję, więc jeśli coś pójdzie nie tak, zmiany nie zostaną zatwierdzone.

Aby dokonać zmian schematu, sqlite obsługuje składnię "ALTER TABLE" dla niektórych operacji (zmiana nazwy tabeli lub dodanie kolumny). Jest to łatwy sposób aktualizowania istniejących tabel w miejscu. Zobacz dokumentację tutaj: http://www.sqlite.org/lang_altertable.html . Aby usunąć kolumny lub inne zmiany, które nie są obsługiwane przez składnię „ALTER TABLE”, tworzę nową tabelę, przenoszę do niej datę, usuwam starą tabelę i zmieniam nazwę nowej tabeli na oryginalną.

Rngbus
źródło
2
Próbuję mieć tę samą logikę, ale z jakiegoś powodu, kiedy wykonuję "pragma user_version =?" programowo zawodzi ... jakiś pomysł?
Unicorn
7
ustawienia pragma nie obsługują parametrów, musisz podać rzeczywistą wartość: "pragma user_version = 1".
csgero
2
Mam jedno pytanie. Powiedzmy, że masz początkową wersję 1. A aktualna wersja to 5. Jest kilka aktualizacji w wersji 2,3,4. Użytkownik końcowy pobrał tylko twoją wersję 1, a teraz uaktualnij do wersji 5. Co powinieneś zrobić?
Bagusflyer,
6
Zaktualizuj bazę danych w kilku krokach, stosując zmiany niezbędne do przejścia z wersji 1 do wersji 2, a następnie z wersji 2 do wersji 3 itd., Aż będzie aktualna. Prostym sposobem na to jest utworzenie instrukcji switch, w której każda instrukcja „case” aktualizuje bazę danych o jedną wersję. Przełączasz się na bieżącą wersję bazy danych, a instrukcje przypadków są przesyłane do zakończenia aktualizacji. Za każdym razem, gdy aktualizujesz bazę danych, po prostu dodaj nową instrukcję case. Zobacz odpowiedź Billy'ego Graya poniżej, aby zapoznać się ze szczegółowym przykładem.
Rngbus
1
@KonstantinTarkus, zgodnie z dokumentacją, application_id jest dodatkowym bitem do identyfikowania formatu pliku filena przykład według narzędzia, a nie dla wersji bazy danych.
xaizek
30

Odpowiedź z Just Curious jest ostateczna (zrozumiałeś!) I to jest to, czego używamy do śledzenia wersji schematu bazy danych, która jest obecnie w aplikacji.

Aby przejść przez migracje, które muszą wystąpić, aby uzyskać user_version pasującą do oczekiwanej wersji schematu aplikacji, używamy instrukcji switch. Oto przykładowy przykład tego, jak to wygląda w naszym pasku aplikacji :

- (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 Grey
źródło
1
Cóż, nie widziałem, gdzie używasz toVersionw swoim kodzie? Jak to jest obsługiwane, gdy jesteś w wersji 0, a po niej są jeszcze dwie wersje. Oznacza to, że musisz przeprowadzić migrację z 0 do 1 iz 1 do 2. Jak sobie z tym radzisz?
konfile
1
@confile nie ma żadnych breakinstrukcji w pliku switch, więc wszystkie kolejne migracje również będą miały miejsce.
matowy
Link Strip nie istnieje
Pedro Luz
20

Pozwólcie, że podzielę się kodem migracji z FMDB i MBProgressHUD.

Oto jak odczytujesz i zapisujesz numer wersji schematu (prawdopodobnie jest to część klasy modelu, w moim przypadku jest to klasa pojedyncza o nazwie 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);
}

Oto [self database]metoda, która leniwie otwiera bazę danych:

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

A oto metody migracji wywoływane z kontrolera widoku:

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

A oto kod kontrolera widoku głównego, który wywołuje migrację, używając MBProgressHUD do wyświetlania ramki postępu:

- (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
źródło
Uwaga: nie jestem w pełni zadowolony ze sposobu organizacji kodu (wolałbym, aby otwieranie i migracja były częściami pojedynczej operacji, najlepiej wywoływanej przez delegata aplikacji), ale to działa i pomyślałem, że mimo wszystko udostępnię .
Andrey Tarantsov
Dlaczego używasz metody „setDatabaseSchemaVersion” do zwracania „user_version”? Myślę, że „user_version” i „schema_version” to dwie różne pragmaty.
Paul Brewczyński
@PaulBrewczynski Ponieważ wolę powszechnie używane terminy, a nie terminy SQLite, a także nazywam to tym, czym jest (wersja schematu mojej bazy danych). W tym przypadku nie obchodzą mnie terminy specyficzne dla SQLite, a schema_versionpragma zwykle nie jest czymś, z czym ludzie mają do czynienia.
Andrey Tarantsov
Napisałeś: // FMDB nie może wykonać tego zapytania, ponieważ FMDB próbuje użyć przygotowanych instrukcji. Co przez to rozumiesz? To powinno działać: NSString * query = [NSString stringWithFormat: @ "PRAGMA USER_VERSION =% i", userVersion]; [_db executeUpdate: zapytanie]; Jak zaznaczono tutaj: stackoverflow.com/a/21244261/1364174
Paul Brewczynski
1
(związane z moim komentarzem powyżej) UWAGA: Biblioteka FMDB zawiera teraz: userVersion i setUserVersion: methods! Nie musisz więc używać szczegółowych metod @Andrey Tarantsov: - (int) databaseSchemaVersion! i (void) setDatabaseSchemaVersion: (int) version. Dokumentacja FMDB: ccgus.github.io/fmdb/html/Categories/… :
Paul Brewczyński
4

Najlepszym rozwiązaniem IMO jest zbudowanie struktury aktualizacji SQLite. Miałem ten sam problem (w świecie C #) i zbudowałem własny taki framework. Możesz o tym przeczytać tutaj . Działa doskonale i sprawia, że ​​moje (wcześniej koszmarne) aktualizacje działają przy minimalnym wysiłku z mojej strony.

Chociaż biblioteka została zaimplementowana w C #, przedstawione tam pomysły powinny działać dobrze również w Twoim przypadku.

Liron Levi
źródło
To miłe narzędzie; szkoda, że ​​nie jest za darmo
Mihai Damian
4

1. Utwórz /migrationsfolder z listą migracji opartych na SQL, gdzie każda migracja wygląda mniej więcej tak:

/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. Utwórz tabelę bazy danych zawierającą listę zastosowanych migracji, na przykład:

CREATE TABLE Migration (name TEXT);

3. Zaktualizuj logikę ładowania początkowego aplikacji, aby przed jej uruchomieniem pobierała listę migracji z /migrationsfolderu i uruchamiała migracje, które nie zostały jeszcze zastosowane.

Oto przykład zaimplementowany w JavaScript: Klient SQLite dla aplikacji Node.js.

Konstantin Tarkus
źródło
2

Kilka porad...

1) Zalecam umieszczenie całego kodu do migracji bazy danych do operacji NSO i uruchomienie jej w wątku w tle. Podczas migracji bazy danych można wyświetlić niestandardowy widok UIAlertView z pokrętłem.

2) Upewnij się, że kopiujesz bazę danych z pakietu do dokumentów aplikacji i używasz jej z tej lokalizacji, w przeciwnym razie po prostu nadpisujesz całą bazę danych przy każdej aktualizacji aplikacji, a następnie migrujesz nową, pustą bazę danych.

3) FMDB jest świetny, ale jego metoda executeQuery z jakiegoś powodu nie może wykonywać zapytań PRAGMA. Będziesz musiał napisać własną metodę, która bezpośrednio korzysta z sqlite3, jeśli chcesz sprawdzić wersję schematu za pomocą PRAGMA user_version.

4) Ta struktura kodu zapewni, że aktualizacje są wykonywane w kolejności i że wszystkie aktualizacje są wykonywane, bez względu na to, jak długo użytkownik przechodzi między aktualizacjami aplikacji. Można go dalej refaktoryzować, ale jest to bardzo prosty sposób spojrzenia na to. Ta metoda może być bezpiecznie uruchamiana za każdym razem, gdy tworzony jest pojedynczy element danych, i kosztuje tylko jedno małe zapytanie bazy danych, które występuje tylko raz na sesję, jeśli poprawnie skonfigurujesz singleton danych.

- (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];
    }
}
Rich Joslin
źródło
1

Jeśli zmienisz schemat bazy danych i cały kod, który używa go w trybie lockstep, jak może się zdarzyć w przypadku aplikacji osadzonych i zlokalizowanych na telefonie, problem jest właściwie pod kontrolą (nic nie jest porównywalne z koszmarem, jakim jest migracja schematu w korporacyjnej bazie danych które mogą obsługiwać setki aplikacji - także nie wszystkie pod kontrolą administratora danych ;-).

Alex Martelli
źródło
0

W przypadku .net możesz użyć lib:

EntityFrameworkCore.Sqlite.Migrations

Jest to proste, więc na każdej innej platformie możesz łatwo zaimplementować to samo zachowanie, co w lib.

ichensky
źródło