SQLite3语句以及FMDB数据存储初步学习
文章目录
- SQLite3语句以及实现FMDB数据存储
- 简介
- SQLite数据类型
- SQLite常用函数
- NSBundle
- 相关语句的使用
- 基础使用
- FMDB使
- 简介
- 使用
- 事务处理
SQLite3语句以及实现FMDB数据存储
简介
SQLite是一款轻型的数据库,包含在一个相对小的C库中,大多用于嵌入式产品,占用资源低。存储在单个文件中。无需服务端参与,体积小、查询灵活、稳定。
SQLite数据类型
一共有五种数据类型:
- NULL:表示该值为NULL
- INTEGER:整形值
- REAL:浮点值
- TEXT:文本字符串,通常UTF-8、UTF-16
- BLOB:二进制大对象,用于存储二进制数据的字段或者数据类型
其中BLOB是给机器看的,TEXT是给人看的
SQLite常用函数
-
sqlite3_open:打开数据库,如果目录下没有就新建一个
-
sqlite3_close:关闭数据库
-
sqlite3_exec:用于执行SQL语句
-
sqlite3_prepare_v2:用于将SQL查询语句编译为可执行的SQLite语句
-
sqlite3_step:执行语句
-
sqlite3_column_text:取出当前行的某列文本
NSBundle
是iOS开发中一个常见且重要的类,用于加载应用程序中的资源文件,iOS中,NSBundle是一个封装了应用程序中所有资源和可执行文件的对象。每个iOS应用都有一个主NSBundle,包含了应用程序的二进制文件和所有资源。当我们要在应用中需要访问某些文件时,都可以通过NSBundle去获取对应路径或加载他们。
有一个经典的应用场景就是在应用启动时加载图片资源、配置文件,或者其他需要在运行时动态加载的资源。
- 使用NSBundle加载资源
在iOS中最常见的用法就是访问主bundle,通过其获取到应用内资源文件的路径或者直接加载
NSBundle *mainBundle = [NSBundle mainBundle];
NSString *imagePath = [mainBundle pathForResource:@"image" ofType:@"png"];//获取路径
UIImage *image = [UIImage imageWithContentsOfFile:imagePath];//加载图片
- 获取bundle中的路径
我们可以使用pathForResource:ofType:方法获取某个资源的路径
NSString *path = [[NSBundle mainBundle] pathForResource:@"fileName" ofType:@"txt"];
相关语句的使用
在我们的应用中通常有下面几个常用目录:
| 目录 | 用途 |
|---|---|
| Documents | 保存用户数据(可被 iTunes/iCloud 备份) |
| Library | 程序配置、缓存 |
| tmp | 临时文件,系统可清理 |
所以我们将数据库放在Documents较为合适。
基础使用
- 配置
打开xCode项目:
xCode -> Target -> Build Phases -> Link Binary With Libraries 加入 libsqlite3.tbd
- 导入
在对应文件头部导入包
#import <sqlite3.h>
- 打开/创建数据库
sqlite3* db;//一个数据库指针,用于保存SQLite数据库连接对象的地址
NSString* dbPath = [NSHomeDirectory() stringByAppendingPathComponent:@"Documents/mydb.sqlite"];
int rc = sqlite3_open([dbPath UTF8String], &db);//会自动创建文件
if (rc != SQLITE_OK) {NSLog(@"打开数据库失败:%s", sqlite3_errmsg(db));sqlite3_close(db);//即使打开失败,数据库也可能会分配一些内部资源,例如错误缓冲区啥的,确保不产生数据泄露。return;
}
- 创建表
这里我们假设创建一个person表:
id:整数
name:文本
age:整数
const char *createSQL = "CREATE TABLE IF NOT EXISTS person (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, age INTEGER);";
//一个C字符串,保存要执行的SQL语句:
//CREATE TABLE IF NOT EXISTS person ():创建名为person的表,这里的IF NOT EXISTS表示如果表已存在不报错。
//id INTEGER PRIMARY KEY AUTOINCREMENT:其中id是列名,这里是主键,自动增长
//name TEXT:文本字段,用来存储字符串
//age INTEGER:整数,存储年龄(就像是一个三列的表格)
char *errMsg;//用于存放错误信息地址的指针
if (sqlite3_exec(db, createSQL, NULL, NULL, &errMsg) == SQLITE_OK) {//第一个NULL指的是回调函数,第二个NULL位置指的是回调参数,&errMsg用来写入错误信息,在sqlite3_exec出错时,会分配一段内存并把指针设置给errMsg,这个内存需要我们自己使用sqlite3_free()来释放,防止内存泄露。NSLog(@"创建表成功");
} else {if (errMsg) {NSLog(@"创建表失败: %s", errMsg);sqlite3_free(errMsg);} else {NSLog(@"创建表失败:%s (rc = %d)", sqlite3_errMsg(db), rc);}
}
细节拓展:
什么时候errMsg会被分配?
当sqlite3_exec语句返回错误吗,并且我们传入了&errMsg时,SQLite通常会为一条错误信息分配内存,并存储指针。我们在执行sqlite_free()时需要注意判空
为什么使用sqlite3_free()而不是free()
因为SQLite可能使用的是自己的内存分配器,确保正确释放
Sqlite3_exec支持一次传入含分号的多条SQL
- 插入数据
假设我们想要向表中添加一行数据:name = “Alice” age = 25
const char *insertSQL = "INSERT INTO person (name, age) VALUES (?, ?);";//要执行的SQL模版,?是占位符,代表将来要绑定实际的值,可以将SQL语句与数据分离,安全高效
sqlite3_stmt *stmt; // 语句对象,是SQlite用来表示准备好的SQL语句的结构体类型,stmt将在sqlite3_prepare_v2成功后指向已编译好的语句对象,后续用它来绑定参数和执行if (sqlite3_prepare_v2(db, insertSQL, -1, &stmt, NULL) == SQLITE_OK) {//sqlite3_prepare_v2:把SQL文本编译成一个可执行的字节码对象,以便于反复执行或者绑定参数/*参数;1.db:数据库连接句柄2.insertSQL:SQL文本3.-1:表示SQLite自动用strlen来测长度,-1表示直到字符串结束符4.&stmt:输出参数,函数成功后把sqlite3_stmt* 写到这里5.NULL:指向未处理SQL的结尾,这里不需要所以传NULL*/sqlite3_bind_text(stmt, 1, [@"Alice" UTF8String], -1, SQLITE_TRANSIENT);/*参数:1.stmt:之前准备好的语句对象2.1:绑定索引,SQLite的绑定索引从1开始3.["Alice" UTF8String]:转换为c字符串4.-1:字符串长度,-1表示计算到\05.SQLITE_TRANSIENT:告诉SQLite内部复制这段数据,保证安全最终结果就是将Alice绑定到第一个?了*/sqlite3_bind_int(stmt, 2, 25);//向第二个?绑定整数值25if (sqlite3_step(stmt) == SQLITE_DONE) {//对于INSERT/UPDATE/DELETE/DDL语句,成功执行完毕后用会返回SQLITE_DONE,表示语句已经完成//对于SELECT查询,会返回SQLITE_ROW表示取到一行结果,多次sqlite3_step直到返回SQLITE_DONE。NSLog(@"插入成功");} else {NSLog(@"插入失败: %s", sqlite3_errmsg(db));}sqlite3_finalize(stmt); // 清理,用来销毁/释放stmt对象占用的内存与资源,必须调用,否则会发生内存泄漏。如果想重复使用同一个stmt,可以使用sqlite3_reset(stmt) + sqlite3_clear_bindings(stmt)来重置和清零已经绑定的参数,然后在绑定新的值并执行sqlite3_step,直到不需要时再执行sqlite3_finalize。
}
注意事项与拓展:
- 绑定索引从1开始,不要写成0了
- Sqlite3_errMsg(db)是针对整个连接的最后错误信息,如果需要更详细的错误码可以打印sqlite3_errcode(db);
- 不要将指向临时内存的指针直接交给SQLite,如果我们只是保存指针,而不内部复制这段字符串,在OC底层,NSString在任何时候都啃可能释放,会导致乱码或者崩溃。所以建议使用SQLITE_TRANSIENT。
- 如果表有INTEGER PRIMARY KEY或者rowid,插入成功后我们可以调用sqlite3_lase_insert_rowid(db)获取刚插入行的ID(long long类型),需要基于同一个数据库连接
- 事务:SQLite每执行一次写(INSERT/UPDATE/DELETE),默认都会开启并提交事务,这会产生磁盘I/O,成本高,我们可以使用(BEGIN TRANSACTION; …; COMMIT;)可以将所有写合并为一次提交,提升速度
示例:对person(name, age)表做批量插入处理,使用预编译的SQL语句(sqlite3_prepare-v2),在一个显示事务(BEGIN TRANSACTION)内循环绑定参数并执行sqlite3_step执行插入,出错回滚,成功提交,最后释放
char* err = NULL; //假设stmt已经编译好 sqlite3_exec(db, "BEGIN TRANSACTION;", NULL, NULL, &err); for (int i = 0; i < n; i++) {sqlite3_bind_text(stmt, 1, [names[i] UTF8String], -1, (void(*)(void*))SQLITE_TRANSIENT);sqlite3_bind_int(stmt, 2, ages[i]);if (sqlite3_step(stmt) != SQLITE_DONE) {NSLog(@"插入失败: %s", sqlite3_errmsg(db));sqlite3_reset(stmt); // 重置到可重新绑定/执行的状态sqlite3_clear_bindings(stmt); // 清除之前的绑定(可选,但推荐) } sqlite3_finalize(stmt); sqlite3_exec(db, "COMMIT TRANSACTION;", NULL, NULL, &err);
重用语句(prepare一次,循环里bind+step+reset):
为什么重用:sqlite3_prepare_v2是把SQL编译为内部字节码,本身是具有成本的,所以对于重复执行同一类语句,只prepare一次,然后在循环里重复绑定和执行最好
// 假设 db 已打开并有效 const char *sql = "INSERT INTO person (name, age) VALUES (?, ?);";//定义SQL语句字符串,设置两个占位符 sqlite3_stmt *stmt = NULL;//用来保存sqlite3_prepare_v2返回的预编译语句句柄。是一个已编译语句对象的结构体指针。 int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);//将SQL文本编译成可执行的预编译语句并将句柄放入stmt if (rc != SQLITE_OK) {NSLog(@"prepare failed: %s", sqlite3_errmsg(db));return; }char *err = NULL; // 开事务 if (sqlite3_exec(db, "BEGIN TRANSACTION;", NULL, NULL, &err) != SQLITE_OK) {/*使用BEGIN TRANSACTION将后续批量插入放在单个事务里,可以显著提升插入性能*/NSLog(@"BEGIN failed: %s", err ? err : sqlite3_errmsg(db));if (err) sqlite3_free(err);sqlite3_finalize(stmt);//正确释放stmtreturn; }BOOL hadError = NO; for (int i = 0; i < count; ++i) {sqlite3_bind_text(stmt, 1, [names[i] UTF8String], -1, (void(*)(void*))SQLITE_TRANSIENT);sqlite3_bind_int(stmt, 2, ages[i]);rc = sqlite3_step(stmt);if (rc != SQLITE_DONE) {NSLog(@"插入失败 (index %d): %s (rc=%d)", i, sqlite3_errmsg(db), rc);hadError = YES;// 不 break 也可以,但通常 break 并回滚更好break;}sqlite3_reset(stmt);sqlite3_clear_bindings(stmt); }if (hadError) {sqlite3_exec(db, "ROLLBACK;", NULL, NULL, &err);if (err) { NSLog(@"ROLLBACK error: %s", err); sqlite3_free(err); err = NULL; } } else {sqlite3_exec(db, "COMMIT;", NULL, NULL, &err);if (err) { NSLog(@"COMMIT error: %s", err); sqlite3_free(err); err = NULL; } }sqlite3_finalize(stmt);流程总结:
- 预编译SQL
- 开启事务
- 多次绑定参数 + 执行
- 出错回滚,全部成功提交
- 释放语句对象
- 查询
- (void)searchDataFromSQL:(sqlite3* )db {const char* querySQL = "SELECT id, name, age FROM Person";sqlite3_stmt* stmt;if (sqlite3_prepare_v2(db, querySQL, -1, &stmt, NULL) == SQLITE_OK) {while (sqlite3_step(stmt) == SQLITE_ROW) {int pid = sqlite3_column_int(stmt, 0);const unsigned char* pname = sqlite3_column_text(stmt, 1);int age = sqlite3_column_int(stmt, 2);NSLog(@"ID: %d, NAME: %s, AGE: %d", pid, pname, age);[self deleteDataFromSQL:db];}}sqlite3_finalize(stmt);
}
- 删除
- (void)deleteDataFromSQL:(sqlite3* )db {char* errMsg;NSString* updateSQL = @"UPDATE Person SET age = 30 WHERE ID = 1";if (sqlite3_exec(db, [updateSQL UTF8String], NULL, NULL, &errMsg) == SQLITE_OK) {NSLog(@"删除成功");} else {NSLog(@"删除失败:%s", errMsg);}
}
- 更新
- (void)updateDataFromSQL:(sqlite3*) db {char* errMsg;NSString *updateSQL = @"UPDATE Person SET age = 30 WHERE name = 'Alice'";if ( sqlite3_exec(db, [updateSQL UTF8String], NULL, NULL, &errMsg) == SQLITE_OK) {NSLog(@"更新成功");} else {NSLog(@"更新失败:%s", errMsg);}
}
FMDB使
简介
是一个对SQLite封装的OC框架,比直接使用SQLite C API更简单安全。支持多线程、面向对象。
使用
这里我们使用一个简单的用户信息表的例子来学习使用FMDB
- 首先需要导入第三方库,步骤同以前一样。导入后在需要使用的文件头部导入包
#import <FMDB/FMDB.h>
- 创建数据库
NSString* docsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
/*
NSSearchPathForDirectoriesInDomains:是Cocoa提供的函数,用来查找系统中某个路径的位置
参数1:指明我们要查询的目录,这里是Documents目录,可读写,通常用来存储应用的用户数据
参数2:指明只在当前用户的沙盒目录中寻找
参数3:表示返回完整路径
方法返回的是一个NSArray数组,这里返回第一个路径
*/
NSString* dbPath = [docsPath stringByAppendingPathComponen:@"myDatabase.sqlite"];//拼接路径
FMDatabase* db = [FMDatabase databaseWithPath:dbpath];
if ([db open]) {//success
} else {//failure
}
executeUpdate:用于执行INSERT、UPDATE、DELETE、CREATE TABLE等SQL语句
- 创建表
NSString* createTableSQL = @"CREAT TABLE IF NOT EXISTS Person (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, age INTEGER)";
if ([db executeUpdate:createTableSQL]) {//success
} else {//failure
}
- 插入数据
NSString* insertSQL = @"INSERT INTO Person (name, age) VALUES (?, ?)";
if ([db executeUpdate:insertSQL, @"ALice", @(25)]) {//success
} else {//failure
}
注释:FMDB支持?占位符,也支持:key占位符
使用占位符的作用是防止SQL注入:具体大概就是说攻击者会提交一些其他的SQL语句使数据库返回其想要的效果
- 查询数据
NSString* querySQL = @"SELECT * FROM Person";
FMResultSet* resultSet = [db executeQuery:querySQL];
/*
db是连接到数据库的实例
executeQuery:是FMDatabase提供的方法,用来执行SQL查询,接收一个查询语句并返回FMResultSet对象,是一个结果集 */
while ([resultSet next]) {int userId = [resultSet stringForColumn:@"id"];NSString* name = [resultSet stringForColumn;@"name"];int age = [resultSet intForColumn:@"age"];
}
[resultSet close];//使用close方法来释放结果集所占用的资源(虽然会自动调用)
- 更新数据
NSString* updateSQL = @"UPDATE Person SET age = ? WHERE name = ?";
if ([db executeUpdate:updateSQL, @(26), @"Tom"]) {//success
} else {//failure
}
- 删除数据库
NSString *deleteSQL = @"DELETE FROM Person WHERE name = ?";
BOOL deleteResult = [db executeUpdate:deleteSQL, @"张三"];
if (deleteResult) {NSLog(@"删除成功");
} else {NSLog(@"删除失败: %@", [db lastErrorMessage]);
}
- 关闭数据库
[db close];
事务处理
我们简单学习一下事务处理的流程:
事务是指一系列数据库操作要么全成功,要么全部失败的机制。保证了在数据库中操作的原子性、一致性、隔离性和持久性原则。事务的基本流程包括三部分:
- 开始事务:可以使用beginTransaction来标记事务的开始
- 提交事务:如果事务中的操作都成功执行,可以使用commit来提交事务,达到永久保存的效果
- 回滚事务:如果事务中的某个操作失败,回滚操作rollback可以撤销所有的已执行操作,使数据库回到初始状态
NSString *dbPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
dbPath = [dbPath stringByAppendingPathComponent:@"myDatabase.sqlite"];
FMDatabase *db = [FMDatabase databaseWithPath:dbPath];if ([db open]) {// 开始事务[db beginTransaction];BOOL shouldRollback = NO;@try {// 第一个插入操作BOOL result1 = [db executeUpdate:@"INSERT INTO t_user (name, age) VALUES (?, ?)", @"Tom", @(28)];if (!result1) {shouldRollback = YES;[db rollback]; // 如果第一个操作失败,直接回滚return;}// 第二个插入操作BOOL result2 = [db executeUpdate:@"INSERT INTO t_user (name, age) VALUES (?, ?)", @"Alice", @(30)];if (!result2) {shouldRollback = YES;[db rollback]; // 如果第二个操作失败,回滚return;}// 提交事务if (!shouldRollback) {[db commit]; // 所有操作成功,提交事务}}@catch (NSException *exception) {// 异常捕获,回滚事务shouldRollback = YES;[db rollback];NSLog(@"事务执行失败: %@", exception);}@finally {if (!shouldRollback) {NSLog(@"事务提交成功");}}// 关闭数据库连接[db close];
} else {NSLog(@"数据库打开失败");
}
为了保证数据库的性能,我们要尽可能去确保事务短小
- 使用FMDatabaseQueue进行多线程处理
NSString *dbPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
dbPath = [dbPath stringByAppendingPathComponent:@"myDatabase.sqlite"];//获取数据库路径// 创建FMDatabaseQueue实例
FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:dbPath];// 启动多个线程进行并发数据库操作
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.myapp.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);[queue inDatabase:^(FMDatabase* db) {if ([db executeUpdate:@"INSERT INTO Person (name, age) VALUES (?, ?)"", @"Alice", @(28)]) {//success} else {//failure}
}];//inDatabase:是一个块,会在FMDatabaseQueue内部执行,此时数据库线程是安全的。for (int i = 0; i < 5; i++) {dispatch_async(concurrentQueue, ^{[queue inDatabase:^(FMDatabase * _Nonnull db) {[db executeUpdate:@"INSERT INTO t_user (name, age) VALUES (?, ?)", [NSString stringWithFormat:@"用户 %d", i], @(20 + i)];NSLog(@"插入用户 %d", i);}];});
}dispatch_barrier_async(concurrentQueue, ^{NSLog(@"所有任务完成");
});
FMDatabase不具备线程安全,可能会导致并发冲突,所以不适合在多线程中使用。在多线程中我们应该使用FMDatabaseQueue来管理数据库的访问,这是一个线程安全的队列,确保数据库按操作顺序串行执行,适用于需要进行多个数据库操作但是每次只能有一个线程访问数据库的场景,允许我们在多个线程中访问数据库,但是所有数据库操作将会按照顺序执行。即串行。
补充
下面简单了解一个多线程数据库操作问题之避免死锁:
当线程之间相互依赖等待时,容易发生死锁,我们可以先使用FMDatabaseQueue来确保数据库操作是串行执行的,同时在多个数据库操作之间,尽量减少相互依赖,保证操作的独立性。
