FMDB主要有以下幾個類:
(1)FMDatabase:代表一個單獨的SQLite操作實例,數(shù)據(jù)庫通過它增刪改查操作;
(2)FMResultSet:代表查詢后的結(jié)果集;
(3)FMDatabaseQueue:代表串行隊列,對多線程操作提供了支持;
(4)FMDatabaseAdditions:本類用于擴展FMDatabase,用于查找表是否存在,版本號等功能;
(5)FMDatabasePool:此方式官方是不推薦使用,代表是任務池,也是對多線程提供了支持。
.線程安全
在多個線程中同時使用一個FMDatabase實例是不明智的。不要讓多個線程分享同一個FMDatabase實例,它無法在多個線程中同時使用。 如果在多個線程中同時使用一個FMDatabase實例,會造成數(shù)據(jù)混亂等問題。所以,請使用 FMDatabaseQueue,它是線程安全的。以下是使用方法:
1.創(chuàng)建
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES).firstObject;
NSString *filePath = [path stringByAppendingPathComponent:@"FMDB.db"];
FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:path];
databaseQueueWithPath:方法里面創(chuàng)建了一個串行隊列
2.操作數(shù)據(jù)
[queue inDatabase:^(FMDatabase*db) {
//FMDatabase數(shù)據(jù)庫操作
if (![db open]) {
NSLog(@"打開數(shù)據(jù)庫失敗");
return ;
}
//創(chuàng)建表(FMDB中只有update和query操作,除了查詢其他都是update操作)}];
[db executeUpdate:@"create table if not exists user(name text,gender text,age integer) "];
//插入數(shù)據(jù)
BOOL inser = [db executeUpdate:@"insert into user values(?,?,?)",_nameTextField.text,_sexTextField.text,_ageTextField.text];
[db close];
}
3.事務操作:
我們可以這樣理解數(shù)據(jù)庫事物:對數(shù)據(jù)庫所做的一系列修改,在修改過程中,暫時不寫入數(shù)據(jù)庫,而是緩存起來,用戶在自己的終端可以預覽變化,直到全部修改完成,并經(jīng)過檢查確認無誤后,一次性提交并寫入數(shù)據(jù)庫,在提交之前,必要的話所做的修改都可以取消。提交之后,就不能撤銷,提交成功后其他用戶才可以通過查詢?yōu)g覽數(shù)據(jù)的變化。
簡單的說也就是,事務可以讓多個表的數(shù)據(jù)同時插入,一旦有一個表操作失敗,那么其他表也都會失敗。當然這種說法是為了理解,不是嚴謹?shù)摹?br>
那么對一個表大量插入數(shù)據(jù)時也可以用事務。比如sqlite3。
數(shù)據(jù)庫 中 insert into 語句等操作是比較耗時的,假如我們一次性插入幾百幾千條數(shù)據(jù)就會造成主線程阻塞,以至于ui界面卡住。那么這時候我們就要開啟一個事物來進行操作。
原因就是它以文件的形式存在磁盤中,每次訪問時都要打開一次文件,如果對數(shù)據(jù)庫進行大量的操作,就很慢??墒侨绻覀冇檬聞盏男问教峤?,開始事務后,進行的大量操作語句都保存在內(nèi)存中,當提交commit時才全部寫入數(shù)據(jù)庫,此時,數(shù)據(jù)庫文件也只用打開一次。如果操作錯誤,還可以回滾事務。
// 單線程事務
//事務
-(void)transaction
{
BOOL isSuccess=[_dataBase open];
if (!isSuccess) {
HSLog(@"打開數(shù)據(jù)庫失敗");
}
[_dataBase beginTransaction];
BOOL isRollBack = NO;
@try {
for (int i = 0; i<500; i++) {
NSString *nId = [NSString stringWithFormat:@"%d",i];
NSString *strName = [[NSString alloc] initWithFormat:@"student_%d",i];
NSString *sql = @"INSERT INTO Student (id,student_name) VALUES (?,?)";
BOOL a = [_dataBase executeUpdate:sql,nId,strName];
if (!a) {
NSLog(@"插入失敗1");
}
}
}
@catch (NSException *exception) {
isRollBack = YES;
[_dataBase rollback];
}
@finally {
if (!isRollBack) {
[_dataBase commit];
}
}
[_dataBase close];
}
// 多線程事務
//事務
-(void)transaction
{
[_dataBase inTransaction:^(FMDatabase *db, BOOL *rollback) {
for (int i = 0; i<500; i++) {
NSString *nId = [NSString stringWithFormat:@"%d",i];
NSString *strName = [[NSString alloc] initWithFormat:@"student_%d",i];
NSString *sql = @"INSERT INTO Student (id,student_name) VALUES (?,?)";
BOOL a = [db executeUpdate:sql,nId,strName];
if (!a) {
*rollback = YES;
return;
}
}
}];
}
+ [FMDatabase databaseWithPath:]
// 核心其實還是調(diào)用了+[FMDataBase initWithPath:]函數(shù),下面會詳解
+ (instancetype)databaseWithPath:(NSString*)aPath {
// FMDBReturnAutoReleased是為了讓FMDB兼容MRC和ARC,具體細節(jié)看下其宏定義就明白了
return FMDBReturnAutoreleased([[self alloc] initWithPath:aPath]);
}
/** 初始化一個FMDataBase對象
根據(jù)path(aPath)來創(chuàng)建一個SQLite數(shù)據(jù)庫。對應的aPath參數(shù)有三種情形:
1. 數(shù)據(jù)庫文件路徑:不為空字符串,不為nil。如果該文件路徑不存在,那么SQLite會給你新建一個;
2. 空字符串@"":將在外存臨時給你創(chuàng)建一個空的數(shù)據(jù)庫,并且如果該數(shù)據(jù)庫連接釋放,那么對應數(shù)據(jù)庫會自動刪除;
3. nil:會在內(nèi)存中創(chuàng)建數(shù)據(jù)庫,隨著該數(shù)據(jù)庫連接的釋放,也會釋放該數(shù)據(jù)庫;
- (instancetype)initWithPath:(NSString*)aPath {
// SQLite支持三種線程模式,sqlite3_threadsafe()函數(shù)的返回值可以確定編譯時指定的線程模式。
// 三種模式分別為1.單線程模式 2.多線程模式 3.串行模式 其中對于單線程模式,sqlite3_threadsafe()返回false
// 對于另外兩個模式,則返回true。這是因為單線程模式下沒有進行互斥(mutex),所以多線程下是不安全的
assert(sqlite3_threadsafe());
self = [super init];
// 很多屬性后面再提。不過這里值得注意的是_db居然賦值為nil,也就是說真正構(gòu)建_db不是在initWithPath:這個函數(shù)中,這里透露下,其實作者是將構(gòu)建部分代碼放到了open函數(shù)中if (self) {
_databasePath = [aPath copy];
_openResultSets = [[NSMutableSet alloc] init];
_db = nil;
_logsErrors = YES;
_crashOnErrors = NO;
_maxBusyRetryTimeInterval = ;
}
return self;
}
+ [FMDatabase open]
上面提到過+ [FMDatabase databaseWithPath:]和- [FMDatabase initWithPath:]本質(zhì)上只是給了數(shù)據(jù)庫一個名字,并沒有真實創(chuàng)建或者獲取數(shù)據(jù)庫。這里的open函數(shù)才是真正獲取到數(shù)據(jù)庫,其本質(zhì)上也就是調(diào)用SQLite的C/C++接口 – sqlite3_open()。
sqlite3_open(const char *filename, sqlite3 **ppDb)
該例子打開一個指向 SQLite 數(shù)據(jù)庫文件的連接,返回一個用于其他 SQLite 程序的數(shù)據(jù)庫連接對象。
如果 filename 參數(shù)是 NULL 或 ':memory:',那么 sqlite3_open() 將會在 RAM 中創(chuàng)建一個內(nèi)存數(shù)據(jù)庫,這只會在 session 的有效時間內(nèi)持續(xù)。
如果文件名 filename 不為 NULL,那么 sqlite3_open() 將使用這個參數(shù)值嘗試打開數(shù)據(jù)庫文件。如果該名稱的文件不存在,sqlite3_open() 將創(chuàng)建一個新的命名為該名稱的數(shù)據(jù)庫文件并打開。
- (BOOL)open {
if (_db) {
return YES;
}
int err = sqlite3_open([self sqlitePath], (sqlite3**)&_db );
if(err != SQLITE_OK) {
NSLog(@"error opening!: %d", err);
return NO;
}
// 若_maxBusyRetryTimeInterval大于0,那么就調(diào)用setMaxBusyRetryTimeInterval:函數(shù)
// setMaxBusyRetryTimeInterval:函數(shù)主要是調(diào)用sqlite3_busy_handler來處理其他線程已經(jīng)在操作數(shù)據(jù)庫的情況,默認_maxBusyRetryTimeInterval為2。
// 具體該參數(shù)有什么用,下面在FMDBDatabaseBusyHandler函數(shù)中會詳解。
if (_maxBusyRetryTimeInterval > 0.0) {
[self setMaxBusyRetryTimeInterval:_maxBusyRetryTimeInterval];
}
return YES;
}
- (void)setMaxBusyRetryTimeInterval:(NSTimeInterval)timeout {
_maxBusyRetryTimeInterval = timeout;
if (!_db) {
return;
}
// 處理的handler設(shè)置為FMDBDatabaseBusyHandler這個函數(shù)
if (timeout > ) {
sqlite3_busy_handler(_db, &FMDBDatabaseBusyHandler, (__bridge void *)(self));
}
else {
// 不使用任何busy handler處理
sqlite3_busy_handler(_db, nil, nil);
}
}
這里需要提一下sqlite3_busy_handler這個函數(shù):
int sqlite3_busy_handler(sqlite3, int()(void,int), void);
第一個參數(shù)是告知哪個數(shù)據(jù)庫需要設(shè)置busy handler。
第二個參數(shù)是其實就是回調(diào)函數(shù)(busy handler)了,當你調(diào)用該回調(diào)函數(shù)時,需傳遞給它的一個void*的參數(shù)的拷貝,也即sqlite3_busy_handler的第三個參數(shù);另一個需要傳給回調(diào)函數(shù)的int參數(shù)是表示這次鎖事件,該回調(diào)函數(shù)被調(diào)用的次數(shù)。如果回調(diào)函數(shù)返回0時,將不再嘗試再次訪問數(shù)據(jù)庫而返回SQLITE_BUSY或者SQLITE_IOERR_BLOCKED。如果回調(diào)函數(shù)返回非0, 將會不斷嘗試操作數(shù)據(jù)庫。
總結(jié):程序運行過程中,如果有其他進程或者線程在讀寫數(shù)據(jù)庫,那么sqlite3_busy_handler會不斷調(diào)用回調(diào)函數(shù),直到其他進程或者線程釋放鎖。獲得鎖之后,不會再調(diào)用回調(diào)函數(shù),從而向下執(zhí)行,進行數(shù)據(jù)庫操作。該函數(shù)是在獲取不到鎖的時候,以執(zhí)行回調(diào)函數(shù)的次數(shù)來進行延遲,等待其他進程或者線程操作數(shù)據(jù)庫結(jié)束,從而獲得鎖操作數(shù)據(jù)庫。
// 注意:appledoc(生成文檔的軟件)中,對于有具體實現(xiàn)的C函數(shù),比如下面這個函數(shù),
// 是有bug的。所以你在生成文檔時,忽略.m文件。
// 該函數(shù)就是簡單調(diào)用sqlite3_sleep來掛起進程
static int FMDBDatabaseBusyHandler(void *f, int count) {
FMDatabase *self = (__bridge FMDatabase*)f;
// 如果count為0,表示的第一次執(zhí)行回調(diào)函數(shù)
// 初始化self->_startBusyRetryTime,供后面計算delta使用
if (count == ) {
self->_startBusyRetryTime = [NSDate timeIntervalSinceReferenceDate];
return ;
}
// 使用delta變量控制執(zhí)行回調(diào)函數(shù)的次數(shù),每次掛起50~100ms
// 所以maxBusyRetryTimeInterval的作用就在這體現(xiàn)出來了
// 當掛起的時長大于maxBusyRetryTimeInterval,就返回0,并停止執(zhí)行該回調(diào)函數(shù)了
NSTimeInterval delta = [NSDate timeIntervalSinceReferenceDate] - (self->_startBusyRetryTime);
if (delta < [self maxBusyRetryTimeInterval]) {
// 使用sqlite3_sleep每次當前線程掛起50~100ms
int requestedSleepInMillseconds = (int) arc4random_uniform() + ;
int actualSleepInMilliseconds = sqlite3_sleep(requestedSleepInMillseconds);
// 如果實際掛起的時長與想要掛起的時長不一致,可能是因為構(gòu)建SQLite時沒將HAVE_USLEEP置為1
if (actualSleepInMilliseconds != requestedSleepInMillseconds) {
NSLog(@"WARNING: Requested sleep of %i milliseconds, but SQLite returned %i. Maybe SQLite wasn't built with HAVE_USLEEP=1?", requestedSleepInMillseconds, actualSleepInMilliseconds);
}
return ;
}
return ;
}
[FMDatabase executeQuery:withArgumentsInArray:orDictionary:orVAList:](重點)
[FMDatabase executeQuery:]等等類似的函數(shù),最終都是對- [FMDatabase executeQuery:withArgumentsInArray:orDictionary:orVAList:]的簡單封裝。該函數(shù)比較關(guān)鍵,主要是針對查詢的sql語句。
- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray*)arrayArgs orDictionary:(NSDictionary *)dictionaryArgs orVAList:(va_list)args {
// 判斷當前是否存在數(shù)據(jù)庫以供操作
if (![self databaseExists]) {
return 0x00;
}
// 如果當前線程已經(jīng)在使用數(shù)據(jù)庫了,那就輸出正在使用的警告
if (_isExecutingStatement) {
[self warnInUse];
return 0x00;
}
_isExecutingStatement = YES;
int rc = 0x00;
sqlite3_stmt *pStmt = 0x00; // sqlite的prepared語句類型
FMStatement *statement = 0x00; // 對sqlite3_stmt的簡單封裝,在實際應用中,你不應直接操作FMStatement對象
FMResultSet *rs = 0x00; // FMResultSet對象是用來獲取最終查詢結(jié)果的
// 需要追蹤sql執(zhí)行狀態(tài)的話,輸出執(zhí)行狀態(tài)
if (_traceExecution && sql) {
NSLog(@"%@ executeQuery: %@", self, sql);
}
// 調(diào)用sql語句之前,首先要將sql字符串預處理一下,轉(zhuǎn)化為SQLite可用的prepared語句(預處理語句)
// 使用sqlite3_prepare_v2來生成sql對應的prepare語句(即pStmt)代價很大
// 所以建議使用緩存機制來減少對sqlite3_prepare_v2的使用
if (_shouldCacheStatements) {
// 獲取到緩存中的prepared語句
statement = [self cachedStatementForQuery:sql];
pStmt = statement ? [statement statement] : 0x00;
// prepared語句可以被重置(調(diào)用sqlite3_reset函數(shù)),然后可以重新綁定參數(shù)以便重新執(zhí)行。
[statement reset];
}
// 如果緩存中沒有sql對應的prepared語句,那么只能使用sqlite3_prepare_v2函數(shù)進行預處理
if (!pStmt) {
rc = sqlite3_prepare_v2(_db, [sql UTF8String], -, &pStmt, );
// 如果生成prepared語句出錯,那么就根據(jù)是否需要打印錯誤信息(_logsErrors)以及是否遇到錯誤直接中止程序執(zhí)行(_crashOnErrors)來執(zhí)行出錯處理。
// 最后調(diào)用sqlite3_finalize函數(shù)釋放所有的內(nèi)部資源和sqlite3_stmt數(shù)據(jù)結(jié)構(gòu),有效刪除prepared語句。
if (SQLITE_OK != rc) {
if (_logsErrors) {
NSLog(@"DB Error: %d \"%@\"", [self lastErrorCode], [self lastErrorMessage]);
NSLog(@"DB Query: %@", sql);
NSLog(@"DB Path: %@", _databasePath);
}
if (_crashOnErrors) {
NSAssert(false, @"DB Error: %d \"%@\"", [self lastErrorCode], [self lastErrorMessage]);
// abort()函數(shù)表示中止程序執(zhí)行,直接從調(diào)用的地方跳出。
abort();
}
sqlite3_finalize(pStmt);
_isExecutingStatement = NO;
return nil;
}
}
id obj;
int idx = ;
// 獲取到pStmt中需要綁定的參數(shù)個數(shù)
int queryCount = sqlite3_bind_parameter_count(pStmt); // pointed out by Dominic Yu (thanks!)
if (dictionaryArgs) {
for (NSString *dictionaryKey in [dictionaryArgs allKeys]) {
// 在每個dictionaryKey之前加上冒號,比如上面的a -> :a,方便獲取參數(shù)在prepared語句中的索引
NSString *parameterName = [[NSString alloc] initWithFormat:@":%@", dictionaryKey];
// 查看執(zhí)行狀況
if (_traceExecution) {
NSLog(@"%@ = %@", parameterName, [dictionaryArgs objectForKey:dictionaryKey]);
}
// 在prepared語句中查找對應parameterName的參數(shù)索引值namedIdx
int namedIdx = sqlite3_bind_parameter_index(pStmt, [parameterName UTF8String]);
FMDBRelease(parameterName);
// 可以利用索引namedIdx獲取對應參數(shù),再使用bindObject:函數(shù)將dictionaryArgs保存的value綁定給對應參數(shù)
if (namedIdx > ) {
[self bindObject:[dictionaryArgs objectForKey:dictionaryKey] toColumn:namedIdx inStatement:pStmt];
// 使用這個idx來判斷sql中的所有參數(shù)值是否都綁定上了
idx++;
}
else {
NSLog(@"Could not find index for %@", dictionaryKey);
}
}
}
else {
while (idx < queryCount) {
// 使用arrayArgs的例子
/**
[db executeQuery:@"insert into testOneHundredTwelvePointTwo values (?, ?)" withArgumentsInArray:[NSArray arrayWithObjects:@"one", [NSNumber numberWithInteger:2], nil]];
*/
if (arrayArgs && idx < (int)[arrayArgs count]) {
obj = [arrayArgs objectAtIndex:(NSUInteger)idx];
}
// 使用args的例子,使用args其實就是調(diào)用- (FMResultSet *)executeQuery:(NSString*)sql, ...;
/**
FMResultSet *rs = [db executeQuery:@"select rowid,* from test where a = ?", @"hi'"];
*/
else if (args) {
obj = va_arg(args, id);
}
else {
break;
}
if (_traceExecution) {
if ([obj isKindOfClass:[NSData class]]) {
NSLog(@"data: %ld bytes", (unsigned long)[(NSData*)obj length]);
}
else {
NSLog(@"obj: %@", obj);
}
}
idx++;
// 綁定參數(shù)值
[self bindObject:obj toColumn:idx inStatement:pStmt];
}
}
// 如果綁定的參數(shù)數(shù)目不對,認為出錯,并釋放資源
if (idx != queryCount) {
NSLog(@"Error: the bind count is not correct for the # of variables (executeQuery)");
sqlite3_finalize(pStmt);
_isExecutingStatement = NO;
return nil;
}
FMDBRetain(statement); // to balance the release below
// statement不為空,進行緩存
if (!statement) {
statement = [[FMStatement alloc] init];
[statement setStatement:pStmt];
// 使用sql作為key來緩存statement(即sql對應的prepare語句)
if (_shouldCacheStatements && sql) {
[self setCachedStatement:statement forQuery:sql];
}
}
// 根據(jù)statement和self(FMDatabase對象)構(gòu)建一個FMResultSet對象,此函數(shù)中僅僅是構(gòu)建該對象,還沒使用next等函數(shù)獲取查詢結(jié)果
// 注意FMResultSet中含有以下成員(除了最后一個,其他成員均在此處初始化過了)
/**
@interface FMResultSet : NSObject {
FMDatabase *_parentDB; // 表示該對象查詢的數(shù)據(jù)庫,主要是為了能在FMResultSet自己的函數(shù)中索引到正在操作的FMDatabase對象
FMStatement *_statement; // prepared語句
NSString *_query; // 對應的sql查詢語句
NSMutableDictionary *_columnNameToIndexMap;
}
*/
rs = [FMResultSet resultSetWithStatement:statement usingParentDatabase:self];
[rs setQuery:sql];
// 將此時的FMResultSet對象添加_openResultSets,主要是為了調(diào)試
NSValue *openResultSet = [NSValue valueWithNonretainedObject:rs];
[_openResultSets addObject:openResultSet];
// 并設(shè)置statement的使用數(shù)目useCount加1,暫時不清楚此成員有何作用,感覺也是用于調(diào)試
[statement setUseCount:[statement useCount] + ];
FMDBRelease(statement);
// 生成statement的操作已經(jīng)結(jié)束
_isExecutingStatement = NO;
return rs;
}
舉例dictionaryArgs: NSMutableDictionary
*dictionaryArgs = [NSMutableDictionary dictionary]; [dictionaryArgs setObject:@"Text1" forKey:@"a"]; [db executeQuery:@"select * from namedparamcounttest where a = :a" withParameterDictionary:dictionaryArgs]; // 注意類似:AAA前面有冒號的就是參數(shù) // 其他的參數(shù)形式如:"?", "?NNN", ":AAA", "$AAA", 或 "@AAA" */
[FMResultSet nextWithError:]
- [FMResultSet next]函數(shù)其實就是對nextWithError:的簡單封裝。作用就是從我們上一步open中獲取到的FMResultSet對象中讀取查詢后結(jié)果的每一行,交給用戶自己處理。讀取每一行的方法(即next)其實就是封裝了sqlite3_step函數(shù)。而nextWithError:主要封裝了對sqlite3_step函數(shù)返回結(jié)果的處理。
int sqlite3_step(sqlite3_stmt*);
sqlite3_prepare函數(shù)將SQL命令字符串解析并轉(zhuǎn)換為一系列的命令字節(jié)碼,這些字節(jié)碼最終被傳送到SQlite3的虛擬數(shù)據(jù)庫引擎(VDBE: Virtual Database Engine)中執(zhí)行,完成這項工作的是sqlite3_step函數(shù)。比如一個SELECT查詢操作,sqlite3_step函數(shù)的每次調(diào)用都會返回結(jié)果集中的其中一行,直到再沒有有效數(shù)據(jù)行了。每次調(diào)用sqlite3_step函數(shù)如果返回SQLITE_ROW,代表獲得了有效數(shù)據(jù)行,可以通過sqlite3_column函數(shù)提取某列的值。如果調(diào)用sqlite3_step函數(shù)返回SQLITE_DONE,則代表prepared語句已經(jīng)執(zhí)行到終點了,沒有有效數(shù)據(jù)了。很多命令第一次調(diào)用sqlite3_step函數(shù)就會返回SQLITE_DONE,因為這些SQL命令不會返回數(shù)據(jù)。對于INSERT,UPDATE,DELETE命令,會返回它們所修改的行號——一個單行單列的值。
// 返回YES表示從數(shù)據(jù)庫中獲取到了下一行數(shù)據(jù)
- (BOOL)nextWithError:(NSError **)outErr {
// 嘗試步進到下一行
int rc = sqlite3_step([_statement statement]);
// 對返回結(jié)果rc進行處理
/**
SQLITE_BUSY 數(shù)據(jù)庫文件有鎖
SQLITE_LOCKED 數(shù)據(jù)庫中的某張表有鎖
SQLITE_DONE sqlite3_step()執(zhí)行完畢
SQLITE_ROW sqlite3_step()獲取到下一行數(shù)據(jù)
SQLITE_ERROR 一般用于沒有特別指定錯誤碼的錯誤,就是說函數(shù)在執(zhí)行過程中發(fā)生了錯誤,但無法知道錯誤發(fā)生的原因。
SQLITE_MISUSE 沒有正確使用SQLite接口,比如一條語句在sqlite3_step函數(shù)執(zhí)行之后,沒有被重置之前,再次給其綁定參數(shù),這時bind函數(shù)就會返回SQLITE_MISUSE。
*/
if (SQLITE_BUSY == rc || SQLITE_LOCKED == rc) {
NSLog(@"%s:%d Database busy (%@)", __FUNCTION__, __LINE__, [_parentDB databasePath]);
NSLog(@"Database busy");
if (outErr) {
// lastError使用sqlite3_errcode獲取到錯誤碼,封裝成NSError對象返回
*outErr = [_parentDB lastError];
}
}
else if (SQLITE_DONE == rc || SQLITE_ROW == rc) {
// all is well, let's return.
}
else if (SQLITE_ERROR == rc) {
// sqliteHandle就是獲取到對應FMDatabase對象,然后使用sqlite3_errmsg來獲取錯誤碼的字符串
NSLog(@"Error calling sqlite3_step (%d: %s) rs", rc, sqlite3_errmsg([_parentDB sqliteHandle]));
if (outErr) {
*outErr = [_parentDB lastError];
}
}
else if (SQLITE_MISUSE == rc) {
// uh oh.
NSLog(@"Error calling sqlite3_step (%d: %s) rs", rc, sqlite3_errmsg([_parentDB sqliteHandle]));
if (outErr) {
if (_parentDB) {
*outErr = [_parentDB lastError];
}
else {
// 如果next和nextWithError函數(shù)是在當前的FMResultSet關(guān)閉之后調(diào)用的
// 這時輸出的錯誤信息應該是parentDB不存在
NSDictionary* errorMessage = [NSDictionary dictionaryWithObject:@"parentDB does not exist" forKey:NSLocalizedDescriptionKey];
*outErr = [NSError errorWithDomain:@"FMDatabase" code:SQLITE_MISUSE userInfo:errorMessage];
}
}
}
else {
// wtf?
NSLog(@"Unknown error calling sqlite3_step (%d: %s) rs", rc, sqlite3_errmsg([_parentDB sqliteHandle]));
if (outErr) {
*outErr = [_parentDB lastError];
}
}
// 如果不是讀取下一行數(shù)據(jù),那么就關(guān)閉數(shù)據(jù)庫
if (rc != SQLITE_ROW) {
[self close];
}
return (rc == SQLITE_ROW);
}
[FMDatabase close]
與open函數(shù)成對調(diào)用。主要還是封裝了sqlite_close函數(shù)。
- (BOOL)close {
// 清除緩存的prepared語句,下面會詳解
[self clearCachedStatements];
// 關(guān)閉所有打開的FMResultSet對象,目前看來這個_openResultSets大概也是用來調(diào)試的
[self closeOpenResultSets];
if (!_db) {
return YES;
}
int rc;
BOOL retry;
BOOL triedFinalizingOpenStatements = NO;
do {
retry = NO;
// 調(diào)用sqlite3_close來嘗試關(guān)閉數(shù)據(jù)庫
rc = sqlite3_close(_db);
//如果當前數(shù)據(jù)庫上鎖,那么就先嘗試重新關(guān)閉(置retry為YES) // 同時還嘗試釋放數(shù)據(jù)庫中的prepared語句資源
if (SQLITE_BUSY == rc || SQLITE_LOCKED == rc) {
if (!triedFinalizingOpenStatements) {
triedFinalizingOpenStatements = YES;
sqlite3_stmt *pStmt;
// sqlite3_next_stmt(sqlite3 *pDb, sqlite3_stmt *pStmt)表示從數(shù)據(jù)庫pDb中對應的pStmt語句開始一個個往下找出相應prepared語句,如果pStmt為nil,那么就從pDb的第一個prepared語句開始。
// 此處迭代找到數(shù)據(jù)庫中所有prepared語句,釋放其資源。
while ((pStmt = sqlite3_next_stmt(_db, nil)) !=) {
NSLog(@"Closing leaked statement");
sqlite3_finalize(pStmt);
retry = YES;
}
}
}
// 關(guān)閉出錯,輸出錯誤碼
else if (SQLITE_OK != rc) {
NSLog(@"error closing!: %d", rc);
}
}
while (retry);
_db = nil;
return YES;
}
// _cachedStatements是用來緩存prepared語句的,所以清空_cachedStatements就是將每個緩存的prepared語句釋放
// 具體實現(xiàn)就是使用下面那個close函數(shù),close函數(shù)中調(diào)用了sqlite_finalize函數(shù)釋放資源
- (void)clearCachedStatements {
for (NSMutableSet *statements in [_cachedStatements objectEnumerator]) {
// makeObjectsPerformSelector會并發(fā)執(zhí)行同一件事,所以效率比for循環(huán)一個個執(zhí)行要快很多
[statements makeObjectsPerformSelector:@selector(close)];
}
[_cachedStatements removeAllObjects];
}
// 注意:此為FMResultSet的close函數(shù)
- (void)close {
if (_statement) {
sqlite3_finalize(_statement);
_statement = 0x00;
}
_inUse = NO;
}
// 清除_openResultSets
- (void)closeOpenResultSets {
//Copy the set so we don't get mutation errors
NSSet *openSetCopy = FMDBReturnAutoreleased([_openResultSets copy]);
// 迭代關(guān)閉_openResultSets中的FMResultSet對象
for (NSValue *rsInWrappedInATastyValueMeal in openSetCopy) {
FMResultSet *rs = (FMResultSet *)[rsInWrappedInATastyValueMeal pointerValue];
// 清除FMResultSet的操作
[rs setParentDB:nil];
[rs close];
[_openResultSets removeObject:rsInWrappedInATastyValueMeal];
}
}
executeUpdate:系列函數(shù)
注意除了“SELECT”語句外,其他的SQL語句都需要使用executeUpdate:系列函數(shù),這些SQL語句包括CREATE, UPDATE, INSERT, ALTER, COMMIT, BEGIN, DETACH, DELETE, DROP, END, EXPLAIN, VACUUM, 和REPLACE等等。
基本上所有executeUpdate:系列函數(shù)都是對- [FMDatabase executeUpdate:error:withArgumentsInArray:orDictionary:orVAList:]函數(shù)的封裝。注意- [FMDatabase executeUpdate:error:withArgumentsInArray:orDictionary:orVAList:]函數(shù)的具體實現(xiàn),基本和- [FMDatabase executeQuery:withArgumentsInArray:orDictionary:orVAList:]大部分實現(xiàn)是差不多的,關(guān)鍵在于executeQuery是查詢語句,所以它需要FMResultSet來保存查詢的結(jié)果。而executeUpdate是非查詢語句,不需要保存查詢結(jié)果,但需要調(diào)用sqlite3_step(pStmt)來執(zhí)行該SQL語句。這里就不贅述了,詳見源碼。
executeStatements:系列函數(shù)
使用executeStatements:函數(shù)可以將多個SQL執(zhí)行語句寫在一個字符串中,并執(zhí)行。具體使用舉例如下:
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;
}];
基本上executeStatements:系列函數(shù)最終封裝的都是- [FMDatabase executeStatements:withResultBlock:]函數(shù),而此函數(shù)又是對sqlite3_exec函數(shù)的封裝。
sqlite3_exec(sqlite3*, const char *sql, sqlite_callback, void *data, char **errmsg)
該例程提供了一個執(zhí)行 SQL 命令的快捷方式,SQL 命令由 sql 參數(shù)提供,可以由多個 SQL 命令組成。
在這里,第一個參數(shù) sqlite3 是打開的數(shù)據(jù)庫對象,sqlite_callback 是一個回調(diào),data 作為其第一個參數(shù),errmsg 將被返回用來獲取程序生成的任何錯誤。
sqlite3_exec() 程序解析并執(zhí)行由 sql 參數(shù)所給的每個命令,直到字符串結(jié)束或者遇到錯誤為止。
executeStatements:源碼如下:
- (BOOL)executeStatements:(NSString *)sql withResultBlock:(FMDBExecuteStatementsCallbackBlock)block {
int rc;
char *errmsg = nil;
rc = sqlite3_exec([self sqliteHandle], [sql UTF8String], block ? FMDBExecuteBulkSQLCallback : nil, (__bridge void *)(block), &errmsg);
if (errmsg && [self logsErrors]) {
NSLog(@"Error inserting batch: %s", errmsg);
sqlite3_free(errmsg);
}
return (rc == SQLITE_OK);
}
executeQueryWithFormat:和executeUpdateWithFormat:函數(shù)
考慮到如果用戶直接調(diào)用printf那種形式的字符串(比如“ INSERT INTO myTable (%@) VALUES (%d)”, “age”,25),那么就需要自己將對應字符串處理成相應的SQL語句。恰好executeQuery和executeUpdate系列函數(shù)提供了相應的接口:
- (FMResultSet *)executeQueryWithFormat:(NSString*)format, ... NS_FORMAT_FUNCTION(1,2);
- (BOOL)executeUpdateWithFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2);
其實這兩個函數(shù)和其他executeQuery和executeUpdate系列方法,多的就是一個將format和…轉(zhuǎn)化為可用的SQL語句步驟。其它部分其實本質(zhì)還是調(diào)用- [FMDatabase executeUpdate:error:withArgumentsInArray:orDictionary:orVAList:]和- [FMDatabase executeQuery:withArgumentsInArray:orDictionary:orVAList:]。下面僅列出format和…的轉(zhuǎn)化代碼:
va_list args;
// 將args指向format中第一個參數(shù)
va_start(args, format);
NSMutableString *sql = [NSMutableString stringWithCapacity:[format length]];
NSMutableArray *arguments = [NSMutableArray array];
// 使用extractSQL函數(shù)將format和args轉(zhuǎn)化為sql和arguments供后面函數(shù)使用
[self extractSQL:format argumentsList:args intoString:sql arguments:arguments];
// 關(guān)閉args,與va_start成對出現(xiàn)
va_end(args);
至于extractSQL:這個函數(shù)其實就是將(“INSERT INTO myTable (%@) VALUES (%d)”, “age”,25)中的%s和%d這種符號變成”?”,然后將”age”和25加入到arguments中。具體實現(xiàn)如下:
- (void)extractSQL:(NSString *)sql argumentsList:(va_list)args intoString:(NSMutableString *)cleanedSQL arguments:(NSMutableArray *)arguments {
NSUInteger length = [sql length];
unichar last = '\0';
for (NSUInteger i = 0; i < length; ++i) {
id arg = nil;
/** 使用last和current兩個變量(有些還需要next變量,比如%llu)判斷當前掃描到的字符串是不是%@、
%c、%s、%d等等。舉個例子,如果碰到%s,那么說明我替換的參數(shù)其實是一個字符串,所以使用arg =
[NSString stringWithUTF8String:]獲取到相應的arg作為參數(shù)值,
*/
// 注意type va_arg(va_list arg_ptr,type)函數(shù)是根據(jù)傳入的type參數(shù)決定返回值類型的
// 另外它的作用是獲取下一個參數(shù)的地址
unichar current = [sql characterAtIndex:i];
unichar add = current;
if (last == '%') {
switch (current) {
case '@':
arg = va_arg(args, id);
break;
case 'c':
// warning: second argument to 'va_arg' is of promotable type 'char'; this va_arg has undefined behavior because arguments will be promoted to 'int'
arg = [NSString stringWithFormat:@"%c", va_arg(args, int)];
break;
case 's':
arg = [NSString stringWithUTF8String:va_arg(args, char*)];
break;
case 'd':
case 'D':
case 'i':
arg = [NSNumber numberWithInt:va_arg(args, int)];
break;
case 'u':
case 'U':
arg = [NSNumber numberWithUnsignedInt:va_arg(args, unsigned int)];
break;
// %hi表示short int,%hu表示short unsigned int
case 'h':
i++;
if (i < length && [sql characterAtIndex:i] == 'i') {
// warning: second argument to 'va_arg' is of promotable type 'short'; this va_arg has undefined behavior because arguments will be promoted to 'int'
arg = [NSNumber numberWithShort:(short)(va_arg(args, int))];
}
else if (i < length && [sql characterAtIndex:i] == 'u') {
// warning: second argument to 'va_arg' is of promotable type 'unsigned short'; this va_arg has undefined behavior because arguments will be promoted to 'int'
arg = [NSNumber numberWithUnsignedShort:(unsigned short)(va_arg(args, uint))];
}
else {
i--;
}
break;
// %qi表示long long,%qu表示unsigned long long
case 'q':
i++;
if (i < length && [sql characterAtIndex:i] == 'i') {
arg = [NSNumber numberWithLongLong:va_arg(args, long long)];
}
else if (i < length && [sql characterAtIndex:i] == 'u') {
arg = [NSNumber numberWithUnsignedLongLong:va_arg(args, unsigned long long)];
}
else {
i--;
}
break;
case 'f':
arg = [NSNumber numberWithDouble:va_arg(args, double)];
break;
// %g原本是根據(jù)數(shù)據(jù)選擇合適的方式輸出(浮點數(shù)還是科學計數(shù)法),不過此處是用float類型輸出
case 'g':
// warning: second argument to 'va_arg' is of promotable type 'float'; this va_arg has undefined behavior because arguments will be promoted to 'double'
arg = [NSNumber numberWithFloat:(float)(va_arg(args, double))];
break;
case 'l':
i++;
if (i < length) {
unichar next = [sql characterAtIndex:i];
if (next == 'l') {
i++;
if (i < length && [sql characterAtIndex:i] == 'd') {
//%lld
arg = [NSNumber numberWithLongLong:va_arg(args, long long)];
}
else if (i < length && [sql characterAtIndex:i] == 'u') {
//%llu
arg = [NSNumber numberWithUnsignedLongLong:va_arg(args, unsigned long long)];
}
else {
i--;
}
}
else if (next == 'd') {
//%ld
arg = [NSNumber numberWithLong:va_arg(args, long)];
}
else if (next == 'u') {
//%lu
arg = [NSNumber numberWithUnsignedLong:va_arg(args, unsigned long)];
}
else {
i--;
}
}
else {
i--;
}
break;
default:
// something else that we can't interpret. just pass it on through like normal
break;
}
}
else if (current == '%') {
// 遇到%,直接跳過。
add = '\0';
}
// 如果arg不為空,表示確定arg是參數(shù),那么就使用?替換它,并將其對應參數(shù)值arg添加到arguments
if (arg != nil) {
[cleanedSQL appendString:@"?"];
[arguments addObject:arg];
}
// 如果參數(shù)格式是%@,但此時arg是空,那么就替換為NULL
else if (add == (unichar)'@' && last == (unichar) '%') {
[cleanedSQL appendFormat:@"NULL"];
}
// 如果不是參數(shù),就用原先字符串替換
else if (add != '\0') {
[cleanedSQL appendFormat:@"%C", add];
}
last = current;
}
}
- (void)bindObject:(id)obj toColumn:(int)idx inStatement:(sqlite3_stmt*)pStmt
該函數(shù)是用來在pStmt中綁定參數(shù)值到指定(根據(jù)idx)參數(shù)上。具體封裝的是sqlite3_bind系列函數(shù)。
如果要使用sqlite3_bind系列函數(shù),需要指定三個參數(shù),一個是正在使用的sqlite_stmt對象,一個是參數(shù)索引idx,還有一個就是需要綁定的參數(shù)值,此函數(shù)解決的關(guān)鍵就是根據(jù)obj判斷出其類型,然后調(diào)用相關(guān)的sqlite3_bind*函數(shù),比如obj是int型,那么就調(diào)用sqlite3_bind_int函數(shù)。又或者obj是NSData類型,那么就調(diào)用sqlite_bind_blob函數(shù)。具體后面詳細解釋。
- (void)bindObject:(id)obj toColumn:(int)idx inStatement:(sqlite3_stmt*)pStmt {
// 如果obj為指針為空,那么就使用sqlite3_bind_null給該參數(shù)綁定SQL null。
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) {
// 如果obj是一個空的NSData對象
// 不要直接將NULL指針作為參數(shù)值,否則sqlite會綁定一個NULL指針給參數(shù),而不是一個blob對象(Binary Large Object)
bytes = "";
}
// SQLITE_STATIC表示傳過來參數(shù)值的指針是不變的,所以完事后不需要銷毀它,與其相對的是
SQLITE_TRANSIENT
sqlite3_bind_blob(pStmt, idx, bytes, (int)[obj length], SQLITE_STATIC);
}
// 如果obj是一個NSDate對象
else if ([obj isKindOfClass:[NSDate class]]) {
// 如果你自定義了Date格式,那么就將該NSDate轉(zhuǎn)化為你定義的格式,并綁定到參數(shù)上
// 如果沒有自定義Date格式,那么默認使用timeIntervalSince1970來計算參數(shù)值進行綁定
if (self.hasDateFormatter)
sqlite3_bind_text(pStmt, idx, [[self stringFromDate:obj] UTF8String], -1, SQLITE_STATIC);
else
sqlite3_bind_double(pStmt, idx, [obj timeIntervalSince1970]);
}
// 如果是NSNumber對象,注意此處判斷obj類型的方法
// @encode,@編譯器指令之一,返回一個給定類型編碼為一種內(nèi)部表示的字符串(例如,@encode(int) → i),類似于 ANSI C 的 typeof 操作。蘋果的 Objective-C 運行時庫內(nèi)部利用類型編碼幫助加快消息分發(fā)。
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) { // bool使用sqlite3_bind_int來綁定的
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);
}
}
openWithFlags:系列函數(shù)
除了前面提到過的open函數(shù)外,F(xiàn)MDB還為我們提供了openWithFlags:系列函數(shù),其本質(zhì)是封裝了sqlite3_open_v2。
int sqlite3_open_v2(
const char *filename, /* 數(shù)據(jù)庫名稱 (UTF-8) */
sqlite3 **ppDb, /* 輸出: SQLite數(shù)據(jù)庫對象 */
int flags, /* 標識符 */
const char *zVfs /* 想要使用的VFS名稱 */
)
對于sqlite3_open和sqlite3_open16函數(shù),如果可能將以可讀可寫的方式打開數(shù)據(jù)庫,否則以只讀的方式打開數(shù)據(jù)庫。如果要打開的數(shù)據(jù)庫文件不存在,就新建一個。對于sqlite3_open_v2函數(shù),情況就要復雜一些了,因為這個v2版本的函數(shù)強大就強大在它可以對打開(連接)數(shù)據(jù)庫的方式進行控制,具體是通過它的參數(shù)flags來完成。sqlite3_open_v2函數(shù)只支持UTF-8編碼的SQlite3數(shù)據(jù)庫文件。
如flags設(shè)置為SQLITE_OPEN_READONLY,則SQlite3數(shù)據(jù)庫文件以只讀的方式打開,如果該數(shù)據(jù)庫文件不存在,則sqlite3_open_v2函數(shù)執(zhí)行失敗,返回一個error。如果flags設(shè)置為SQLITE_OPEN_READWRITE,則SQlite3數(shù)據(jù)庫文件以可讀可寫的方式打開,如果該數(shù)據(jù)庫文件本身被操作系統(tǒng)設(shè)置為寫保護狀態(tài),則以只讀的方式打開。如果該數(shù)據(jù)庫文件不存在,則sqlite3_open_v2函數(shù)執(zhí)行失敗,返回一個error。如果flags設(shè)置為SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE,則SQlite3數(shù)據(jù)庫文件以可讀可寫的方式打開,如果該數(shù)據(jù)庫文件不存在則新建一個。這也是sqlite3_open和sqlite3_open16函數(shù)的默認行為。除此之外,flags還可以設(shè)置為其他標志,具體可以查看SQlite官方文檔。
參數(shù)zVfs允許客戶應用程序命名一個虛擬文件系統(tǒng)(Virtual File System)模塊,用來與數(shù)據(jù)庫連接。VFS作為SQlite library和底層存儲系統(tǒng)(如某個文件系統(tǒng))之間的一個抽象層,通??蛻魬贸绦蚩梢院唵蔚慕o該參數(shù)傳遞一個NULL指針,以使用默認的VFS模塊。
對于UTF-8編碼的SQlite3數(shù)據(jù)庫文件,推薦使用sqlite3_open_v2函數(shù)進行連接,它可以對數(shù)據(jù)庫文件的打開和處理操作進行更多的控制。
FMResultSet其他的獲取結(jié)果方式
FMResultSet的resultSetWithStatement:、close、next函數(shù)。其實FMResultSet除了使用next獲取查詢結(jié)果外,還有很多其他的接口可以查詢到結(jié)果。
一系列的ForColumn:和ForColumnIndex:(表示對應的數(shù)據(jù)類型)函數(shù)都是用來獲取查詢結(jié)果的。這里值得注意的是ForColumn:函數(shù)本質(zhì)是調(diào)用相應的*ForColumnIndex:函數(shù)。比如:
- (int)intForColumn:(NSString*)columnName {
return [self intForColumnIndex:[self columnIndexForName:columnName]];
}
上述函數(shù)實現(xiàn)內(nèi)部做了一個轉(zhuǎn)化,就是利用columIndexForName:函數(shù)查詢到這個columnName對應的索引值。而這個columnIndexForName:本質(zhì)是根據(jù)_columnNameToIndexMap屬性獲取到列名稱(columnName)的對應列號(columnIdx)。_columnNameToIndexMap是一個NSMutableDictionary對象。其中key表示的是指定結(jié)果集中對應列的名稱,value表示的是指定結(jié)果集中對應的列號(columnIdx)。所以我們這里主要看下columnNameToIndexMap的實現(xiàn):
- (NSMutableDictionary *)columnNameToIndexMap {
if (!_columnNameToIndexMap) {
// 找出由statement指定的結(jié)果集中列的數(shù)目
int columnCount = sqlite3_column_count([_statement statement]);
_columnNameToIndexMap = [[NSMutableDictionary alloc] initWithCapacity:(NSUInteger)columnCount];
int columnIdx = 0;
// 將列號和該列對應名稱綁定在一起,組成_columnNameToIndexMap
for (columnIdx = 0; columnIdx < columnCount; columnIdx++) {
[_columnNameToIndexMap setObject:[NSNumber numberWithInt:columnIdx]
forKey:[[NSString stringWithUTF8String:sqlite3_column_name([_statement statement], columnIdx)] lowercaseString]];
}
}
return _columnNameToIndexMap;
}
這時我們再回頭看看*ForColumnIndex:函數(shù)的實現(xiàn)。它的本質(zhì)就是調(diào)用sqlite3_column_*(*表示對應的數(shù)據(jù)類型),也就是從statement中獲取到對應列號的數(shù)據(jù),比如
- (int)intForColumnIndex:(int)columnIdx {
return sqlite3_column_int([_statement statement], columnIdx);
}
FMDB的加解密
FMDB中使用- [FMDatabase setKey:]和- [FMDatabase setKeyWithData:]輸入數(shù)據(jù)庫密碼以求驗證用戶身份,使用- [FMDatabase rekey:]和- [FMDatabase rekeyWithData:]來給數(shù)據(jù)庫設(shè)置密碼或者清除密碼。這兩類函數(shù)分別對sqlite3_key和sqlite3_rekey函數(shù)進行了封裝。
int sqlite3_key( sqlite3 *db, const void *pKey, int nKey)
db 是指定數(shù)據(jù)庫,pKey 是密鑰,nKey 是密鑰長度。例:sqlite3_key( db, "abc", 3);
sqlite3_key是輸入密鑰,如果數(shù)據(jù)庫已加密必須先執(zhí)行此函數(shù)并輸入正確密鑰才能進行操作,如果數(shù)據(jù)庫沒有加密,執(zhí)行此函數(shù)后進行數(shù)據(jù)庫操作反而會出現(xiàn)“此數(shù)據(jù)庫已加密或不是一個數(shù)據(jù)庫文件”的錯誤。
int sqlite3_rekey( sqlite3 *db, const void *pKey, int nKey)
參數(shù)同sqlite3_key。
sqlite3_rekey是變更密鑰或給沒有加密的數(shù)據(jù)庫添加密鑰或清空密鑰,變更密鑰或清空密鑰前必須先正確執(zhí)行 sqlite3_key。在正確執(zhí)行 sqlite3_rekey 之后在 sqlite3_close 關(guān)閉數(shù)據(jù)庫之前可以正常操作數(shù)據(jù)庫,不需要再執(zhí)行 sqlite3_key。
清空密鑰為 sqlite3_rekey( db, NULL, 0)。
FMDatabaseQueue使用舉例
// 創(chuàng)建,最好放在一個單例的類中
FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:aPath];
// 使用
[queue
inDatabase
:^(FMDatabase *db) {
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:]];
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:]];
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:]];
FMResultSet *rs = [db executeQuery:@"select * from foo"];
while ([rs next]) {
// …
}
}];
// 如果要支持事務
[queue
inTransaction
:^(FMDatabase *db, BOOL *rollback) {
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:]];
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:]];
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:]];
if (whoopsSomethingWrongHappened) {
*rollback = YES;
return;
}
// etc…
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:]];
}];
我們可以看到FMDB的多線程實現(xiàn)主要是依賴于FMDatabaseQueue這個類。
+ [FMDatabaseQueue databaseQueueWithPath:]
// 調(diào)用initWithPath:函數(shù)構(gòu)建一個FMDatabaseQueue對象
+ (instancetype)databaseQueueWithPath:(NSString*)aPath {
FMDatabaseQueue *q = [[self alloc] initWithPath:aPath];
FMDBAutorelease(q);
return q;
}
// 使用aPath作為數(shù)據(jù)庫名稱,并傳入openFlags和vfsName作為openWithFlags:vfs:函數(shù)的參數(shù)
// 初始化一個database和相應的queue
- (instancetype)initWithPath:(NSString*)aPath flags:(int)openFlags vfs:(NSString *)vfsName {
// 除了另外定義了一個_queue外,其他部分和FMDatabase的初始化沒什么不同
self = [super init];
if (self != nil) {
_db = [[[self class] databaseClass] databaseWithPath:aPath];
FMDBRetain(_db);
#if SQLITE_VERSION_NUMBER >= 3005000
BOOL success = [_db openWithFlags:openFlags vfs:vfsName];
#else
BOOL success = [_db open];
#endif
if (!success) {
NSLog(@"Could not create database queue for path %@", aPath);
FMDBRelease(self);
return 0x00;
}
_path = FMDBReturnRetained(aPath);
// 創(chuàng)建了一個串行隊列
_queue = dispatch_queue_create([[NSString stringWithFormat:@"fmdb.%@", self] UTF8String], NULL);
/** 給_queue這個GCD隊列指定了一個kDispatchQueueSpecificKey字符串,并和self(即當前FMDatabaseQueue對象)進行綁定。日后可以通過此字符串獲取到綁定的對象(此處就是self)。當然,你要保證正在執(zhí)行的GCD隊列是你之前指定的那個_queue隊列。是不是有objc_setAssociatedObject函數(shù)的感覺。
此步驟的作用后面inDatabase函數(shù)中會具體講解。
*/
dispatch_queue_set_specific(_queue, kDispatchQueueSpecificKey, (__bridge void *)self, NULL);
_openFlags = openFlags;
}
return self;
}
[FMDatabaseQueue inDatabase:]
注意inDatabase的參數(shù)是一個block。這個block一般是封裝了數(shù)據(jù)庫的操作,另外這個block在inDatabase中是同步執(zhí)行的。
- (void)inDatabase:(void (^)(FMDatabase *db))block {
/* 使用dispatch_get_specific來查看當前queue是否是之前設(shè)定的那個_queue,如果是的話,那么使用kDispatchQueueSpecificKey作為參數(shù)傳給dispatch_get_specific,如果返回的值不為空,那么返回值應該就是上面initWithPath:函數(shù)中綁定的那個FMDatabaseQueue對象。有人說除了當前queue還有可能有其他什么queue?這就是FMDatabaseQueue的用途,你可以創(chuàng)建多個FMDatabaseQueue對象來并發(fā)執(zhí)行不同的SQL語句。
另外為啥要判斷是不是當前執(zhí)行的這個queue?是為了防止死鎖!
*/
FMDatabaseQueue *currentSyncQueue = (__bridge id)dispatch_get_specific(kDispatchQueueSpecificKey);
assert(currentSyncQueue != self && "inDatabase: was called reentrantly on the same queue, which would lead to a deadlock");
FMDBRetain(self);
// 在當前這個queue中同步執(zhí)行block
dispatch_sync(_queue, ^() {
FMDatabase *db = [self database];
block(db);
// 下面這部分你也看到了,定義了DEBUG宏,明顯是用來調(diào)試用的。就不贅述了
if ([db hasOpenResultSets]) {
NSLog(@"Warning: there is at least one open result set around after performing [FMDatabaseQueue inDatabase:]");
#if defined(DEBUG) && DEBUG
NSSet *openSetCopy = FMDBReturnAutoreleased([[db valueForKey:@"_openResultSets"] copy]);
for (NSValue *rsInWrappedInATastyValueMeal in openSetCopy) {
FMResultSet *rs = (FMResultSet *)[rsInWrappedInATastyValueMeal pointerValue];
NSLog(@"query: '%@'", [rs query]);
}
#endif
}
});
FMDBRelease(self);
}
其實我們從這個函數(shù)中就可以看出FMDatabaseQueue具體是怎么完成多線程的:

