FMDB+FMDBMigrationManager的数据库迁移

最近公司在做一个新项目,刚好碰到需要数据缓存的需求;加之因为之前项目是用的Relam,还没用过久仰大名的FMDB,所以就入手一试;But, 但凡提到数据库,随着应用版本升级迭代,数据迁移这个问题无可避免

写文章预览模式时,排版还挺不错的,一发布,就跟shit一样- - 简书这Markdown的渲染也是丑的没谁了...

快刀斩乱麻

新版本涉及到修改原有数据库表的字段时,把原有的数据库表删除,再重新建表。这种方式虽然简单,但缺点也是显而易见的,就是太过暴力了,这样原先的数据就完全丢失了;所以放弃,寻找更优雅的方式。

顾全大局

要做到优雅,那么就要在数据迁移的时候,原先的数据不会受影响,同时还要考虑到尽可能多的情况,在不同的情况下,数据迁移不会出现问题,尤其是线上crash。
这里需要考虑到两种情况:

  • 用户当前版本跟最新版本只差一个版本
  • 用户当前版本跟最新版本差多个版本

看到这里,立马可以联想到,代码肯定很多个版本判断去保证用户旧的数据格式平滑的升级到最新,而且每发一个带有修改数据库表的版本,都要增加一个判断;当然,相信每个项目都会存在数据库版本兼容的逻辑代码。既然无可避免,那么有没有工具来帮助我们以更快,更简洁的方式实现这部分逻辑,释放我们的双手?

通过搜索了解到FMDBMigrationManager这个工具,FMDB官方README也有提到,使用起来也简单,可以很大程度简化我们的工作。因为比较简单,下面就直接附上代码,有注释,可能有点长,看起来不方便,建议放到Xcode上;
我自己下午粗略的测了一下,暂时没发现什么问题,如果代码有纰漏错误,或者写得不够优雅的地方,望大佬们指出哈。

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创建多个数据库
    }
    
    // 创建一张记录着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)
                // 第一次创建后,插入当前版本信息记录
                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("创建迁移表成功")
            }catch {
                print("创建迁移表失败")
                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)")
        }
    }
}

上面是数据库的基本操作,然后具体的逻辑业务,我以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 查看

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,016评论 4 62
  • 我叫毛毛。 我住在江西省玉山县 , 我是一个全职妈妈。 我有一个女儿,一个儿子。 他们现在都在上大学。 我想跟大家...
    京晶阅读 1,337评论 1 2
  • 来!做我的爱人吧 我会带你去我的故乡 清晨,当阳光停靠在窗台 不用急着梳洗 赤脚跑到小木屋后的花园 踩在松软的杂着...
    LS醉生梦死阅读 134评论 0 0
  • 窗外明媚的不能再明媚了,我不能去想象外面有多响晴,日光穿透在我家14层的玻璃上,窗前的那棵三七花,在阳光下越发显得...
    大白菜小豆腐阅读 263评论 0 1
  • 今天上午开了一上午的会议,大致了解了下甸村的基本情况,也基本掌握了扶贫攻坚政策等常识。 下午,所有...
    和竹芳阅读 9,187评论 0 0