[iOS学习笔记]·FMDB:第三方本地数据库处理框架(官方文档翻译篇)

目前,虽然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 。

[iOS学习笔记]·FMDB:第三方本地数据库处理框架(官方文档翻译篇)_第1张图片

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,NSDictionaryva_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中查找

英文原文出处:

https://github.com/ccgus/fmdb

你可能感兴趣的:([iOS学习笔记]·FMDB:第三方本地数据库处理框架(官方文档翻译篇))