แนวทางปฏิบัติที่ดีที่สุดสำหรับการย้ายฐานข้อมูลในแอปสำหรับ Sqlite


97

ฉันใช้ sqlite สำหรับ iphone ของฉันและฉันคาดว่าสคีมาฐานข้อมูลอาจเปลี่ยนแปลงเมื่อเวลาผ่านไป gotchas หลักการตั้งชื่อและสิ่งที่ต้องระวังในการย้ายข้อมูลที่ประสบความสำเร็จในแต่ละครั้งคืออะไร?

ตัวอย่างเช่นฉันคิดที่จะต่อท้ายเวอร์ชันเข้ากับชื่อฐานข้อมูล (เช่น Database_v1)

คำตอบ:


116

ฉันดูแลแอปพลิเคชันที่ต้องอัปเดตฐานข้อมูล 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 ไม่รองรับพารามิเตอร์คุณจะต้องระบุค่าจริง: "pragma user_version = 1"
csgero

2
ฉันมีหนึ่งคำถาม. สมมติว่าคุณเป็นเวอร์ชันเริ่มต้น 1. และเวอร์ชันปัจจุบันคือ 5 มีการอัปเดตบางอย่างในเวอร์ชัน 2,3,4 ผู้ใช้ดาวน์โหลดเฉพาะเวอร์ชัน 1 ของคุณและอัปเกรดเป็นเวอร์ชัน 5 แล้วคุณควรทำอย่างไร
Bagusflyer

6
อัปเดตฐานข้อมูลในหลายขั้นตอนโดยใช้การเปลี่ยนแปลงที่จำเป็นในการเปลี่ยนจากเวอร์ชัน 1 เป็นเวอร์ชัน 2 จากนั้นเวอร์ชัน 2 เป็นเวอร์ชัน 3 เป็นต้นจนกว่าจะเป็นข้อมูลล่าสุด วิธีง่ายๆในการทำเช่นนี้คือการมีคำสั่ง switch โดยแต่ละคำสั่ง "case" จะอัพเดตฐานข้อมูลทีละเวอร์ชัน คุณ "เปลี่ยน" เป็นเวอร์ชันฐานข้อมูลปัจจุบันและคำสั่ง case จะผ่านไปจนกว่าการอัปเดตจะเสร็จสมบูรณ์ เมื่อใดก็ตามที่คุณอัปเดตฐานข้อมูลเพียงแค่เพิ่มคำสั่ง case ใหม่ ดูคำตอบด้านล่างโดย Billy Gray สำหรับตัวอย่างโดยละเอียดเกี่ยวกับเรื่องนี้
Rngbus

1
@KonstantinTarkus ตามเอกสาร application_idเป็นบิตพิเศษสำหรับการระบุรูปแบบไฟล์โดยfileยูทิลิตี้เช่นไม่ใช่สำหรับเวอร์ชันของฐานข้อมูล
xaizek

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 คุณจะจัดการอย่างไร
เก็บตัว

1
@confile ไม่มีbreakข้อความในswitchดังนั้นการย้ายข้อมูลที่ตามมาทั้งหมดจะเกิดขึ้นด้วย
เคลือบ

ไม่มีลิงก์ Strip
Pedro Luz

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

และนี่คือวิธีการโยกย้ายที่เรียกจากตัวควบคุมมุมมอง:

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

เหตุใดคุณจึงใช้เมธอด "setDatabaseSchemaVersion" เพื่อส่งคืน "user_version" "user_version" และ "schema_version" เป็นสอง pragmas ที่แตกต่างกันที่ฉันคิด
Paul Brewczynski

@PaulBrewczynski เพราะฉันชอบคำที่ใช้กันทั่วไปไม่ใช่คำ SQLite และฉันก็เรียกมันว่ามันคืออะไร (เวอร์ชันของสคีมาฐานข้อมูลของฉัน) ฉันไม่สนใจคำศัพท์เฉพาะของ SQLite ในกรณีนี้และschema_versionปกติแล้ว pragma ก็ไม่ใช่สิ่งที่ผู้คนจัดการด้วยเช่นกัน
Andrey Tarantsov

คุณได้เขียน: // 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: วิธีการ! ดังนั้นคุณไม่จำเป็นต้องใช้วิธี verbose ของ @Andrey Tarantsov: - (int) databaseSchemaVersion! และ (โมฆะ) setDatabaseSchemaVersion: (int) เวอร์ชัน เอกสาร FMDB: ccgus.github.io/fmdb/html/Categories/… :
Paul Brewczynski

