iOS开发基础86-FMDB

FMDB 完全指南:从基础 CRUD 到线程安全与事务优化

FMDB 是 iOS 开发中最常用的 SQLite 数据库封装库,它以 Objective-C 的方式封装了 SQLite 的 C 语言 API,让数据库操作变得简单直观。本文从 FMDB 的核心概念讲起,覆盖完整的 CRUD 操作、进阶查询、线程安全(FMDatabaseQueue)、事务优化、数据库升级迁移,并给出完善的工具类封装和常见坑排查。


一、FMDB 简介

一句话原理

FMDB 是对 SQLite 的 Objective-C 封装,把 SQLite 繁琐的 C 语言 API 包装成面向对象的方法,让你用 executeUpdate:executeQuery: 就能操作数据库。

三个核心类

作用 类比
FMDatabase 数据库对象,负责打开/关闭数据库、执行 SQL 一个数据库连接
FMResultSet 查询结果集,遍历查询返回的数据 查询结果的游标
FMDatabaseQueue 线程安全的数据库队列,多线程操作时必须用它 串行队列 + 数据库连接

为什么用 FMDB 而不是原生 SQLite

  • 原生 SQLite 是 C 语言 API,繁琐难用,需要手动管理内存、处理 C 字符串。
  • FMDB 封装成 OC 方法,代码简洁,自动管理内存。
  • 提供 FMDatabaseQueue 解决线程安全问题。
  • 提供 ? 占位符,自动防止 SQL 注入。
  • 社区成熟,大量项目使用。

二、环境搭建

1. 安装 FMDB

用 CocoaPods 安装(推荐):

# Podfile
pod 'FMDB'

然后运行:

pod install

2. 导入头文件

#import <FMDB/FMDB.h>

3. 数据库文件路径选择

数据库文件放在沙盒的哪个目录,是有讲究的:

目录 是否备份到 iCloud 适合场景
Documents ✅ 会备份 用户生成的重要数据(不能丢失)
Library/Caches ❌ 不备份 缓存数据(可重新下载/生成)
tmp ❌ 不备份,系统可能清理 临时数据

注意:如果数据库里存的是从服务器下载的可恢复数据,放 Library/Caches 更合适,避免占用 iCloud 备份空间。如果是用户本地生成的重要数据,放 Documents

// 获取 Documents 目录
NSString *docsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
NSString *dbPath = [docsPath stringByAppendingPathComponent:@"app.db"];

// 获取 Library/Caches 目录
NSString *cachePath = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)[0];
NSString *dbPath = [cachePath stringByAppendingPathComponent:@"cache.db"];

三、基本操作(CRUD)

1. 初始化与打开数据库

NSString *docsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
NSString *dbPath = [docsPath stringByAppendingPathComponent:@"test.db"];

// 创建 FMDatabase 对象
FMDatabase *db = [FMDatabase databaseWithPath:dbPath];

// 打开数据库(文件不存在会自动创建)
if (![db open]) {
    NSLog(@"数据库打开失败: %@", db.lastErrorMessage);
    return;
}

databaseWithPath:nil 会创建内存数据库(临时存在内存中,关闭后数据丢失),适合测试。

2. 创建表

NSString *sql = @"CREATE TABLE IF NOT EXISTS User ("
                  "id INTEGER PRIMARY KEY AUTOINCREMENT, "
                  "name TEXT NOT NULL, "
                  "age INTEGER, "
                  "email TEXT"
                  ")";

if (![db executeUpdate:sql]) {
    NSLog(@"创建表失败: %@", db.lastErrorMessage);
}

SQLite 常用数据类型

类型 说明 对应 OC 类型
INTEGER 整数 NSInteger / int / long long
REAL 浮点数 CGFloat / double / float
TEXT 文本字符串 NSString
BLOB 二进制数据 NSData
NULL 空值 nil / NSNull

SQLite 是动态类型的,即使声明了类型,也可以存其他类型的数据(但不建议这么做)。

3. 插入数据

// 用 ? 占位符,FMDB 自动处理类型转换和 SQL 注入
NSString *sql = @"INSERT INTO User (name, age, email) VALUES (?, ?, ?)";

if (![db executeUpdate:sql, @"张三", @(25), @"zhangsan@example.com"]) {
    NSLog(@"插入失败: %@", db.lastErrorMessage);
}

