SQLite的运用

\color{red}{仅供参考}

class SQLiteManager: NSObject {
    static let shared = SQLiteManager()
    
    /// 数据库名称(以登录账号命名)
    var dbName: String {
        didSet {
            updateDatabaseIfNeeded()
        }
    }
    
    /// 数据库地址
    private(set) var dbURL: URL
    
    /// FMDatabase对象
    private(set) var db: FMDatabase
    
    /// FMDatabaseQueue对象
    private(set) var dbQueue: FMDatabaseQueue?
    
    private let queue = DispatchQueue(label: "com.kehuibao.com.sqliteManager")
    
    override init() {
        // 初始数据库名称
        let initialName = Defaults.loginAccount.isEmpty ? "tempdata" : Defaults.loginAccount
        dbName = initialName + ".db"
        
        // 初始化数据库路径
        do {
            dbURL = try FileManager.default
                .url(for: .applicationSupportDirectory, in: .userDomainMask,
                     appropriateFor: nil, create: true)
                .appendingPathComponent(dbName)
        } catch {
            // 提供更好的错误处理
            dbURL = URL(fileURLWithPath: "/fallback/path/to/default.db") // 备用路径
            PrintLog(message: "数据库路径获取失败,使用备用路径:\(dbURL)")
        }
        
        PrintLog(message: "数据库地址:\(dbURL)")
        
        // 初始化数据库对象
        db = FMDatabase(url: dbURL)
        dbQueue = FMDatabaseQueue(url: dbURL)
        
        super.init()
    }
    
    private func updateDatabaseIfNeeded() {
        // 只有在数据库路径变化时才更新数据库对象
        guard dbURL.lastPathComponent != dbName else { return }

        queue.sync {
            do {
                dbURL = try FileManager.default
                    .url(for: .applicationSupportDirectory, in: .userDomainMask,
                         appropriateFor: nil, create: true)
                    .appendingPathComponent(dbName)
                
                db = FMDatabase(url: dbURL)
                dbQueue = FMDatabaseQueue(url: dbURL)
                
                PrintLog(message: "数据库已更新,新地址:\(dbURL)")
            } catch {
                // 增强错误处理
                PrintLog(message: "更新数据库失败: \(error)")
                // 提供更多的错误反馈机制
            }
        }
    }
}

protocol SQLModelProtocol {}

@objcMembers
class SQLModel: NSObject, SQLModelProtocol {
    // 当前模型的数据库版本(子类可重写)
    class var dbVersion: Int { return 1 }
    
    // 模型对应的表名(如果没有指定,则使用类名作为表名)
    internal var table = ""
    
    // 记录每个模型对应的数据表是否已经创建完毕
    private static var verified = [String: Bool]()
    
    // 是否需要迁移
    private func needsMigration() -> Bool {
        let savedVersion = UserDefaults.dbVersion(forTable: table)
        return savedVersion < Self.dbVersion
    }
    
    // 记录数据库版本
    private func updateDBVersion() {
        UserDefaults.setDBVersion(Self.dbVersion, forTable: table)
    }
    
    // 初始化方法,在初始化时自动检查表是否存在,若不存在则创建
    required override init() {
        super.init()
        
        // 自动初始化表名,若用户未指定表名,则默认使用类名
        self.table = type(of: self).table
        
        // 检查表是否已经创建,如果没有则创建表
        let verified = SQLModel.verified[self.table]
        if verified == nil || !verified! || Defaults.changeDB {
            Defaults.changeDB = false
            createTableIfNeeded() // 创建表
        }
    }
    
    // 静态方法返回表名(如果类名和表名一致,则默认直接返回类名)
    static var table: String {
        return "\(classForCoder())" // 返回类名作为表名
    }
    
    // 返回主键字段名(可以在子类中覆盖此方法以指定主键)
    func primaryKey() -> String {
        return "id" // 默认主键为"id"
    }
    
    // 忽略的属性(返回不需要映射到数据库表的字段)
    func ignoredKeys() -> [String] {
        return [] // 默认没有需要忽略的字段
    }
    
    // 创建数据库表(检查并创建表,如果表不存在)
    private func createTableIfNeeded() {
        let db = SQLiteManager.shared.db
        var sql = "CREATE TABLE IF NOT EXISTS \(table) ("
        
        // 获取模型的所有字段和值,生成SQL语句的列部分
        let cols = values()
        var first = true
        for col in cols {
            sql += first ? getColumnSQL(column: col) : ", " + getColumnSQL(column: col)
            first = false
        }
        
        // 关闭SQL语句
        sql += ")"
        
        // 执行SQL语句创建表
        if db.open(), db.executeUpdate(sql, withArgumentsIn: []) {
            SQLModel.verified[table] = true
            PrintLog(message: "\(table) 表已创建")
            // 版本升级时检查字段
            if needsMigration() {
                checkAndAlterColumns()
                updateDBVersion() // 更新版本号
            }
        }
    }
    
