FMDB简介
SQLite是一个轻量级的关系型数据库,在使用的时候需要加入libsqlite3.tbd
的依赖并引入 sqlite3.h
头文件即可. 但是,原生的SQLite API在使用上十分繁杂. 因此,FMDB是以OC的方式封装了SQLite的C语言API,使用起来比原生的 API 更加简便。
优点
- 更加面向对象,省去了很多麻烦、冗余的C语言代码
- 比苹果自带的Core Data框架,更加轻量级和灵活
- 提供了多线程安全的数据库操作方法,有效地防止数据混乱
缺点
- 因为它是OC语言封装的,只能在ios使用,跨平台操作存在局限性
使用步骤
导入libsqlite3.0框架,导入头文件FMDatabase.h
推荐使用App Store里的Datum Free工具操作本地的.sqlite数据库文件
FMDB有三个主要的类
- FMDatabase
一个FMDatabase对象就代表一个单独的SQLite数据库,用来执行SQL语句 - FMResultSet
使用FMDatabase执行查询后的结果集 -
FMDatabaseQueue
用于在多线程中执行多个查询或更新,它是线程安全的
打开数据库
通过指定SQLite数据库文件路径来创建FMDatabase对象
FMDatabase *db = [FMDatabase databaseWithPath:path];
if (![db open]) {//在和数据库交互 之前,数据库必须是打开的
NSLog(@"数据库打开失败!");
}
- 文件路径有三种情况
具体文件路径
如果不存在会自动创建空字符串@""
会在临时目录创建一个空的数据库
当FMDatabase连接关闭时,数据库文件也被删除nil
会创建一个内存中临时数据库,当FMDatabase连接关闭时,数据库会被销毁
执行更新
在FMDB中,除查询以外的所有操作,都称为“更新”,create、drop、insert、update、delete等
;该方法返回BOOL型, 成功返回 YES, 失败返回 NO
- 格式
使用executeUpdate:方法执行更新
- (BOOL)executeUpdate:(NSString*)sql, ...
- (BOOL)executeUpdateWithFormat:(NSString*)format, ...
- (BOOL)executeUpdate:(NSString*)sql withArgumentsInArray:(NSArray *)arguments
注意
- executeUpdate: 标准的 SQL 语句,参数用?来占位,参数必须是对象类型,不能是int,double,bool等基本数据类型
-executeUpdateWithFormat:使用字符串的格式化构建 SQL 语句,参数用%@、%d等来占位
-executeUpdate:withArgumentsInArray:也可以把对应的参数装到数组里面传进去,SQL语句中的参数用?代替
- 示例
//增
[db executeUpdate:@“INSERT INTO t_student (name, age) VALUES (?,?);”,name,@(age)];
[db executeUpdate:@“INSERT INTO
t_student(name,age) VALUES (?,?);”withArgumentsInArray:@[name,@(age)]];
//删除
int idNum = 101;
[self.db executeUpdate:@“delete from t_student where id = ?;”,@(idNum)];
[self.db executeUpdateWithFormat:@“delete from t_student where name = %@;”,@“apple_name”];
//改
[db executeUpdate:@"UPDATE t_student SET age = ? WHERE name = ?;", @20, @"Jack"];
注意事项
可以插入一个空值,但是这里的空值不是nil,而是NSNull
[db executeUpdate:@"INSERT INTO t_student (name, age, sex) VALUES (?,?,?)",name,@(age),[NSNull null]];
同样在对FMDB数据库对象的属性值判断是否为空时,不能用 nil 来判断,而应该用null来判断
[[resultSet objectForColumnName:@"name"] isKindOfClass:[NSNull Class]];
执行查询
执行查询时,如果成功返回FMResultSet对象,错误返回nil;也可以使用-lastErrorCode和-lastErrorMessage获知错误信息
- 格式
- (FMResultSet *)executeQuery:(NSString*)sql, ...
- (FMResultSet *)executeQueryWithFormat:(NSString*)format, ...
- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray *)arguments
- 示例
// 查询数据
FMResultSet *rs = [db executeQuery:@"SELECT * FROM t_student"];
// 遍历结果集
while ([rs next]) {
NSString *name = [rs stringForColumn:@"name"];
int age = [rs intForColumn:@"age"];
double score = [rs doubleForColumn:@"score"];
}
FMResultSet 提供了很多获取不同类型数据的方法
// 获取下一个记录
- (BOOL)next;
// 获取记录有多少列
- (int)columnCount;
// 通过列名得到列序号,通过列序号得到列名
- (int)columnIndexForName:(NSString *)columnName;
- (NSString *)columnNameForIndex:(int)columnIdx;
// 获取存储的整形值
- (int)intForColumn:(NSString *)columnName;
- (int)intForColumnIndex:(int)columnIdx;
// 获取存储的长整形值
- (long)longForColumn:(NSString *)columnName;
- (long)longForColumnIndex:(int)columnIdx;
// 获取存储的布尔值
- (BOOL)boolForColumn:(NSString *)columnName;
- (BOOL)boolForColumnIndex:(int)columnIdx;
// 获取存储的浮点值
- (double)doubleForColumn:(NSString *)columnName;
- (double)doubleForColumnIndex:(int)columnIdx;
// 获取存储的字符串
- (NSString *)stringForColumn:(NSString *)columnName;
- (NSString *)stringForColumnIndex:(int)columnIdx;
// 获取存储的日期数据
- (NSDate *)dateForColumn:(NSString *)columnName;
- (NSDate *)dateForColumnIndex:(int)columnIdx;
// 获取存储的二进制数据
- (NSData *)dataForColumn:(NSString *)columnName;
- (NSData *)dataForColumnIndex:(int)columnIdx;
// 获取存储的UTF8格式的C语言字符串
- (const unsigned cahr *)UTF8StringForColumnName:(NSString *)columnName;
- (const unsigned cahr *)UTF8StringForColumnIndex:(int)columnIdx;
// 获取存储的对象,只能是NSNumber、NSString、NSData、NSNull
- (id)objectForColumnName:(NSString *)columnName;
- (id)objectForColumnIndex:(int)columnIdx
FMDatabaseQueue
FMDatabase这个类是线程不安全的,如果在多个线程中同时使用一个FMDatabase实例,会造成数据混乱、程序崩溃、报告异常等问题。为了保证线程安全,FMDB提供方便快捷的FMDatabaseQueue类
FMDatabaseQueue 中创建了一个GCD的串行队列,同一个FMDatabaseQueue 共享同一个FMDatabase对象,每一个SQL操作都是同步执行,因此在多线程环境下保障了数据的安全有序的访问
参考1
参考2这个比较有深度
首先使用一个数据库文件地址来初始化 FMDatabaseQueue ,然后就可以将一个闭包(block)传入inDatabase方法中.在闭包中操作数据库,而不是直接参与 FMDatabase 的管理
FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:path];
---------------------------------FMDatabaseQueue.m内部实现----------------------------
- (instancetype)initWithPath:(NSString*)aPath flags:(int)openFlags vfs:(NSString *)vfsName {
_queue = dispatch_queue_create([[NSString stringWithFormat:@"fmdb.%@", self] UTF8String], NULL);
dispatch_queue_set_specific(_queue, kDispatchQueueSpecificKey, (__bridge void *)self, NULL);
}
dispatch_queue_create第二个参数 指定 DISPATCH_QUEUE_SERIAL 或者是NULL的时候,创建的队列是串行队列.所以,FMDatabaseQueue是一个串行队列。然后使用dispatch_queue_set_specific向_queue中设置一个kDispatchQueueSpecificKey标识
使用
[queue inDatabase:^(FMDatabase *db) {
[db executeUpdate:@"INSERT INTO t_student(name) VALUES (?)", @"Jack"];
[db executeUpdate:@"INSERT INTO t_student(name) VALUES (?)", @"Rose"];
[db executeUpdate:@"INSERT INTO t_student(name) VALUES (?)", @"Jim"];
FMResultSet *rs = [db executeQuery:@"select * from t_student"];
while ([rs next]) {
// …
}
}];
注意事项
-(void)inDatabase:(void (^)(FMDatabase *db))block不可以嵌套使用。原理很简单。基于_queue为同步串行队列,如果嵌套使用则会引起死锁
事务
参考1 参考2 详细-参考3
sqlite 本身是支持事务操作的,FMDB 作为对 sqlite 的封装自然也是支持的。
那什么是事务操作?
简单讲一次 executeUpdate (插入数据,本质是 sqlite3_exec)就是一次事务操作,其具体过程是:
开始新事务-->插入数据-->提交事务
我们向数据库中插入多少条数据,这个过程就会执行多少次,显然当插入的数据很多的时候这是非常耗时的。
事务操作的原理就是:所有任务执行完成后再将结果一次性提交到数据库
优点:
- 使用事务操作要比不使用节省大量时间,尤其当数据非常多的时候,避免重复的步骤浪费时间
开启事务的作用
- 使用事务处理就是将所有任务执行完成以后将结果一次性提交到数据库,如果此过程出现异常则会执行回滚操作,这样节省了大量的重复提交环节所浪费的时间。要么全部成功,要么全部失败,防止出现中间状态,以保证数据处理过程中始终处于正确的状态
FMDatabase使用事务的方法:
-(void)insertUserInfoWithDataArr:(NSArray *)dataArr{
if (dataArr.count<1) {
return;
}
FMDatabase *fmdb = self.fmdb;
[fmdb open];//开启数据库
[fmdb beginTransaction];//开启事务
BOOL isRollBack = NO;
@try {
for (int i = 0; i<dataArr.count; i++) {
MsgCenterModel *model = dataArr[i];
NSArray *sqlDataArr = @[@(model.userId),model.name,model.age];
BOOL isSuccess = [fmdb executeUpdate:@"INSERT INTO UserInfoTable (userId,name,age) values (?,?,?)" withArgumentsInArray:sqlDataArr];
if (!isSuccess) {
NSLog(@"update Failure");
}
}
}
@catch (NSException *exception) {
isRollBack = YES;
[fmdb rollback];//事务回滚
}
@finally {
if (!isRollBack) {
[fmdb commit];//提交事务
}
}
[fmdb close];//关闭数据库
}
FMDatabaseQueue使用事务的方法:
//多线程事务
- (void)transactionByQueue {
//开启事务
[self.queue inTransaction:^(FMDatabase *db, BOOL *rollback) {
for (int i = 0; i<500; i++) {
NSNumber *num = @(i+1);
NSString *name = [[NSString alloc] initWithFormat:@"student_%d",i];
NSString *sex = (i%2==0)?@"f":@"m";
NSString *sql = @"insert into mytable(num,name,sex) values(?,?,?);";
BOOL result = [db executeUpdate:sql,num,name,sex];
if (!result) {
//当最后*rollback的值为YES的时候,事务回退,如果最后*rollback为NO,事务提交
*rollback = YES;
return;
}
}
}];
}
源码分析
databaseWithPath
_db赋值为nil,也就是说真正构建_db不是在initWithPath:这个函数中,其实作者是将构建部分代码放到了open函数中
+ (instancetype)databaseWithPath:(NSString *)aPath {
//FMDBReturnAutoReleased是为了让FMDB兼容MRC和ARC
return FMDBReturnAutoreleased([[self alloc] initWithPath:aPath]);
}
- (instancetype)initWithPath:(NSString *)path {
assert(sqlite3_threadsafe()); // whoa there big boy- gotta make sure sqlite it happy with what we're going to do.
self = [super init];
if (self) {
_databasePath = [path copy];
_openResultSets = [[NSMutableSet alloc] init];
_db = nil;
_logsErrors = YES;
_crashOnErrors = NO;
_maxBusyRetryTimeInterval = 2;
_isOpen = NO;
}
return self;
}
open
open函数才是真正获取到数据库,其本质上也就是调用SQLite的C/C++接口 – sqlite3_open()
- (BOOL)open {
if (_isOpen) {
return YES;
}
// if we previously tried to open and it failed, make sure to close it before we try again
if (_db) {
[self close];
}
// now open database
int err = sqlite3_open([self sqlitePath], (sqlite3**)&_db );
if(err != SQLITE_OK) {
NSLog(@"error opening!: %d", err);
return NO;
}
if (_maxBusyRetryTimeInterval > 0.0) {
// set the handler
[self setMaxBusyRetryTimeInterval:_maxBusyRetryTimeInterval];
}
_isOpen = YES;
return YES;
}
executeUpdate的内部实现
- (BOOL)executeUpdate:(NSString*)sql withArgumentsInArray:(NSArray *)arguments {
return [self executeUpdate:sql error:nil withArgumentsInArray:arguments orDictionary:nil orVAList:nil];
}
- (BOOL)executeUpdate:(NSString*)sql values:(NSArray *)values error:(NSError * __autoreleasing *)error {
return [self executeUpdate:sql error:error withArgumentsInArray:values orDictionary:nil orVAList:nil];
}
- (BOOL)executeUpdate:(NSString*)sql withParameterDictionary:(NSDictionary *)arguments {
return [self executeUpdate:sql error:nil withArgumentsInArray:nil orDictionary:arguments orVAList:nil];
}
- (BOOL)executeUpdate:(NSString*)sql withVAList:(va_list)args {
return [self executeUpdate:sql error:nil withArgumentsInArray:nil orDictionary:nil orVAList:args];
}
- (BOOL)executeUpdate:(NSString*)sql
error:(NSError**)outErr
withArgumentsInArray:(NSArray*)arrayArgs
orDictionary:(NSDictionary *)dictionaryArgs
orVAList:(va_list)args {
if (dictionaryArgs) {
for (NSString *dictionaryKey in [dictionaryArgs allKeys]) {
// Prefix the key with a colon.
NSString *parameterName = [[NSString alloc] initWithFormat:@":%@", dictionaryKey];
if (_traceExecution) {
NSLog(@"%@ = %@", parameterName, [dictionaryArgs objectForKey:dictionaryKey]);
}
// Get the index for the parameter name.
int namedIdx = sqlite3_bind_parameter_index(pStmt, [parameterName UTF8String]);
FMDBRelease(parameterName);
if (namedIdx > 0) {
// Standard binding from here.
[self bindObject:[dictionaryArgs objectForKey:dictionaryKey] toColumn:namedIdx inStatement:pStmt];
// increment the binding count, so our check below works out
idx++;
}
else {
NSString *message = [NSString stringWithFormat:@"Could not find index for %@", dictionaryKey];
if (_logsErrors) {
NSLog(@"%@", message);
}
if (outErr) {
*outErr = [self errorWithMessage:message];
}
}
}
}
}
- (void)bindObject:(id)obj toColumn:(int)idx inStatement:(sqlite3_stmt*)pStmt {
if ((!obj) || ((NSNull *)obj == [NSNull null])) {
sqlite3_bind_null(pStmt, idx);
}
// FIXME - someday check the return codes on these binds.
else if ([obj isKindOfClass:[NSData class]]) {
const void *bytes = [obj bytes];
if (!bytes) {
// it's an empty NSData object, aka [NSData data].
// Don't pass a NULL pointer, or sqlite will bind a SQL null instead of a blob.
bytes = "";
}
sqlite3_bind_blob(pStmt, idx, bytes, (int)[obj length], SQLITE_STATIC);
}
else if ([obj isKindOfClass:[NSDate class]]) {
if (self.hasDateFormatter)
sqlite3_bind_text(pStmt, idx, [[self stringFromDate:obj] UTF8String], -1, SQLITE_STATIC);
else
sqlite3_bind_double(pStmt, idx, [obj timeIntervalSince1970]);
}
else if ([obj isKindOfClass:[NSNumber class]]) {
if (strcmp([obj objCType], @encode(char)) == 0) {
sqlite3_bind_int(pStmt, idx, [obj charValue]);
}
else if (strcmp([obj objCType], @encode(unsigned char)) == 0) {
sqlite3_bind_int(pStmt, idx, [obj unsignedCharValue]);
}
else if (strcmp([obj objCType], @encode(short)) == 0) {
sqlite3_bind_int(pStmt, idx, [obj shortValue]);
}
else if (strcmp([obj objCType], @encode(unsigned short)) == 0) {
sqlite3_bind_int(pStmt, idx, [obj unsignedShortValue]);
}
else if (strcmp([obj objCType], @encode(int)) == 0) {
sqlite3_bind_int(pStmt, idx, [obj intValue]);
}
else if (strcmp([obj objCType], @encode(unsigned int)) == 0) {
sqlite3_bind_int64(pStmt, idx, (long long)[obj unsignedIntValue]);
}
else if (strcmp([obj objCType], @encode(long)) == 0) {
sqlite3_bind_int64(pStmt, idx, [obj longValue]);
}
else if (strcmp([obj objCType], @encode(unsigned long)) == 0) {
sqlite3_bind_int64(pStmt, idx, (long long)[obj unsignedLongValue]);
}
else if (strcmp([obj objCType], @encode(long long)) == 0) {
sqlite3_bind_int64(pStmt, idx, [obj longLongValue]);
}
else if (strcmp([obj objCType], @encode(unsigned long long)) == 0) {
sqlite3_bind_int64(pStmt, idx, (long long)[obj unsignedLongLongValue]);
}
else if (strcmp([obj objCType], @encode(float)) == 0) {
sqlite3_bind_double(pStmt, idx, [obj floatValue]);
}
else if (strcmp([obj objCType], @encode(double)) == 0) {
sqlite3_bind_double(pStmt, idx, [obj doubleValue]);
}
else if (strcmp([obj objCType], @encode(BOOL)) == 0) {
sqlite3_bind_int(pStmt, idx, ([obj boolValue] ? 1 : 0));
}
else {
sqlite3_bind_text(pStmt, idx, [[obj description] UTF8String], -1, SQLITE_STATIC);
}
}
else {
sqlite3_bind_text(pStmt, idx, [[obj description] UTF8String], -1, SQLITE_STATIC);
}
}
参考看一下
iOS FMDB多线程之FMDatabaseQueue
FMDB无法更新二进制的问题
FMDB详解
数据库操作工具SQLiteManager