必须用 ? 占位符,不要用字符串拼接(stringWithFormat:),否则有 SQL 注入风险,且特殊字符(如引号)会导致 SQL 语法错误。

4. 查询数据

NSString *sql = @"SELECT * FROM User";
FMResultSet *results = [db executeQuery:sql];

// 遍历结果集
while ([results next]) {
    NSInteger userId = [results intForColumn:@"id"];
    NSString *name = [results stringForColumn:@"name"];
    NSInteger age = [results intForColumn:@"age"];
    NSString *email = [results stringForColumn:@"email"];
    
    NSLog(@"用户: id=%ld, name=%@, age=%ld, email=%@",
          (long)userId, name, (long)age, email);
}

// 关闭结果集(虽然 FMDB 会自动关闭,但手动关闭更安全)
[results close];

FMResultSet 常用取值方法

方法 返回类型
intForColumn: int
longForColumn: long
longLongIntForColumn: long long
doubleForColumn: double
boolForColumn: BOOL
stringForColumn: NSString
dataForColumn: NSData
dateForColumn: NSDate
objectForColumn: id(自动判断类型)

5. 更新数据

NSString *sql = @"UPDATE User SET name = ?, age = ? WHERE id = ?";

if (![db executeUpdate:sql, @"张三(已修改)", @(26), @(1)]) {
    NSLog(@"更新失败: %@", db.lastErrorMessage);
}

6. 删除数据

NSString *sql = @"DELETE FROM User WHERE id = ?";

if (![db executeUpdate:sql, @(1)]) {
    NSLog(@"删除失败: %@", db.lastErrorMessage);
}

7. 关闭数据库

[db close];

数据库用完后要关闭,释放资源。但如果用单例管理整个 App 生命周期,通常不需要手动关闭。


四、进阶查询

1. 条件查询(WHERE)

// 查询年龄大于 20 的用户
FMResultSet *results = [db executeQuery:@"SELECT * FROM User WHERE age > ?", @(20)];
while ([results next]) {
    // ...
}

2. 排序(ORDER BY)

// 按年龄降序排列
FMResultSet *results = [db executeQuery:@"SELECT * FROM User ORDER BY age DESC"];

// 按年龄升序,年龄相同按 id 降序
FMResultSet *results = [db executeQuery:@"SELECT * FROM User ORDER BY age ASC, id DESC"];

3. 分页查询(LIMIT / OFFSET)

// 第 1 页,每页 10 条
NSInteger page = 1;
NSInteger pageSize = 10;
NSInteger offset = (page - 1) * pageSize;

FMResultSet *results = [db executeQuery:@"SELECT * FROM User ORDER BY id DESC LIMIT ? OFFSET ?",
                        @(pageSize), @(offset)];

4. 模糊查询(LIKE)

// 查询名字包含"张"的用户
NSString *keyword = @"张";
NSString *likeKeyword = [NSString stringWithFormat:@"%%%@%%", keyword]; // %张%

FMResultSet *results = [db executeQuery:@"SELECT * FROM User WHERE name LIKE ?", likeKeyword];

% 匹配任意多个字符,_ 匹配单个字符。注意 %stringWithFormat: 中需要写成 %%

5. 聚合函数

// 统计用户总数
FMResultSet *countResult = [db executeQuery:@"SELECT COUNT(*) as count FROM User"];
if ([countResult next]) {
    NSInteger count = [countResult intForColumn:@"count"];
    NSLog(@"用户总数: %ld", (long)count);
}

// 平均年龄
FMResultSet *avgResult = [db executeQuery:@"SELECT AVG(age) as avgAge FROM User"];
if ([avgResult next]) {
    double avgAge = [avgResult doubleForColumn:@"avgAge"];
}

// 最大/最小年龄
// SELECT MAX(age) FROM User
// SELECT MIN(age) FROM User
// SELECT SUM(age) FROM User

6. 联表查询(JOIN)

假设有两张表:User(用户)和 Order(订单),通过 user_id 关联:

// 查询每个用户及其订单
NSString *sql = @"SELECT u.name, o.order_no, o.amount "
                 "FROM User u "
                 "LEFT JOIN Order o ON u.id = o.user_id "
                 "WHERE u.id = ?";

FMResultSet *results = [db executeQuery:sql, @(1)];
while ([results next]) {
    NSString *name = [results stringForColumn:@"name"];
    NSString *orderNo = [results stringForColumn:@"order_no"];
    double amount = [results doubleForColumn:@"amount"];
}