    // 返回每个字段的SQL类型语句(根据字段类型生成SQL语句)
    private func getColumnSQL(column: (key: String, value: Any)) -> String {
        let key = column.key
        let val = column.value
        var sql = "'\(key)' "
        
        // 判断字段类型并生成对应的SQL类型语句
        switch val {
        case is Int:
            sql += "INTEGER"
            if key == primaryKey() {
                sql += " PRIMARY KEY AUTOINCREMENT" // 主键字段
            }
        case is Float, is Double:
            sql += "REAL DEFAULT \(val)"
        case is Bool:
            sql += "BOOLEAN DEFAULT " + ((val as! Bool) ? "1" : "0")
        case is Date:
            sql += "DATE"
        case is NSData:
            sql += "BLOB"
        default:
            sql += "TEXT" // 默认类型为文本
        }
        
        return sql
    }
    
    // 保存当前对象数据(如果主键为空或查询不到数据则执行新增,否则执行更新)
    func save() {
        let data = values() // 获取当前对象的字段和值
        // 如果有数据库队列,使用队列来进行数据库操作
        if let dbQueue = SQLiteManager.shared.dbQueue {
            dbQueue.inDatabase { db in
                let (sql, params) = getSQL(data: data) // 获取SQL语句和参数
                _ = db.executeUpdate(sql, withArgumentsIn: params ?? [])
            }
        }
    }
    
    // 删除当前对象数据
    @discardableResult
    func delete() -> Bool {
        let key = primaryKey() // 获取主键字段
        let data = values() // 获取当前对象的字段和值
        let db = SQLiteManager.shared.db
        
        // 查找主键值并执行删除操作
        if let rid = data[key] {
            if db.open() {
                let sql = "DELETE FROM \(table) WHERE \(primaryKey())=\(rid)"
                return db.executeUpdate(sql, withArgumentsIn: [])
            }
        }
        return false
    }
    
    // 删除指定条件的数据(根据条件过滤)
    @discardableResult
    class func remove(filter: String = "") -> Bool {
        let db = SQLiteManager.shared.db
        var sql = "DELETE FROM \(table)"
        
        // 如果传入了过滤条件,则附加到SQL语句中
        if !filter.isEmpty {
            sql += " WHERE \(filter)"
        }
        
        // 执行SQL语句
        if db.open() {
            return db.executeUpdate(sql, withArgumentsIn: [])
        } else {
            return false
        }
    }
    
    // 获取模型属性值(反射机制)
    internal func values() -> [String: Any] {
        var res = [String: Any]()
        let obj = Mirror(reflecting: self) // 使用反射获取当前对象的所有属性
        processMirror(obj: obj, results: &res) // 处理当前对象的属性
        getValues(obj: obj.superclassMirror, results: &res) // 递归获取父类的属性
        return res
    }
    
    // 递归获取父类属性
    private func getValues(obj: Mirror?, results: inout [String: Any]) {
        guard let obj = obj else { return }
        processMirror(obj: obj, results: &results) // 处理父类的属性
        getValues(obj: obj.superclassMirror, results: &results) // 递归继续获取上级类的属性
    }
    
    // 遍历Mirror反射结果并处理属性(用于将对象的属性值加入结果字典)
    private func processMirror(obj: Mirror, results: inout [String: Any]) {
        for (_, attr) in obj.children.enumerated() {
            if let name = attr.label {
                // 忽略 table 和 db 两个属性,这两个是类的特殊属性,不需要映射到数据库
                if name == "table" || name == "db" {
                    continue
                }
                
                // 忽略手动指定的属性(通过ignoredKeys()方法指定的属性不需要映射)
                if ignoredKeys().contains(name) || name.hasSuffix(".storage") {
                    continue
                }
                
                // 将属性值存入字典
                results[name] = unwrap(attr.value)
            }
        }
    }
    
    // 拆包Optional类型(如果属性值是Optional类型,则拆包后返回实际值)
    func unwrap(_ any: Any) -> Any {
        let mi = Mirror(reflecting: any)
        if mi.displayStyle != .optional {
            return any
        }
        
        // 如果是Optional类型,拆包并返回
        if mi.children.count == 0 { return any }
        let (_, some) = mi.children.first!
        return some
    }
    
