iOS 中使用 FMDB 同时操作多个数据库技术点

391 阅读3分钟

在 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. 路径处理要点

场景处理方法示例代码
应用沙盒内数据库使用 NSDocumentDirectoryFileManager.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. 常见错误码

错误码含义解决方案
5BUSY(数据库被锁定)检查未关闭的 ResultSet 或事务
14CANTOPEN验证数据库文件路径权限
19CONSTRAINT(约束冲突)检查唯一索引和空值约束

2. 调试技巧

// 启用 SQL 跟踪
db.traceExecution = true

// 获取最后错误信息
if db.hadError() {
    print("错误代码: \(db.lastErrorCode()), 信息: \(db.lastErrorMessage())")
}

// 检查表是否存在
let tableExists = try db.tableExists("users")

六、关键注意事项总结

  1. 连接生命周期管理

    • 确保每个线程使用独立的数据库连接
    • 及时调用 close() 释放资源
  2. 文件系统权限

    • 验证应用对数据库文件的读写权限
    • 处理 iOS 文件沙盒限制
  3. 内存管理

    • 及时关闭 FMResultSet
    • 使用 @autoreleasepool 包裹批量操作
  4. SQL 注入防护

    • 始终使用参数化查询
    • 避免拼接 SQL 字符串