五、FMDatabaseQueue(线程安全,重点)

一句话原理

FMDatabase 本身不是线程安全的,多个线程同时操作同一个 FMDatabase 会导致崩溃或数据错乱。FMDatabaseQueue 内部用一个串行队列保证所有数据库操作按顺序执行,是多线程操作数据库的标准方案。

为什么必须用 FMDatabaseQueue

// ⚠️ 错误示例:多线程共享一个 FMDatabase,会崩溃
FMDatabase *db = [FMDatabase databaseWithPath:path];
[db open];

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [db executeUpdate:@"INSERT INTO User ..."]; // 线程 A 写
});

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    FMResultSet *rs = [db executeQuery:@"SELECT * FROM User"]; // 线程 B 读
    // 同时操作,可能崩溃!
});

FMDatabaseQueue 基本用法

// 创建队列(内部自动创建并打开数据库)
NSString *dbPath = [docsPath stringByAppendingPathComponent:@"app.db"];
FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:dbPath];

// 执行数据库操作(block 中的 db 是线程安全的)
[queue inDatabase:^(FMDatabase *db) {
    // 在这里执行任何数据库操作
    [db executeUpdate:@"INSERT INTO User (name, age) VALUES (?, ?)", @"张三", @(25)];
    
    FMResultSet *results = [db executeQuery:@"SELECT * FROM User"];
    while ([results next]) {
        // ...
    }
}];

多线程操作示例

// 线程 A:写入
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [queue inDatabase:^(FMDatabase *db) {
        [db executeUpdate:@"INSERT INTO User (name, age) VALUES (?, ?)", @"用户A", @(20)];
    }];
});

// 线程 B:写入
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [queue inDatabase:^(FMDatabase *db) {
        [db executeUpdate:@"INSERT INTO User (name, age) VALUES (?, ?)", @"用户B", @(30)];
    }];
});

// 线程 C:读取
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [queue inDatabase:^(FMDatabase *db) {
        FMResultSet *results = [db executeQuery:@"SELECT COUNT(*) as count FROM User"];
        if ([results next]) {
            NSLog(@"用户数: %d", [results intForColumn:@"count"]);
        }
    }];
});

FMDatabaseQueue 内部是串行队列,所有操作按提交顺序执行,不会并发,所以是线程安全的。

事务也用 FMDatabaseQueue

[queue inTransaction:^(FMDatabase *db, BOOL *rollback) {
    // 在这里执行多条语句,要么全部成功,要么全部回滚
    [db executeUpdate:@"INSERT INTO User (name) VALUES (?)", @"张三"];
    [db executeUpdate:@"INSERT INTO User (name) VALUES (?)", @"李四"];
    
    // 如果出错,设置 *rollback = YES 回滚
    if (出错) {
        *rollback = YES;
        return;
    }
}];

六、事务(Transaction)

一句话原理

事务把多条 SQL 语句打包成一个整体,要么全部成功,要么全部失败回滚。批量插入数据时用事务能大幅提升性能(几十上百倍)。

为什么批量插入要用事务

SQLite 默认每条 executeUpdate: 都是一个独立的事务,每条语句结束后都会刷盘(fsync),性能很差。如果插入 1000 条数据,就要刷盘 1000 次。

用事务把 1000 条插入包起来,只在最后提交时刷盘 1 次,性能能提升几十上百倍。

性能对比

方式 插入 1000 条耗时(约)
不用事务(每条单独提交) 5~10 秒
用事务(批量提交) 0.05~0.2 秒

使用方法

// 方式一:手动管理事务
[db executeUpdate:@"BEGIN TRANSACTION"];

for (NSInteger i = 0; i < 1000; i++) {
    [db executeUpdate:@"INSERT INTO User (name, age) VALUES (?, ?)",
     [NSString stringWithFormat:@"用户%ld", (long)i], @(i)];
}

[db executeUpdate:@"COMMIT"]; // 提交
// [db executeUpdate:@"ROLLBACK"]; // 出错时回滚
// 方式二:用 FMDB 的 inTransaction(推荐,自动处理回滚)
[db inTransaction:^(FMDatabase *db, BOOL *rollback) {
    for (NSInteger i = 0; i < 1000; i++) {
        BOOL success = [db executeUpdate:@"INSERT INTO User (name, age) VALUES (?, ?)",
                        [NSString stringWithFormat:@"用户%ld", (long)i], @(i)];
        if (!success) {
            *rollback = YES; // 出错,回滚
            break;
        }
    }
}];
// 方式三:FMDatabaseQueue + inTransaction(多线程环境推荐)
[queue inTransaction:^(FMDatabase *db, BOOL *rollback) {
    // 批量操作
}];