    // 根据数据返回INSERT或REPLACE SQL语句(将对象的属性值转化为SQL语句)
    private func getSQL(data: [String: Any]) -> (String, [Any]?) {
        var sql = "INSERT OR REPLACE INTO \(table) ("
        var params: [Any]? = nil
        let pkey = primaryKey() // 获取主键字段
        var wsql = ""
        var first = true
        
        // 遍历对象的每个字段并生成SQL语句和参数列表
        for (key, val) in data {
            // 处理主键字段的特殊情况
            if pkey == key, val is Int, (val as! Int) <= 0 {
                continue
            }
            
            // 为SQL语句添加字段和对应的占位符
            if first && params == nil {
                params = [AnyObject]()
            }
            sql += first ? "\(key)" : ", \(key)"
            wsql += first ? " VALUES (?" : ", ?"
            params!.append(val)
            first = false
        }
        
        // 完成SQL语句并返回
        sql += ")" + wsql + ")"
        return (sql, params)
    }
}

// 扩展SQLModelProtocol实现查询功能
extension SQLModelProtocol where Self: SQLModel {
    
    // 根据SQL语句查询所有结果(执行指定的SQL查询语句并返回结果)
    static func rowsFor(sql: String = "") -> [Self] {
        var result = [Self]()
        let tmp = self.init()
        let data = tmp.values() // 获取字段和值
        let db = SQLiteManager.shared.db
        let fsql = sql.isEmpty ? "SELECT * FROM \(table)" : sql
        PrintLog(message: "数据库语句:\(fsql)")
        // 执行查询并遍历结果
        if let res = db.executeQuery(fsql, withArgumentsIn: []) {
            while res.next() {
                let t = self.init()
                for (key, _) in data {
                    if let val = res.object(forColumn: key) {
                        t.setValue(val, forKey: key)
                    }
                }
                result.append(t)
            }
        } else {
            PrintLog(message: "数据库查询失败")
        }
        return result
    }
    
    // 根据指定的过滤条件、排序和限制返回查询结果
    /// 查询
    /// - Parameters:
    ///   - filter: 条件
    ///   - order: 设置排序的字段,默认为""(不需要)
    ///   - limit: 结果数量的限制
    /// - Returns: 返回结果
    static func rows(filter: String = "", order: String = "", limit: Int = 0) -> [Self] {
        var sql = "SELECT * FROM \(table)"
        if !filter.isEmpty { sql += " WHERE \(filter)" }
        if !order.isEmpty { sql += " ORDER BY \(order)" }
        if limit > 0 { sql += " LIMIT \(limit)" }
        
        return self.rowsFor(sql: sql) // 调用查询方法
    }
    
    
    /// 分页获取数据
    /// - Parameters:
    ///   - pageNo: 页数 从1开始
    ///   - pageSize: 每页条数
    /// - Returns: 返回查询的数据
    static func rows(pageNo: Int, pageSize: Int = 20) -> [Self] {
        var sql = "SELECT * FROM \(table)"
        if pageSize > 0 { sql += " LIMIT \(pageSize)" }
        let offset = (pageNo - 1) * pageSize
        if offset > 0 {
            sql += " OFFSET \(offset)"
        }
        return self.rowsFor(sql: sql) // 调用查询方法
    }
}

//MARK: - 检查表字段是否与模型属性匹配,并自动修正类型
extension SQLModel{
    // MARK: -检查并修正字段类型
    private func checkAndAlterColumns() {
        PrintLog(message: "检查并修正字段类型")
        let db = SQLiteManager.shared.db
        guard db.open() else { return }

        // 1. 获取当前表的字段信息
        let pragmaQuery = "PRAGMA table_info(\(table))"
        guard let pragmaResult = db.executeQuery(pragmaQuery, withArgumentsIn: []) else { return }
        
        // 创建一个字典,用于存储表中已有字段的名称和类型
        var existingColumns = [String: String]() // [字段名: 类型]
        while pragmaResult.next() {
            let name = pragmaResult.string(forColumn: "name") ?? "" // 字段名
            let type = pragmaResult.string(forColumn: "type") ?? "" // 字段类型
            existingColumns[name] = type.uppercased() // 统一转为大写,SQLite 不区分大小写
        }

        // 2. 对比模型属性与数据库字段,检查是否有新的字段需要添加,或者现有字段类型是否需要修改
        let modelColumns = values() // 获取当前模型中的所有字段和对应的值
        for (key, value) in modelColumns {
            let expectedType = getColumnType(value: value, columnName: key) // 获取字段的预期类型

            // 如果字段不存在,则直接添加
            if existingColumns[key] == nil {
                addColumn(name: key, type: expectedType)
            } else if existingColumns[key] != expectedType {
                // 如果字段类型不匹配,触发修改操作
                alterColumn(name: key, newType: expectedType)
            }
        }
    }

