在 iOS 中使用 FMDB 同时操作多个(这里以两个数据库为例子)两个数据库的核心技术点集中在 跨数据库操作 和 资源管理 上。以下是详细的技术解析和关键代码实现:
一、跨数据库操作的核心 SQL 语句
通过 SQLite 的 ATTACH DATABASE 命令实现跨库操作,这是多数据库协同的核心机制。
1. 附加数据库(Attach)
语法:
ATTACH DATABASE '数据库文件路径' AS 数据库别名;
示例:
let db1: FMDatabase // 主数据库
let db2Path: String // 第二个数据库路径
do {
try db1.executeUpdate("ATTACH DATABASE ? AS secondary", values: [db2Path])
} catch {
print("附加失败: \(error.localizedDescription)")
}
关键特性:
- 附加后的数据库通过
别名.表名访问(例如secondary.users) - 附加操作只在当前数据库连接有效
- 最多支持 10 个附加数据库(SQLite 默认限制)
2. 跨数据库查询
-- 联合查询两个数据库的表
SELECT * FROM main.table1
UNION ALL
SELECT * FROM secondary.table2;
-- 跨数据库 JOIN 操作
SELECT * FROM main.orders
JOIN secondary.customers
ON orders.customer_id = customers.id;
3. 分离数据库(Detach)
语法:
DETACH DATABASE 数据库别名;
示例:
do {
try db1.executeUpdate("DETACH DATABASE secondary", values: nil)
} catch {
print("分离失败: \(error.localizedDescription)")
}
二、关键技术实现细节
1. 路径处理要点
| 场景 | 处理方法 | 示例代码 |
|---|---|---|
| 应用沙盒内数据库 | 使用 NSDocumentDirectory | FileManager.default.urls(for: .documentDirectory, ...) |
| Bundle 中的预置数据库 | 先复制到可写目录再附加 | FileManager.default.copyItem(atPath:toPath:) |
正确路径示例:
let docDir = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!
let db2Path = (docDir as NSString).appendingPathComponent("secondary.db")
2. 事务的原子性控制
跨数据库事务需要 手动协调:
// 开始事务
db1.beginTransaction()
db2.beginTransaction()
var success = true
do {
// 操作主数据库
try db1.executeUpdate("INSERT INTO main.logs (message) VALUES (?)", values: ["操作开始"])
// 操作附加数据库
try db1.executeUpdate("INSERT INTO secondary.records (data) VALUES (?)", values: [data])
// 操作独立数据库
try db2.executeUpdate("UPDATE stats SET count = count + 1", values: nil)
} catch {
success = false
print("操作失败: \(error)")
}
// 提交或回滚
if success {
db1.commit()
db2.commit()
} else {
db1.rollback()
db2.rollback()
}
3. 线程安全方案
使用 FMDatabaseQueue 管理每个数据库:
let mainQueue = FMDatabaseQueue(path: mainDbPath)
let secondaryQueue = FMDatabaseQueue(path: secondaryDbPath)
mainQueue.inTransaction { db, rollback in
do {
// 附加数据库
try db.executeUpdate("ATTACH DATABASE ? AS sec", values: [secondaryDbPath])
// 跨数据库操作
try db.executeUpdate("""
INSERT INTO main.logs (message)
SELECT name FROM sec.users WHERE id = ?
""", values: [userID])
} catch {
rollback.pointee = true
}
}
三、高级技术场景
1. 数据库版本迁移
// 将旧数据库数据迁移到新结构
mainQueue.inDatabase { db in
do {
// 附加旧数据库
try db.executeUpdate("ATTACH DATABASE ? AS old", values: [legacyDbPath])
// 迁移用户数据
try db.executeUpdate("""
INSERT INTO main.users (id, name)
SELECT id, username FROM old.user_table
""")
// 分离旧库
try db.executeUpdate("DETACH DATABASE old")
} catch {
print("迁移失败: \(error)")
}
}
2. 读写分离架构
// 主库(写操作)
let masterDb = FMDatabase(path: masterPath)
// 从库(读操作)
let slaveDb = FMDatabase(path: slavePath)
// 写操作
masterDb.executeUpdate("INSERT INTO data ...")
// 同步到从库(示例)
masterDb.executeUpdate("ATTACH DATABASE ? AS slave", values: [slavePath])
masterDb.executeUpdate("INSERT INTO slave.data SELECT * FROM main.data WHERE ...")
3. 跨数据库索引优化
-- 创建跨库视图
CREATE VIEW combined_view AS
SELECT m.id, m.name, s.score
FROM main.students m
JOIN secondary.scores s ON m.id = s.student_id;
-- 使用 EXPLAIN 分析查询计划
EXPLAIN QUERY PLAN
SELECT * FROM combined_view WHERE score > 90;
四、性能优化策略
| 策略 | 实现方法 | 效果 |
|---|---|---|
| WAL 模式 | PRAGMA journal_mode=WAL | 提升并发读写性能 |
| 内存数据库 | ATTACH DATABASE ':memory:' AS mem | 临时数据处理 |
| 预编译语句 | 使用 FMDatabase.executeUpdate:withParameterDictionary: | 减少 SQL 解析开销 |
| 批量操作 | 事务包裹多个操作 | 减少 I/O 次数 |
WAL 模式配置示例:
do {
try db.executeUpdate("PRAGMA journal_mode=WAL", values: nil)
try db.executeUpdate("PRAGMA synchronous=NORMAL", values: nil)
} catch {
print("WAL 配置失败")
}
五、错误排查指南
1. 常见错误码
| 错误码 | 含义 | 解决方案 |
|---|---|---|
| 5 | BUSY(数据库被锁定) | 检查未关闭的 ResultSet 或事务 |
| 14 | CANTOPEN | 验证数据库文件路径权限 |
| 19 | CONSTRAINT(约束冲突) | 检查唯一索引和空值约束 |
2. 调试技巧
// 启用 SQL 跟踪
db.traceExecution = true
// 获取最后错误信息
if db.hadError() {
print("错误代码: \(db.lastErrorCode()), 信息: \(db.lastErrorMessage())")
}
// 检查表是否存在
let tableExists = try db.tableExists("users")
六、关键注意事项总结
-
连接生命周期管理:
- 确保每个线程使用独立的数据库连接
- 及时调用
close()释放资源
-
文件系统权限:
- 验证应用对数据库文件的读写权限
- 处理 iOS 文件沙盒限制
-
内存管理:
- 及时关闭
FMResultSet - 使用
@autoreleasepool包裹批量操作
- 及时关闭
-
SQL 注入防护:
- 始终使用参数化查询
- 避免拼接 SQL 字符串