FMDB+FMDBMigrationManager的數據庫遷移

最近公司在做一個新項目,剛好碰到需要數據緩存的需求;加之因為之前項目是用的Relam,還沒用過久仰大名的FMDB,所以就入手一試;But, 但凡提到數據庫,隨著應用版本升級迭代,數據遷移這個問題無可避免

寫文章預覽模式時,排版還挺不錯的,一發(fā)布,就跟shit一樣- - 簡書這Markdown的渲染也是丑的沒誰了...

快刀斬亂麻

新版本涉及到修改原有數據庫表的字段時,把原有的數據庫表刪除,再重新建表。這種方式雖然簡單,但缺點也是顯而易見的,就是太過暴力了,這樣原先的數據就完全丟失了;所以放棄,尋找更優(yōu)雅的方式。

顧全大局

要做到優(yōu)雅,那么就要在數據遷移的時候,原先的數據不會受影響,同時還要考慮到盡可能多的情況,在不同的情況下,數據遷移不會出現問題,尤其是線上crash。
這里需要考慮到兩種情況:

  • 用戶當前版本跟最新版本只差一個版本
  • 用戶當前版本跟最新版本差多個版本

看到這里,立馬可以聯想到,代碼肯定很多個版本判斷去保證用戶舊的數據格式平滑的升級到最新,而且每發(fā)一個帶有修改數據庫表的版本,都要增加一個判斷;當然,相信每個項目都會存在數據庫版本兼容的邏輯代碼。既然無可避免,那么有沒有工具來幫助我們以更快,更簡潔的方式實現這部分邏輯,釋放我們的雙手?

通過搜索了解到FMDBMigrationManager這個工具,FMDB官方README也有提到,使用起來也簡單,可以很大程度簡化我們的工作。因為比較簡單,下面就直接附上代碼,有注釋,可能有點長,看起來不方便,建議放到Xcode上;
我自己下午粗略的測了一下,暫時沒發(fā)現什么問題,如果代碼有紕漏錯誤,或者寫得不夠優(yōu)雅的地方,望大佬們指出哈。

class DataBaseManager {
    
    public static let shared = DataBaseManager()
    public static let serialQueue = DispatchQueue(label: "DataBase.Serial.Queue")
    
    private(set) var dataBase: FMDatabaseQueue!
    
    @discardableResult
    public func createDabaBase() -> Bool {
        var created = false
        
        dataBase = FMDatabaseQueue(path: pathToDataBase())
        if dataBase != nil {
            createAppVersionTable()
            executeMigration()
            created = true
        }
        return created
    }

    // 數據庫文件的所在路徑
    private func pathToDataBase() -> String {
        let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!
        return path + "/\(AccountInfo.userID).db" // 多用戶情況下,根據用戶ID創(chuàng)建多個數據庫
    }
    
    // 創(chuàng)建一張記錄著APP版本的表,只會有一條記錄
    private func createAppVersionTable() {
        guard !appVersionTableExists() else {return} // 判斷APP版本表是否存在
        
        dataBase.inDatabase { (db) in
            let createAppVersion = "CREATE TABLE if not exists APP_Version (id integer primary key not null, version text not null)"
            do {
                try db.executeUpdate(createAppVersion, values: nil)
                // 第一次創(chuàng)建后,插入當前版本信息記錄
                try db.executeUpdate("INSERT INTO APP_Version (version) values(?)", values: [AppInfo.currentShortVersion])
                print("Create App Version Table successful")
            }catch {
                print("Create App Version error: \(error)")
            }
        }
    }
    
    // 判斷APP版本表中的值是否小于當前APP版本
    private func isCachedVersionLessThanCurrent() -> Bool {
        var isLess = false
        dataBase.inDatabase { (db) in
            do {
                let results = try db.executeQuery("SELECT * from APP_Version", values: nil)
                while results.next() { // 只會有一條數據
                    if let cacheVersion = results.string(forColumn: "version") {
                        isLess = cacheVersion < AppInfo.currentShortVersion
                    }
                }
                // 如果小于當前版本,則更新APP版本表的記錄
                if isLess {
                    try db.executeUpdate("UPDATE APP_Version SET version=? WHERE id=1", values: [AppInfo.currentShortVersion])
                }
            }catch {
                print(db.lastErrorMessage)
            }
        }
        return isLess
    }
    
    // 獲取當前數據庫中所有表
    private func fetchExistsTables() -> [String] {
        var existsTables: [String] = []
        dataBase.inDatabase { (db) in
            do {
                let results = try db.executeQuery("SELECT * from sqlite_master WHERE type='table'", values: nil)
                while results.next() {
                    if let tableName = results.string(forColumn: "name") {
                        existsTables.append(tableName)
                    }
                }
            }catch {
                print(db.lastErrorMessage)
            }
        }
        return existsTables
    }
    
