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