iOS中原生的SQLite API在使用上相当不友好,在使用时,非常不便。于是,就出现了一系列将SQLite API进行封装的库,例如FMDB,PlausibleDatabase等.
安装
使用CocoaPods来安装FMDB
CocoaPods是Swift和Objective-C Cocoa项目的依赖管理器。它拥有4万多个库,用于超过280万个应用程序。CocoaPods可以帮助您优雅地扩展您的项目。
如果你没有创建CocoaPods工程,在工程目录下使用
$ pod init
来初始化项目,在生成的Podfile文件中添加FMDB:
target 'MyApp' do
pod 'FMDB'
# pod 'FMDB/FTS' # FMDB with FTS
# pod 'FMDB/standalone' # FMDB with latest SQLite amalgamation source
# pod 'FMDB/standalone/FTS' # FMDB with latest SQLite amalgamation source and FTS
# pod 'FMDB/SQLCipher' # FMDB with SQLCipher
end
然后执行
$ pod install
然后使用新生成的*.xcworkspace工程,而不是*.xcodeproj。
用法
在FMDB中有三个主要的类;
-
FMDataase-表示一个SQLite数据库.用于执行SQLite语句 -
FMResultSet-表示数据库的查询结果 -
FMDatabaseQueue-在多个线程来执行查询和更新时会使用这个类
创建数据库
需要制定一个文件路径(path)来创建一个给予SQLite的FMDatabase,有以下三种方式可以指定文件路径:
- 指定一个文件路径,如果文件不存在,则自动创建
- 指定路径为一个空字符串(
@""),使用该方式会创建一个临时数据库文件,当FMDatabase被关闭的时候,临时数据库文件自动被删除 -
NULL会在内存中创建一个临时数据库,在FMDatabase被关闭的时候自动删除
NSString *path = [NSTemporaryDirectory() stringByAppendingPathComponent:@"tmp.db"];
FMDatabase *db = [FMDatabase databaseWithPath:path];
打开数据库
在与数据库进行交互之前,我们需要打开数据库,如果没有足够的资源或权限打开和/或创建数据库,则打开失败,当打开失败的时候,把db置为nil.
if(![db open]){
db = nil;
return ;
}
执行更新
除了SELECT操作外,SQLite的所有命令都使用executeUpdate消息来执行.包括CREATE, UPDATE, INSERT, ALTER, COMMIT, BEGIN, DETACH, DELETE, DROP, END, EXPLAIN, VACUUM,REPLACE等,只要你不是执行SELECT语句,就是更新数据库操作.
BOOL succ = [db executeUpdate:sql]; //sql是SQLite命令语句字符串
执行更新会返回一个布尔值。返回值YES意味着更新成功执行,返回值NO意味着遇到了一些错误。可以调用lastErrorMessage和lastErrorCode方法来检索更多的信息。
执行查询
一个SELECT语句是一个查询,并通过其中一种-executeQuery...方法执行。
如果成功执行查询则返回一个FMResultSet对象,失败返回nil。可以使用lastErrorMessage和lastErrorCode方法来确定查询失败的原因。
为了迭代你的查询的结果,可以用一个while()循环,在循环中使用next方法来不断取出结果.
FMResultSet *s = [db executeQuery:@"SELECT * FROM myTable"];
while ([s next]) {
//retrieve values for each record
}
即使你只希望查询一个值也需要使用next方法
FMResultSet * s = [db executeQuery:@“ SELECT COUNT(*)FROM myTable ” ];
if([s next ]){
int totalCount = [s intForColumnIndex:0 ];
}
FMResultSet有许多方法可以取出数据:
intForColumn:longForColumn:longLongIntForColumn:boolForColumn:doubleForColumn:stringForColumn:dateForColumn:dataForColumn:dataNoCopyForColumn:UTF8StringForColumn:-
objectForColumn:
例如,取出一个NSString的name数据
NSString *name = [s stringForColumn:@"name"];
每个方法还有一个对应的{type}ForColumnIndex:变量,用来根据结果中列的位置来检索数据,而不是列名。
通常情况下没有必要手动关闭FMResultSet对象,因为出现这种情况可以当结果集被释放,或者数据库被关闭自动关闭.
关闭数据库
当你完成对数据库的查询和更新时,你应该close关闭对FMDatabase连接,这样SQLite将释放资源。
[db close];
事务(Transactions)
FMDatabase可以使用begin和commit来包裹一个事务
多个语句的执行
FMDatabase可以一次执行多个语句和用block进行操作
NSString *sql = @"create table bulktest1 (id integer primary key autoincrement, x text);"
"create table bulktest2 (id integer primary key autoincrement, y text);"
"create table bulktest3 (id integer primary key autoincrement, z text);"
"insert into bulktest1 (x) values ('XXX');"
"insert into bulktest2 (y) values ('YYY');"
"insert into bulktest3 (z) values ('ZZZ');";
success = [db executeStatements:sql];
sql = @"select count(*) as count from bulktest1;"
"select count(*) as count from bulktest2;"
"select count(*) as count from bulktest3;";
success = [self.db executeStatements:sql withResultBlock:^int(NSDictionary *dictionary) {
NSInteger count = [dictionary[@"count"] integerValue];
XCTAssertEqual(count, 1, @"expected one record for dictionary %@", dictionary);
return 0;
}];
数据
当执行在FMDB中执行SQL语句,为了数据库的安全,需要使用数据绑定语法.
INSERT INTO myTable VLUEA (?,?,?,?)
?字符被SQLite识别为要插入的值的占位符。
NSInteger identifier = 42;
NSString *name = @"Liam O'Flaherty ";
NSDate *date = [NSDate date];
NSString *comment = nil;
BOOL success = [db executeUpdate:@"INSERT INTO authors (identifier, name, date, comment) VALUES (?, ?, ?, ?)", @(identifier), name, date, comment ?: [NSNull null]];
if (!success) {
NSLog(@"error = %@", [db lastErrorMessage]);
}
注意:基本数据类型(如
NSInteger),应该作为一个NSNumber对象,通过一个语法糖@()来完成NSInteger到NSNumber的转变
同样的SQL中的NULL值应该使用[NSNull null]来代替,如上面实例代码中的comment,使用comment ?:[NSNull null]来代替nil.
或者,也可以使用命名参数语法,当传入一个字典类型的时候尤其适用:
INSERT INTO authors (identifier, name, date, comment) VALUES (:identifier, :name, :date, :comment)
参数必须以:开头
NSDictionary *arguments = @{@"identifier": @(identifier), @"name": name, @"date": date, @"comment": comment ?: [NSNull null]};
BOOL success = [db executeUpdate:@"INSERT INTO authors (identifier, name, date, comment) VALUES (:identifier, :name, :date, :comment)" withParameterDictionary:arguments];
if (!success) {
NSLog(@"error = %@", [db lastErrorMessage]);
}
关键是不应该使用NSString方法stringWithFormat手动将值插入到SQL语句本身。
使用FMDatabaseQueue和线程安全
不要在多个线程中同时使用一个FMDatabase对象。当然每个线程创建一个FMDatabase对象总是可以的.只是不要在线程之间共享一个实例,而且绝对不能同时在多个线程之间共享。这样可能导致脏数据或者写入异常.
所以不要实例化一个FMDatabase对象并在多个线程中使用它。
正确的做法是,实例化一个FMDatabaseQueue对象,并在不同的线程中使用这个对象,FMDatabaseQueue会在不同的线程之间同步和协调.
首先,创建一个FMDatabaseQueue队列:
FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:aPath];
然后使用:
[queue inDatabase:^(FMDatabase *db) {
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", @1];
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", @2];
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", @3];
FMResultSet *rs = [db executeQuery:@"select * from foo"];
while ([rs next]) {
…
}
}];
一种简单的事务的处理方法:
[queue inTransaction:^(FMDatabase *db, BOOL *rollback) {
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", @1];
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", @2];
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", @3];
if (whoopsSomethingWrongHappened) {
*rollback = YES;
return;
}
// etc ...
}];
注意:对FMDatabaseQueue方法的调用是阻塞的,所以你传递的^block,不会在另一个线程上运行.