七、数据库升级与迁移

一句话原理

App 版本迭代时,数据库表结构可能变化(新增字段、新增表),需要处理升级逻辑,否则新版本打开旧版本的数据库会崩溃或数据丢失。

方案一:用 user_version 管理版本

SQLite 内置了 user_version 字段,可以存储数据库版本号,每次打开数据库时检查版本,决定是否需要升级。

- (void)setupDatabase {
    [queue inDatabase:^(FMDatabase *db) {
        // 获取当前数据库版本
        FMResultSet *rs = [db executeQuery:@"PRAGMA user_version"];
        NSInteger currentVersion = 0;
        if ([rs next]) {
            currentVersion = [rs intForColumnIndex:0];
        }
        [rs close];
        
        // 目标版本
        NSInteger targetVersion = 2;
        
        if (currentVersion < targetVersion) {
            // 需要升级
            [self migrateDatabase:db fromVersion:currentVersion toVersion:targetVersion];
            
            // 更新版本号
            [db executeUpdate:[NSString stringWithFormat:@"PRAGMA user_version = %ld", (long)targetVersion]];
        }
    }];
}

- (void)migrateDatabase:(FMDatabase *)db fromVersion:(NSInteger)from toVersion:(NSInteger)to {
    if (from < 1) {
        // 版本 0 → 1:创建初始表
        [db executeUpdate:@"CREATE TABLE IF NOT EXISTS User (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)"];
    }
    
    if (from < 2) {
        // 版本 1 → 2:新增 age 字段
        [db executeUpdate:@"ALTER TABLE User ADD COLUMN age INTEGER DEFAULT 0"];
    }
    
    // 后续版本继续加 if (from < 3) { ... }
}

方案二:ALTER TABLE 添加列

SQLite 支持用 ALTER TABLE 添加新列(但不支持删除列、修改列类型):

// 给 User 表添加 phone 字段,默认值为空字符串
[db executeUpdate:@"ALTER TABLE User ADD COLUMN phone TEXT DEFAULT ''"];

如果需要删除列或修改列类型,SQLite 不直接支持,需要:创建新表 → 导出数据 → 删除旧表 → 重命名新表。

方案三:复杂迁移(重建表)

// 1. 创建新表
[db executeUpdate:@"CREATE TABLE User_new (id INTEGER PRIMARY KEY, name TEXT, age INTEGER, phone TEXT)"];

// 2. 从旧表导入数据
[db executeUpdate:@"INSERT INTO User_new (id, name, age) SELECT id, name, age FROM User"];

// 3. 删除旧表
[db executeUpdate:@"DROP TABLE User"];

// 4. 重命名新表
[db executeUpdate:@"ALTER TABLE User_new RENAME TO User"];

八、完善的数据库管理工具类封装

用了 FMDatabase 存在线程安全隐患。下面给出一个封装,使用 FMDatabaseQueue 保证线程安全,并支持事务和版本管理。

DBManager.h

#import <Foundation/Foundation.h>
#import <FMDB/FMDB.h>

@interface DBManager : NSObject

/// 单例
+ (instancetype)sharedInstance;

/// 数据库队列(线程安全)
@property (nonatomic, strong, readonly) FMDatabaseQueue *queue;

/// 执行更新(INSERT/UPDATE/DELETE/CREATE)
- (BOOL)executeUpdate:(NSString *)sql, ...;

/// 执行查询
- (FMResultSet *)executeQuery:(NSString *)sql, ...;

/// 在事务中执行操作
- (void)inTransaction:(void (^)(FMDatabase *db, BOOL *rollback))block;

/// 批量插入(自动开启事务)
- (void)insertWithSQL:(NSString *)sql valuesArray:(NSArray<NSArray *> *)valuesArray;

@end

DBManager.m

#import "DBManager.h"

@interface DBManager ()
@property (nonatomic, strong, readwrite) FMDatabaseQueue *queue;
@end

@implementation DBManager

+ (instancetype)sharedInstance {
    static DBManager *instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[self alloc] init];
    });
    return instance;
}

