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(免费)、Navicat、TablePlus。 - Xcode 中:Window → Devices and Simulators → 选择设备 → 下载 App 容器 → 查看 Documents 目录下的 .db 文件。
- 模拟器:直接在 Finder 中找到模拟器沙盒路径(
~/Library/Developer/CoreSimulator/Devices/...),用工具打开。
Q8:App 升级后数据库表结构变了怎么办?
答案:需要做数据库迁移。常用方案:
- 用
PRAGMA user_version管理数据库版本号。 - 每次打开数据库时检查版本,低于当前版本就执行迁移。
- 简单的新增字段用
ALTER TABLE ADD COLUMN。 - 复杂的修改(删列、改类型)需要重建表:建新表 → 导入数据 → 删旧表 → 重命名。
详见本文第七节。
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(线程安全队列)。 - 基本 CRUD:
executeUpdate:执行增删改,executeQuery:执行查询,?占位符防止 SQL 注入。 - 线程安全(最重要):
FMDatabase不是线程安全的,多线程必须用FMDatabaseQueue,内部串行队列保证安全。 - 事务优化:批量插入必须用事务(
inTransaction:),性能提升几十上百倍。 - 数据库升级:用
PRAGMA user_version管理版本,ALTER TABLE加列,复杂迁移重建表。 - 工具类封装:单例 +
FMDatabaseQueue+ 版本管理 + 事务支持,是实际项目的标准写法。 - 常见坑:多线程用 FMDatabase 崩溃、批量插入慢、SQL 注入、查询结果集在 queue block 外使用、忘记回主线程更新 UI。
- 选择建议:大多数项目用 FMDB 足够,简单直接性能好;需要对象关系映射用 Core Data。

浙公网安备 33010602011771号