4

ทางออกที่ดีที่สุด IMO คือการสร้างกรอบการอัพเกรด SQLite ฉันมีปัญหาเดียวกัน (ในโลก C #) และฉันสร้างกรอบของตัวเองขึ้นมา คุณสามารถอ่านเกี่ยวกับเรื่องนี้ที่นี่ มันทำงานได้อย่างสมบูรณ์และทำให้การอัปเกรด (ก่อนหน้านี้ฝันร้าย) ของฉันทำงานได้โดยใช้ความพยายามเพียงเล็กน้อยในด้านข้างของฉัน

แม้ว่าไลบรารีจะถูกนำไปใช้ใน C # แต่แนวคิดที่นำเสนอในที่นั้นก็ควรใช้งานได้ดีในกรณีของคุณเช่นกัน


เป็นเครื่องมือที่ดี แย่จังมันไม่ฟรี
Mihai Damian

4

1. สร้าง/migrationsโฟลเดอร์ที่มีรายการการย้ายข้อมูลโดยใช้ SQL ซึ่งการย้ายข้อมูลแต่ละครั้งจะมีลักษณะดังนี้:

/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. อัปเดตตรรกะ bootstrap ของแอปพลิเคชันเพื่อให้ก่อนที่จะเริ่มระบบจะคว้ารายการการย้ายข้อมูลจาก/migrationsโฟลเดอร์และเรียกใช้การย้ายข้อมูลที่ยังไม่ได้ใช้

นี่คือตัวอย่างที่ใช้กับ JavaScript: SQLite Client สำหรับ Node.js Apps


2

เคล็ดลับบางประการ ...

1) ฉันแนะนำให้ใส่รหัสทั้งหมดเพื่อย้ายฐานข้อมูลของคุณไปยัง NSOperation และเรียกใช้ในเธรดพื้นหลัง คุณสามารถแสดง UIAlertView ที่กำหนดเองด้วยสปินเนอร์ในขณะที่กำลังย้ายฐานข้อมูล

2) ตรวจสอบให้แน่ใจว่าคุณกำลังคัดลอกฐานข้อมูลของคุณจากบันเดิลไปยังเอกสารของแอพและใช้จากตำแหน่งนั้นมิฉะนั้นคุณจะเขียนทับฐานข้อมูลทั้งหมดด้วยการอัปเดตแต่ละแอปจากนั้นย้ายฐานข้อมูลใหม่ที่ว่างเปล่า

3) FMDB ดีมาก แต่เมธอด executeQuery ไม่สามารถทำแบบสอบถาม PRAGMA ได้ด้วยเหตุผลบางประการ คุณจะต้องเขียนวิธีการของคุณเองที่ใช้ sqlite3 โดยตรงหากคุณต้องการตรวจสอบเวอร์ชันสคีมาโดยใช้ PRAGMA user_version

4) โครงสร้างรหัสนี้จะช่วยให้แน่ใจว่าการอัปเดตของคุณจะดำเนินการตามลำดับและการอัปเดตทั้งหมดจะดำเนินการไม่ว่าผู้ใช้จะใช้เวลาระหว่างการอัปเดตแอปนานแค่ไหนก็ตาม สามารถ refactored เพิ่มเติมได้ แต่นี่เป็นวิธีที่ง่ายมากในการดู วิธีนี้สามารถทำงานได้อย่างปลอดภัยทุกครั้งที่มีการสร้างอินสแตนซ์ข้อมูลของคุณซิงเกิลตันและมีค่าใช้จ่ายเพียงหนึ่งแบบสอบถามฐานข้อมูลเล็ก ๆ ที่เกิดขึ้นเพียงครั้งเดียวต่อเซสชันหากคุณตั้งค่าข้อมูลเดี่ยวอย่างถูกต้อง

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

หากคุณเปลี่ยนสคีมาฐานข้อมูลและรหัสทั้งหมดที่ใช้ในขั้นตอนล็อกเช่นเดียวกับที่เป็นไปได้ในแอพที่ฝังตัวและที่อยู่ในโทรศัพท์ปัญหานั้นอยู่ภายใต้การควบคุมที่ดี (ไม่มีอะไรเทียบได้กับฝันร้ายนั่นคือการย้ายสคีมาบนฐานข้อมูลขององค์กร ที่อาจให้บริการแอพหลายร้อยแอพไม่ใช่ทั้งหมดที่อยู่ภายใต้การควบคุมของ DBA ;-)


โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.