- (instancetype)init {
    self = [super init];
    if (self) {
        [self setupDatabase];
    }
    return self;
}

- (void)setupDatabase {
    NSString *docsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
    NSString *dbPath = [docsPath stringByAppendingPathComponent:@"app.db"];
    
    // 创建 FMDatabaseQueue(线程安全)
    self.queue = [FMDatabaseQueue databaseQueueWithPath:dbPath];
    
    // 数据库版本管理和初始化
    [self setupTablesAndVersion];
}

- (void)setupTablesAndVersion {
    [self.queue inDatabase:^(FMDatabase *db) {
        // 获取当前版本
        FMResultSet *rs = [db executeQuery:@"PRAGMA user_version"];
        NSInteger currentVersion = 0;
        if ([rs next]) {
            currentVersion = [rs intForColumnIndex:0];
        }
        [rs close];
        
        NSInteger targetVersion = 1;
        
        if (currentVersion < targetVersion) {
            // 创建表
            [db executeUpdate:@"CREATE TABLE IF NOT EXISTS User ("
                              "id INTEGER PRIMARY KEY AUTOINCREMENT, "
                              "name TEXT NOT NULL, "
                              "age INTEGER DEFAULT 0, "
                              "email TEXT"
                              ")"];
            
            // 更新版本
            [db executeUpdate:[NSString stringWithFormat:@"PRAGMA user_version = %ld", (long)targetVersion]];
        }
    }];
}

- (BOOL)executeUpdate:(NSString *)sql, ... {
    __block BOOL success = NO;
    va_list args;
    va_start(args, sql);
    
    [self.queue inDatabase:^(FMDatabase *db) {
        success = [db executeUpdate:sql withArgumentsInArray:[self arrayFromVaList:args]];
        if (!success) {
            NSLog(@"DB 执行失败: %@, 错误: %@", sql, db.lastErrorMessage);
        }
    }];
    
    va_end(args);
    return success;
}

- (FMResultSet *)executeQuery:(NSString *)sql, ... {
    // 注意:executeQuery 返回的 FMResultSet 需要在 block 内遍历完
    // 因为 FMDatabaseQueue 的 block 结束后连接会归还
    // 所以这个方法只适合简单查询,复杂查询建议直接用 [queue inDatabase:]
    __block FMResultSet *result = nil;
    va_list args;
    va_start(args, sql);
    
    [self.queue inDatabase:^(FMDatabase *db) {
        result = [db executeQuery:sql withArgumentsInArray:[self arrayFromVaList:args]];
    }];
    
    va_end(args);
    return result;
}

- (void)inTransaction:(void (^)(FMDatabase *, BOOL *))block {
    [self.queue inTransaction:block];
}

- (void)insertWithSQL:(NSString *)sql valuesArray:(NSArray<NSArray *> *)valuesArray {
    [self.queue inTransaction:^(FMDatabase *db, BOOL *rollback) {
        for (NSArray *values in valuesArray) {
            BOOL success = [db executeUpdate:sql withArgumentsInArray:values];
            if (!success) {
                NSLog(@"批量插入失败: %@", db.lastErrorMessage);
                *rollback = YES;
                return;
            }
        }
    }];
}

// 把可变参数列表转成数组
- (NSArray *)arrayFromVaList:(va_list)args {
    NSMutableArray *array = [NSMutableArray array];
    id arg;
    while ((arg = va_arg(args, id)) != nil) {
        [array addObject:arg];
    }
    return array;
}

@end

使用示例

// 插入
[[DBManager sharedInstance] executeUpdate:@"INSERT INTO User (name, age) VALUES (?, ?)", @"张三", @(25)];

// 查询
[[DBManager sharedInstance].queue inDatabase:^(FMDatabase *db) {
    FMResultSet *rs = [db executeQuery:@"SELECT * FROM User WHERE age > ?", @(20)];
    while ([rs next]) {
        NSLog(@"%@", [rs stringForColumn:@"name"]);
    }
}];

// 批量插入(自动事务)
NSArray *values = @[
    @[@"张三", @(20)],
    @[@"李四", @(25)],
    @[@"王五", @(30)]
];
[[DBManager sharedInstance] insertWithSQL:@"INSERT INTO User (name, age) VALUES (?, ?)" valuesArray:values];

