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为遵循的自定义类,其中每个操作的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 查看

你可能感兴趣的:(FMDB+FMDBMigrationManager的数据库迁移)