    // 添加新字段到表中
    private func addColumn(name: String, type: String) {
        let db = SQLiteManager.shared.db
        guard db.open() else { return }

        // 使用 ALTER TABLE 添加字段
        let sql = "ALTER TABLE \(table) ADD COLUMN \(name) \(type)"
        if db.executeUpdate(sql, withArgumentsIn: []) {
            PrintLog(message: "字段 \(name) 已添加")
        } else {
            PrintLog(message: "添加字段 \(name) 失败")
        }
    }

    // 修改字段类型(通过创建临时表并迁移数据的方式)
    private func alterColumn(name: String, newType: String) {
        let db = SQLiteManager.shared.db
        guard db.open() else { return }

        // 1. 开启数据库事务
        db.beginTransaction()
        
        // 2. 创建临时表(新结构,包含所有需要的字段)
        let tempTable = "\(table)_temp"
        let createTempTable = """
        CREATE TABLE \(tempTable) (
            \(getAllColumnsSQL())  -- 生成包含所有字段的 CREATE SQL 语句
        );
        """
        // 执行创建临时表的 SQL 语句
        guard db.executeUpdate(createTempTable, withArgumentsIn: []) else {
            db.rollback() // 如果创建临时表失败,回滚事务
            return
        }

        // 3. 迁移数据(将原表中的数据插入到临时表中)
        let copyData = """
        INSERT INTO \(tempTable) (\(getAllColumnNames()))
        SELECT \(getAllColumnNames()) FROM \(table);
        """
        // 执行数据迁移操作
        guard db.executeUpdate(copyData, withArgumentsIn: []) else {
            db.rollback() // 如果数据迁移失败,回滚事务
            return
        }

        // 4. 删除原表
        guard db.executeUpdate("DROP TABLE \(table)", withArgumentsIn: []) else {
            db.rollback() // 如果删除原表失败,回滚事务
            return
        }

        // 5. 重命名临时表为原表名
        guard db.executeUpdate("ALTER TABLE \(tempTable) RENAME TO \(table)", withArgumentsIn: []) else {
            db.rollback() // 如果重命名失败,回滚事务
            return
        }

        // 6. 提交事务,保存所有操作
        db.commit()
    }
    
    // 获取所有列名,以逗号分隔
    private func getAllColumnNames() -> String {
        return values().keys.joined(separator: ", ")
        
    }

    // 获取所有列的完整 SQL 语句(用于创建临时表)
    private func getAllColumnsSQL() -> String {
        return values().map { (key, value) in
            let columnType = getColumnType(value: value, columnName: key) // 获取字段类型
            let defaultValue = getDefaultSQLValue(value: value) // 获取默认值的 SQL 表达式
            return "\(key) \(columnType) \(defaultValue)"
        }.joined(separator: ", ")
    }
    
    // 获取字段的预期 SQLite 类型(包括主键和自增长)
    private func getColumnType(value: Any, columnName: String) -> String {
        switch value {
        case is Int:
            // 如果是主键字段,返回 "INTEGER PRIMARY KEY AUTOINCREMENT"
            if columnName == primaryKey() {
                return "INTEGER PRIMARY KEY AUTOINCREMENT"
            }
            return "INTEGER"  // 非主键字段返回 INTEGER 类型
        case is Float, is Double:
            return "REAL"
        case is Bool:
            return "BOOLEAN"
        case is Date:
            return "DATE"
        case is NSData:
            return "BLOB"
        default:
            return "TEXT"  // 默认类型
        }
    }
    
    // 获取字段的默认值 SQL 表达式(处理默认值)
    private func getDefaultSQLValue(value: Any) -> String {
        switch value {
        case is Bool:
            // 布尔类型默认值
            return "DEFAULT 0" // 或者 "DEFAULT 1" (视需要而定)
        case is Int, is Float, is Double:
            // 数字类型默认值
            return "DEFAULT 0"
        case is String:
            // 字符串类型默认值
            return "DEFAULT ''"
        default:
            return ""
        }
    }
}

//MARK: - 数据库版本控制(使用 UserDefaults)
extension UserDefaults {
    private static let kDBVersionPrefix = "db_version_"
    static func dbVersion(forTable table: String) -> Int {
        return UserDefaults.standard.integer(forKey: kDBVersionPrefix + table)
    }
    
    static func setDBVersion(_ version: Int, forTable table: String) {
        UserDefaults.standard.set(version, forKey: kDBVersionPrefix + table)
    }
}

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容