    // 檢查APP版本記錄表是否存在
    private func appVersionTableExists() -> Bool {
        let tables = fetchExistsTables();
        return tables.contains("APP_Version")
    }
    
    // 數據庫遷移操作
    private func executeMigration() {
        
        // 判斷當前數據庫中表的數量大于1時,即除了APP版本表之外
        // APP版本表中記錄的值小于當前APP版本
        guard fetchExistsTables().count > 1, isCachedVersionLessThanCurrent() else { return }
        
        let migrationManager = FMDBMigrationManager(databaseAtPath: pathToDataBase(), migrationsBundle: Bundle.main)
        guard let manager = migrationManager else { return }
        manager.dynamicMigrationsEnabled = false
        if !manager.hasMigrationsTable {
            do {
                try manager.createMigrationsTable()
                print("創(chuàng)建遷移表成功")
            }catch {
                print("創(chuàng)建遷移表失敗")
                return
            }
        }
        /* ----- 相關遷移操作寫在這里 ----- */
        // 下面這幾個是示例代碼
        // 這里就寫著每個版本需要做的修改
        // NOTE: DataBaseMigration為遵循<FMDBMigrating>的自定義類,其中每個操作的version都必須保持遞增?。。?        let ageMigration = DataBaseMigration(name: "ADD_Age", version: 0, updateQueries: ["ALTER TABLE Person ADD age integer"]) // 新增age字段
        let detailsMigration = DataBaseMigration(name: "ADD_Details", version: 1, updateQueries: ["ALTER TABLE Person ADD details text default \"Tomorrow\""])
        let vipMigration = DataBaseMigration(name: "ADD_VIP", version: 2, updateQueries: ["ALTER TABLE Person ADD vip integer default 0"])
        
        manager.addMigrations([ageMigration, detailsMigration, vipMigration])
        /* ----- 相關遷移操作寫在這里 ----- */
        do {
            try manager.migrateDatabase(toVersion: UINT64_MAX) { (progress) in
                if let progress = progress {
                    print("數據庫遷移進度 \(progress)  \(Thread.current)")
                }
            }
            print("數據庫遷移成功")
        }catch {
            print("數據庫遷移失敗 \(error.localizedDescription)")
        }
    }
}

上面是數據庫的基本操作,然后具體的邏輯業(yè)務,我以Extension的形式來寫;

// 這部分為測試代碼
extension DataBaseManager {
    public func createPersonTable() {
        dataBase.inDatabase { (db) in
            let createFriendQuery = "CREATE TABLE if not exists Person (id long primary key not null, name text default \"\", gender integer default 1)"
            do {
                try db.executeUpdate(createFriendQuery, values: nil)
                print("Create Person Table successful")
            }catch {
                print("Create Person Table error: \(error)")
            }
        }
    }
    
    public func addPersonData() {
        DataBaseManager.serialQueue.async {
            self.dataBase.inDatabase { (db) in
                let insert = "REPLACE INTO Person (id, name, gender, age) values (?, ?, ?, ?)"
                let tuples = [(0, "Person1", 1, 20), (10, "Person2", 0, 10), (11, "Person3", 0, 25)]
                do {
                    for tuple in tuples {
                        try db.executeUpdate(insert, values: [tuple.0, tuple.1, tuple.2, tuple.3])
                    }
                    print("Create Person Table successful")
                }catch {
                    print("Create Person Table error: \(error)")
                }
            }
        }
    }
}

到這里,基本就結束了;可以看到,使用FMDBMigrationManager還是很方便的;
最后啰嗦一句,我是用的DB Browser for SQLite查看數據庫,畢竟查看數據庫里面數據是驗證我們代碼最直接的方式,哈。
當然你如果嫌安裝麻煩,也可以打開Terminal 輸入sqlite3 查看

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • 發(fā)現 關注 消息 iOS 第三方庫、插件、知名博客總結 作者大灰狼的小綿羊哥哥關注 2017.06.26 09:4...
    肇東周閱讀 15,422評論 4 61
  • 我叫毛毛。 我住在江西省玉山縣 , 我是一個全職媽媽。 我有一個女兒,一個兒子。 他們現在都在上大學。 我想跟大家...
    京晶閱讀 1,408評論 1 2
  • 來!做我的愛人吧 我會帶你去我的故鄉(xiāng) 清晨,當陽光??吭诖芭_ 不用急著梳洗 赤腳跑到小木屋后的花園 踩在松軟的雜著...
    LS醉生夢死閱讀 186評論 0 0
  • 窗外明媚的不能再明媚了,我不能去想象外面有多響晴,日光穿透在我家14層的玻璃上,窗前的那棵三七花,在陽光下越發(fā)顯得...
    大白菜小豆腐閱讀 326評論 0 1
  • 今天上午開了一上午的會議,大致了解了下甸村的基本情況,也基本掌握了扶貧攻堅政策等常識。 下午,所有...
    和竹芳閱讀 9,301評論 0 0

友情鏈接更多精彩內容