目前,虽然SQLite也为iOS提供了数据库操作方法,但更多的时候,一般用FMDB,正如主流APP(如QQ和微信)会用到。这里介绍一个查询主流APP主要框架的网站:AppSight 。
这篇文章,主要挑选FMDB官方文档中的使用方法部分进行了翻译。关于Pod以及Carthage安装第三方库的部分,可以参考笔者相关文章(Pod,Carthage)。FMDB官方源码地址传送门:https://github.com/ccgus/fmdb 。
FMDB是SQLite的Objective-C包装器:http://sqlite.org/ 。由于FMDB是建立在SQLite之上的,所以您至少阅读相关页面一次:http://www.sqlite.org/docs.html,http://www.sqlite.org/faq.html 。
只看官方文档是不够的,看完后要多融合介绍的这些方法进行练习。关于FMDB的使用示例代码和DEMO可以参考笔者的另一篇文章http://www.jianshu.com/p/18cd2416ccc3 。
1.使用方法(Usage)
FMDB有三个主要的类:
- FMDatabase:表示一个单独的SQLite数据库。 用来执行SQLite的命令。
- FMResultSet:表示FMDatabase执行查询后结果集
- FMDatabaseQueue:如果你想在多线程中执行多个查询或更新,你应该使用该类。这是线程安全的。
1.1 数据库创建(Database Creation)
创建FMDatabase对象时参数为SQLite数据库文件路径。该路径可以是以下三种之一:
- 1.文件路径。该文件路径无需真实存,如果不存在会自动创建。
- 2.空字符串(@"")。表示会在临时目录创建一个空的数据库,当FMDatabase 链接关闭时,文件也被删除。
- 3.NULL. 将创建一个内在数据库。同样的,当FMDatabase连接关闭时,数据会被销毁。
NSString *path = [NSTemporaryDirectory() stringByAppendingPathComponent:@"tmp.db"];
FMDatabase *db = [FMDatabase databaseWithPath:path];
(如需对临时数据库或内在数据库进行一步了解,请继续阅读:http://www.sqlite.org/inmemorydb.html)
1.2 打开数据库(Opening)
在和数据库交互之前,数据库必须是打开的。如果资源或权限不足无法打开或创建数据库,都会导致打开失败。
if (![db open]) {
[db release]; //ARC无需此行
return;
}
1.3 执行更新(Executing Updates)
一切不是SELECT
命令的命令都视为更新。这包括CREATE, UPDATE, INSERT,ALTER,COMMIT, BEGIN, DETACH, DELETE, DROP, END, EXPLAIN, VACUUM, and REPLACE
(等)。
简单来说,只要不是以SELECT
开头的命令都是UPDATE
命令。
执行更新返回一个BOOL
值。YES
表示执行成功,否则表示有那些错误 。你可以调用-lastErrorMessage
和-lastErrorCode
方法来得到更多信息。
执行更新的方法是以-executeUpdate:
开头的。
1.4 执行查询(Executing Queries)
SELECT
命令就是查询,执行查询的方法是以-excuteQuery:
开头的。
执行查询时,如果成功返回FMResultSet对象,错误返回nil. 与执行更新相当,支持使用 NSError**参数。同时,你也可以使用-lastErrorCode
和-lastErrorMessage
获知错误信息。
为了遍历查询结果,你可以使用while循环。你还需要知道怎么跳到下一个记录。使用FMDB,很简单实现,就像这样:
FMResultSet *s = [db executeQuery:@"SELECT * FROM myTable"];
while ([s next]) {
//retrieve values for each record
}
在你访问查询返回值之前,你必须一直调用-[FMResultSet next]
,即使你只想要一个记录:
FMResultSet *s = [db executeQuery:@"SELECT COUNT(*) FROM myTable"];
if ([s next]) {
int totalCount = [s intForColumnIndex:0];
}
FMResultSet
提供了很多方法来获得所需的格式的值:
- intForColumn:
- longForColumn:
- longLongIntForColumn:
- boolForColumn:
- doubleForColumn:
- stringForColumn:
- dataForColumn:
- dataNoCopyForColumn:
- UTF8StringForColumnIndex:
- objectForColumn:
这些方法也都包括 {type}ForColumnIndex
的这样子的方法,参数是查询结果集的列的索引位置。
你无需调用 [FMResultSet close]
来关闭结果集, 当新的结果集产生,或者其数据库关闭时,会自动关闭。
1.5 关闭数据库(Closing)
当使用完数据库,你应该-close
来关闭数据库连接来释放SQLite使用的资源。
[db close];
1.6 事务(Transactions)
FMDatabase是支持事务的。
1.7 多重语句和批次信息(Multiple Statements and Batch Stuff)
您可以使用FMDatabaseexecuteStatements:withResultBlock:
在字符串中执行多个语句:
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;
}];
1.8 数据格式化(Data Sanitization)
利用一个SQL语句为FMDB插入数据前,你不要尝试SQL审查(sanitize)任何值。相反的,你应该使用标准的SQLite数据绑定语法。
INSERT INTO myTable VALUES (?, ?, ?, ?)
该?
字符由SQLite识别为要插入的值的占位符。这些执行方法全部接受数量可变的参数(或这些参数的一个代表,例如NSArray,NSDictionary
或va_list
)。
并且,在Objective-C中将该SQL的占位符?
一起使用:
NSInteger identifier = 42;
NSString *name = @"Liam O'Flaherty (\"the famous Irish author\")";
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变量identifier,应该是一个NSNumber对象,通过使用@如上所示的语法实现。或者您也可以使用[NSNumber numberWithInt:identifier]语法。
同样,NULL应该插入SQL 值[NSNull null]。例如,在案件的comment,这可能是nil(而且是在这个例子中),你可以使用comment ?: [NSNull null]语法,如果将插入字符串comment不是nil,而是将插入[NSNull null]如果它是nil。
在Swift中,您将使用它executeUpdate(values:)
,这不仅仅是一个简洁的Swift语法,而且也是throws错误处理正确的错误:
do {
let identifier = 42
let name = "Liam O'Flaherty (\"the famous Irish author\")"
let date = Date()
let comment: String? = nil
try db.executeUpdate("INSERT INTO authors (identifier, name, date, comment) VALUES (?, ?, ?, ?)", values: [identifier, name, date, comment ?? NSNull()])
} catch {
print("error = \(error)")
}
注意:在Swift中,您不必像Objective-C那样包装基本的数字类型。但是如果要插入一个可选的字符串,你可能会使用comment ?? NSNull()语法(即,如果是nil,使用NSNull,否则使用字符串)。
或者,您可以使用命名参数语法:
INSERT INTO authors (identifier, name, date, comment) VALUES (:identifier, :name, :date, :comment)
参数名必须以冒名开头。SQLite本身支持其他字符,但Dictionary key的内部实现是冒号开头,所以注意你的NSDictionary key不要包含冒号。
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语句本身。一个Swift字符串插入也不应该将值插入到SQL中。使用?占位符将值插入到数据库中(或WHERE在SELECT语句中的子句中使用)。
1.9 补充:老版本的README
提供给-executeUpdate:
方法的参数都必须是对象。就像以下的代码就无法工作,且会产生崩溃。
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", 42];
正确有做法是把数字打包成 NSNumber
对象
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:42]];
或者,你可以使用 -execute*WithFormat:
,这是NSString风格的参数
[db executeUpdateWithFormat:@"INSERT INTO myTable VALUES (%d)", 42];
-execute*WithFormat:
的方法的内部实现会帮你封装数据, 以下这些修饰符都可以使用: %@, %c, %s, %d, %D,%i, %u, %U, %hi, %hu, %qi, %qu, %f, %g, %ld, %lu, %lld, and %llu
。 除此之外的修饰符可能导致无法预知的结果。 一些情况下,你如果要在SQL语句中使用 %
字符,你应该使用%%
。
2. 使用FMDatabaseQueue 及线程安全 (Using FMDatabaseQueue and Thread Safety)
在多个线程中同时使用一个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 ]){
...
}
}]
在transaction中封装事务的简单方法:
[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 ...
}];
Swift相应的版本为:
queue.inTransaction { db, rollback in
do {
try db.executeUpdate("INSERT INTO myTable VALUES (?)", values: [1])
try db.executeUpdate("INSERT INTO myTable VALUES (?)", values: [2])
try db.executeUpdate("INSERT INTO myTable VALUES (?)", values: [3])
if whoopsSomethingWrongHappened {
rollback.pointee = true
return
}
// etc ...
} catch {
rollback.pointee = true
print(error)
}
}
(注意,从Swift 3开始使用pointee,但在Swift 2.3中,使用memory而不是pointee。)
FMDatabaseQueue将运行(序列化队列上的)块(因此是类名)。所以如果你同时从多个线程调用FMDatabaseQueue的方法,它们将按照它们被接收的顺序执行。这样查询和更新将不会对对方的脚趾,每一个都很开心。
注意:对FMDatabaseQueue方法的调用是阻塞的。所以即使你正在传递块,它们也不会在另一个线程上运行。
3. 基于块制作定制的sqlite函数(Making custom sqlite functions, based on blocks)
你可以这样做!例如,-makeFunctionNamed:
在main.m中查找