// 事务
[[DBManager sharedInstance] inTransaction:^(FMDatabase *db, BOOL *rollback) {
    [db executeUpdate:@"INSERT INTO User (name) VALUES (?)", @"张三"];
    [db executeUpdate:@"INSERT INTO User (name) VALUES (?)", @"李四"];
}];

九、FMDB vs Core Data vs 原生 SQLite

对比维度 FMDB Core Data 原生 SQLite
本质 SQLite 的 OC 封装 对象图管理框架(底层可用 SQLite) C 语言 API
学习成本 低(会 SQL 就行) 高(对象图、上下文、持久化存储) 高(C API 繁琐)
性能 高(直接操作 SQL) 中高(有缓存和优化) 最高(无封装开销)
线程安全 FMDatabaseQueue 解决 上下文合并机制 需自己处理
数据迁移 手动写 SQL 自动迁移(轻量) 手动
适合场景 大量结构化数据、复杂 SQL 查询、批量操作 对象关系映射、数据量不大、不需要复杂 SQL 极致性能需求
内存占用 较高(对象图缓存) 最低

选择建议:大多数 iOS 项目用 FMDB 就够了,简单直接性能好。Core Data 适合数据模型复杂、需要对象关系映射的场景。原生 SQLite 几乎不用,除非有极致性能需求。


十、Swift 版本对照

1. 安装

pod 'FMDB'
# 或 Swift 封装版
pod 'FMDB/Swift'

2. 基本操作

import FMDB

// 获取路径
let docsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
let dbPath = (docsPath as NSString).appendingPathComponent("test.db")

// 创建队列
let queue = FMDatabaseQueue(path: dbPath)

// 插入
queue.inDatabase { db in
    try? db.executeUpdate("INSERT INTO User (name, age) VALUES (?, ?)", values: ["张三", 25])
}

// 查询
queue.inDatabase { db in
    let rs = try? db.executeQuery("SELECT * FROM User", values: nil)
    while rs?.next() == true {
        let name = rs?.string(forColumn: "name")
        let age = rs?.int(forColumn: "age")
        print("name: \(name ?? ""), age: \(age ?? 0)")
    }
}

// 事务
queue.inTransaction { db, rollback in
    do {
        try db.executeUpdate("INSERT INTO User (name) VALUES (?)", values: ["张三"])
        try db.executeUpdate("INSERT INTO User (name) VALUES (?)", values: ["李四"])
    } catch {
        rollback.pointee = true
    }
}

Swift 中推荐用 FMDatabaseQueue,用法和 OC 基本一致,只是语法更简洁。


十一、常见问题与坑

Q1:FMDB 是线程安全的吗?

答案FMDatabase 本身不是线程安全的,多线程同时操作同一个实例会崩溃。必须用 FMDatabaseQueue 来保证线程安全,它内部用串行队列保证所有操作按顺序执行。

这是 FMDB 最常见的坑,很多人直接用 FMDatabase 单例,在多线程环境下会偶发崩溃。

Q2:查询后需要手动关闭 FMResultSet 吗?

答案:FMDB 会在 FMDatabase 关闭或下一次查询时自动关闭结果集,但手动调用 [results close] 更安全,尤其是长时间持有结果集时。如果用 FMDatabaseQueue,必须在 inDatabase: 的 block 内遍历完结果集,因为 block 结束后数据库连接会归还队列,结果集就无效了。

Q3:executeUpdate 和 executeQuery 有什么区别?

答案

  • executeUpdate::用于非查询语句(INSERT、UPDATE、DELETE、CREATE、ALTER、DROP 等),返回 BOOL 表示是否成功。
  • executeQuery::用于查询语句(SELECT),返回 FMResultSet 结果集。
  • 不要用 executeQuery: 执行非查询语句,也不要用 executeUpdate: 执行 SELECT。

Q4:为什么批量插入很慢?怎么优化?

答案:SQLite 默认每条语句都是一个事务,每次都要刷盘,所以批量插入很慢。优化方法是用事务把多条插入包起来,只在最后提交一次,性能能提升几十上百倍:

[db inTransaction:^(FMDatabase *db, BOOL *rollback) {
    for (int i = 0; i < 1000; i++) {
        [db executeUpdate:@"INSERT INTO User (name) VALUES (?)", [NSString stringWithFormat:@"用户%d", i]];
    }
}];

Q5:怎么防止 SQL 注入?

答案:用 ? 占位符,让 FMDB 自动处理参数绑定,不要用字符串拼接 SQL:

// ✅ 正确:用 ? 占位符
[db executeUpdate:@"SELECT * FROM User WHERE name = ?", userName];

// ❌ 错误:字符串拼接,有 SQL 注入风险
NSString *sql = [NSString stringWithFormat:@"SELECT * FROM User WHERE name = '%@'", userName];
[db executeQuery:sql];

如果 userName 是 ' OR '1'='1,拼接后的 SQL 会变成永真条件,返回所有用户数据。

Q6:数据库文件可以放在哪里?有什么区别?

答案

  • Documents:会备份到 iCloud,适合用户重要数据。
  • Library/Caches:不备份,系统内存不足时可能清理,适合缓存数据。
  • tmp:临时目录,App 退出后可能清理,适合临时文件。

注意:如果数据库放在 Documents 且数据量很大,会占用用户的 iCloud 备份空间,可能被 App Store 审核提醒。可恢复的缓存数据建议放 Caches。

Q7:怎么查看数据库里的数据?

答案

  • 用工具打开 .db 文件:如 DB Browser for SQLite(免费)、NavicatTablePlus
  • Xcode 中:Window → Devices and Simulators → 选择设备 → 下载 App 容器 → 查看 Documents 目录下的 .db 文件。
  • 模拟器:直接在 Finder 中找到模拟器沙盒路径(~/Library/Developer/CoreSimulator/Devices/...),用工具打开。

Q8:App 升级后数据库表结构变了怎么办?

答案:需要做数据库迁移。常用方案:

  1. PRAGMA user_version 管理数据库版本号。
  2. 每次打开数据库时检查版本,低于当前版本就执行迁移。
  3. 简单的新增字段用 ALTER TABLE ADD COLUMN
  4. 复杂的修改(删列、改类型)需要重建表:建新表 → 导入数据 → 删旧表 → 重命名。

详见本文第七节。

Q9:FMDatabaseQueue 的 block 是在主线程执行吗?

答案:不是。FMDatabaseQueue 内部创建了一个串行队列,block 在这个串行队列上执行,不是主线程。如果在 block 中需要更新 UI,必须回到主线程:

[queue inDatabase:^(FMDatabase *db) {
    // 查询数据
    NSMutableArray *users = [NSMutableArray array];
    FMResultSet *rs = [db executeQuery:@"SELECT * FROM User"];
    while ([rs next]) {
        [users addObject:[rs stringForColumn:@"name"]];
    }
    
    // 回主线程更新 UI
    dispatch_async(dispatch_get_main_queue(), ^{
        self.users = users;
        [self.tableView reloadData];
    });
}];

Q10:怎么判断操作是否成功?怎么获取错误信息?

答案executeUpdate: 返回 BOOL 表示是否成功。失败时通过 db.lastErrorMessage 获取错误信息,db.lastError 获取 NSError 对象:

BOOL success = [db executeUpdate:@"INSERT INTO User (name) VALUES (?)", @"张三"];
if (!success) {
    NSLog(@"错误码: %d", db.lastErrorCode);
    NSLog(@"错误信息: %@", db.lastErrorMessage);
    NSLog(@"错误对象: %@", db.lastError);
}

十二、总结

  • FMDB 是什么:SQLite 的 Objective-C 封装,三个核心类 FMDatabase(数据库连接)、FMResultSet(查询结果)、FMDatabaseQueue(线程安全队列)。
  • 基本 CRUDexecuteUpdate: 执行增删改,executeQuery: 执行查询,? 占位符防止 SQL 注入。
  • 线程安全(最重要)FMDatabase 不是线程安全的,多线程必须用 FMDatabaseQueue,内部串行队列保证安全。
  • 事务优化:批量插入必须用事务(inTransaction:),性能提升几十上百倍。
  • 数据库升级:用 PRAGMA user_version 管理版本,ALTER TABLE 加列,复杂迁移重建表。
  • 工具类封装:单例 + FMDatabaseQueue + 版本管理 + 事务支持,是实际项目的标准写法。
  • 常见坑:多线程用 FMDatabase 崩溃、批量插入慢、SQL 注入、查询结果集在 queue block 外使用、忘记回主线程更新 UI。
  • 选择建议:大多数项目用 FMDB 足够,简单直接性能好;需要对象关系映射用 Core Data。

posted @ 2018-05-30 15:30  Mr.陳  阅读(536)  评论(0)    收藏  举报