[FMDatabaseQueue inTransaction:]
該函數(shù)主要是針對數(shù)據(jù)庫事務的處理:
- (void)inTransaction:(void (^)(FMDatabase *db, BOOL *rollback))block {
[self beginTransaction:NO withBlock:block];
}
可以看到,內(nèi)部直接封裝的是beginTransaction:withBlock:函數(shù),那我們直接來看beginTransaction:withBlock:函數(shù)。
- (void)beginTransaction:(BOOL)useDeferred withBlock:(void (^)(FMDatabase *db, BOOL *rollback))block {
FMDBRetain(self);
dispatch_sync(_queue, ^() {
BOOL shouldRollback = NO;
if (useDeferred) {
// 如果使用延遲事務,那么就調(diào)用該函數(shù),下面有對該函數(shù)的詳解
// 想令useDeferred為YES,可以調(diào)用與inTransaction相對的inDeferredTransaction函數(shù)
[[self database] beginDeferredTransaction];
}
else {
// 默認使用排他事務,下面有排他事務的詳解
[[self database] beginTransaction];
}
// 注意該block除了要創(chuàng)建相應的數(shù)據(jù)庫事務,還需要根據(jù)需要選擇是否需要回滾
// 比如上面如果數(shù)據(jù)庫操作出錯了,那么你可以設(shè)置需要回滾,即返回shouldRollback為YES
block([self database], &shouldRollback);
// 如果需要回滾,那么就調(diào)用FMDatabase的rollback函數(shù)
if (shouldRollback) {
[[self database] rollback];
}
// 如果不需要回滾,那么就調(diào)用FMDatabase的commit函數(shù)確認提交相應SQL操作
else {
[[self database] commit];
}
});
FMDBRelease(self);
}
// 通過執(zhí)行rollback transaction語句來執(zhí)行回滾操作
- (BOOL)rollback {
BOOL b = [self executeUpdate:@"rollback transaction"];
// 既然已經(jīng)回滾了,那么表示是否在進行事務的_inTransaction屬性也要置為NO
if (b) {
_inTransaction = NO;
}
return b;
}
// 通過執(zhí)行commit transaction語句來執(zhí)行提交事務操作
- (BOOL)commit {
BOOL b = [self executeUpdate:@"commit transaction"];
// 既然已經(jīng)提交過事務了,那么表示是否在進行事務的_inTransaction屬性也要置為NO
if (b) {
_inTransaction = NO;
}
return b;
}
// 延遲事務指的是在對數(shù)據(jù)庫操作前不進行任何加鎖。默認情況下,
// 如果僅僅用BEGIN開始一個事務,那么事務就是DEFERRED的,同時它不會獲取任何鎖
- (BOOL)beginDeferredTransaction {
BOOL b = [self executeUpdate:@"begin deferred transaction"];
if (b) {
_inTransaction = YES;
}
return b;
}
// 默認進行的是排他(exclusive)操作
// 排他操作的實質(zhì)是在開始對數(shù)據(jù)庫讀寫前,獲得EXCLUSIVE鎖,即排他鎖。排它鎖說白點就是
// 告訴數(shù)據(jù)庫別的連接:這是我獨有的,誰都別想占有。
- (BOOL)beginTransaction {
BOOL b = [self executeUpdate:@"begin exclusive transaction"];
if (b) {
_inTransaction = YES;
}
return b;
}
[FMDatabaseQueue inSavePoint:]
savepoint類似于游戲存檔一樣的東西,一般的rollback相當于游戲重新開始,而加了savepoint后,相當于回到存檔的位置然后接著游戲。與inDatabase和inTransaction相對有一個inSavePoint:的方法(相當于加了save point功能的inDatabase函數(shù))。
/*
save point功能只在SQLite3.7及以上版本中使用,所以下面多數(shù)代碼加上了
#if SQLITE_VERSION_NUMBER >= 3007000
#else
#endif
*/
- (NSError*)inSavePoint:(void (^)(FMDatabase *db, BOOL *rollback))block {
#if SQLITE_VERSION_NUMBER >= 3007000
static unsigned long savePointIdx = ;
__block NSError *err = 0x00;
FMDBRetain(self);
// 同步執(zhí)行
dispatch_sync(_queue, ^() {
// 設(shè)定savepoint的名稱,即給游戲存檔設(shè)一個名字
NSString *name = [NSString stringWithFormat:@"savePoint%ld", savePointIdx++];
// 默認不回滾
BOOL shouldRollback = NO;
// 在執(zhí)行block之前,先進行存檔(save point)。如果有問題,直接退回這個存檔(save point)
if ([[self database] startSavePointWithName:name error:&err]) {
block([self database], &shouldRollback);
// 如果需要回滾,調(diào)用rollbackToSavePointWithName:error:回滾到存檔位置(savepoint)
if (shouldRollback) {
[[self database] rollbackToSavePointWithName:name error:&err];
}
// 記得執(zhí)行完block后,不管有沒有回滾,還需要釋放掉這個存檔
[[self database] releaseSavePointWithName:name error:&err];
}
});
FMDBRelease(self);
return err;
#else
NSString *errorMessage = NSLocalizedString(@"Save point functions require SQLite 3.7", nil);
if (self.logsErrors) NSLog(@"%@", errorMessage);
return [NSError errorWithDomain:@"FMDatabase" code: userInfo:@{NSLocalizedDescriptionKey : errorMessage}];
#endif
}
// 調(diào)用savepoint $savepointname的SQL語句對數(shù)據(jù)庫操作進行存檔
- (BOOL)startSavePointWithName:(NSString*)name error:(NSError**)outErr {
#if SQLITE_VERSION_NUMBER >= 3007000
NSParameterAssert(name);
NSString *sql = [NSString stringWithFormat:@"savepoint '%@';", FMDBEscapeSavePointName(name)];
return [self executeUpdate:sql error:outErr withArgumentsInArray:nil orDictionary:nil orVAList:nil];
#else
NSString *errorMessage = NSLocalizedString(@"Save point functions require SQLite 3.7", nil);
if (self.logsErrors) NSLog(@"%@", errorMessage);
return NO;
#endif
}
// 使用release savepoint $savepointname的SQL語句刪除存檔,主要是為了釋放資源
- (BOOL)releaseSavePointWithName:(NSString*)name error:(NSError**)outErr {
#if SQLITE_VERSION_NUMBER >= 3007000
NSParameterAssert(name);
NSString *sql = [NSString stringWithFormat:@"release savepoint '%@';", FMDBEscapeSavePointName(name)];
return [self executeUpdate:sql error:outErr withArgumentsInArray:nil orDictionary:nil orVAList:nil];
#else
NSString *errorMessage = NSLocalizedString(@"Save point functions require SQLite 3.7", nil);
if (self.logsErrors) NSLog(@"%@", errorMessage);
return NO;
#endif
}
// 調(diào)用rollback transaction to savepoint $savepointname的SQL語句來回退到存檔處
- (BOOL)rollbackToSavePointWithName:(NSString*)name error:(NSError**)outErr {
#if SQLITE_VERSION_NUMBER >= 3007000
NSParameterAssert(name);
NSString *sql = [NSString stringWithFormat:@"rollback transaction to savepoint '%@';", FMDBEscapeSavePointName(name)];
return [self executeUpdate:sql error:outErr withArgumentsInArray:nil orDictionary:nil orVAList:nil];
#else
NSString *errorMessage = NSLocalizedString(@"Save point functions require SQLite 3.7", nil);
if (self.logsErrors) NSLog(@"%@", errorMessage);
return NO;
#endif
}
總結(jié)
FMDB比較常用的幾個類基本上學習完畢。FMDB代碼上不是很難,核心還是SQLite3和數(shù)據(jù)庫的知識。更重要的還是要知道真實環(huán)境中的最佳實踐。