【原】FMDB源碼閱讀(二) 本文轉載請註明出處 —— polobymulberry-博客園 1. 前言 上一篇只是簡單地過了一下FMDB一個簡單例子的基本流程,並沒有涉及到FMDB的所有方方面面,比如FMDB的executeUpdate:系列方法、資料庫的加解密等等。這次寫的就是對FMDataba...
【原】FMDB源碼閱讀(二)
本文轉載請註明出處 —— polobymulberry-博客園
1. 前言
上一篇只是簡單地過了一下FMDB一個簡單例子的基本流程,並沒有涉及到FMDB的所有方方面面,比如FMDB的executeUpdate:系列方法、資料庫的加解密等等。這次寫的就是對FMDatabase和FMResultSet這兩個文件的補全內容。每次寫這種補全的內容最頭疼,內容會很分散,感覺沒啥條理。
2. executeUpdate:系列函數
註意除了“SELECT”語句外,其他的SQL語句都需要使用executeUpdate:系列函數,這些SQL語句包括`CREATE`, `UPDATE`, `INSERT`, `ALTER`, `COMMIT`, `BEGIN`, `DETACH`, `DELETE`, `DROP`, `END`, `EXPLAIN`, `VACUUM`, 和`REPLACE`等等。
executeUpdate:函數使用例子如下:
BOOL success = [db executeUpdate:@"INSERT INTO authors (identifier, name, date, comment) VALUES (?, ?, ?, ?)", @(identifier), name, date, comment ?: [NSNull null]]; if (!success) { NSLog(@"error = %@", [db lastErrorMessage]); }
基本上所有executeUpdate:系列函數都是對- [FMDatabase executeUpdate:error:withArgumentsInArray:orDictionary:orVAList:]函數的封裝。註意- [FMDatabase executeUpdate:error:withArgumentsInArray:orDictionary:orVAList:]函數的具體實現,基本和- [FMDatabase executeQuery:withArgumentsInArray:orDictionary:orVAList:]大部分實現是差不多的,關鍵在於executeQuery是查詢語句,所以它需要FMResultSet來保存查詢的結果。而executeUpdate是非查詢語句,不需要保存查詢結果,但需要調用sqlite3_step(pStmt)來執行該SQL語句。這裡就不贅述了,詳見源碼。
3. executeStatements:系列函數
使用executeStatements:函數可以將多個SQL執行語句寫在一個字元串中,並執行。具體使用舉例如下:
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:系列函數最終封裝的都是- [FMDatabase executeStatements:withResultBlock:]函數,而此函數又是對sqlite3_exec函數的封裝。
sqlite3_exec(sqlite3*, const char *sql, sqlite_callback, void *data, char **errmsg)
該常式提供了一個執行 SQL 命令的快捷方式,SQL 命令由 sql 參數提供,可以由多個 SQL 命令組成。
在這裡,第一個參數 sqlite3 是打開的資料庫對象,sqlite_callback 是一個回調,data 作為其第一個參數,errmsg 將被返回用來獲取程式生成的任何錯誤。
sqlite3_exec() 程式解析並執行由 sql 參數所給的每個命令,直到字元串結束或者遇到錯誤為止。
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); }
4. executeQueryWithFormat:和executeUpdateWithFormat:函數
考慮到如果用戶直接調用printf那種形式的字元串(比如“ INSERT INTO myTable (%@) VALUES (%d)”, “age”,25),那麼就需要自己將對應字元串處理成相應的SQL語句。恰好executeQuery和executeUpdate系列函數提供了相應的介面:
- (FMResultSet *)executeQueryWithFormat:(NSString*)format, ... NS_FORMAT_FUNCTION(1,2); - (BOOL)executeUpdateWithFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2);
其實這兩個函數和其他executeQuery和executeUpdate系列方法,多的就是一個將format和…轉化為可用的SQL語句步驟。其它部分其實本質還是調用- [FMDatabase executeUpdate:error:withArgumentsInArray:orDictionary:orVAList:]和- [FMDatabase executeQuery:withArgumentsInArray:orDictionary:orVAList:]。下麵僅列出format和…的轉化代碼:
va_list args; // 將args指向format中第一個參數 va_start(args, format); NSMutableString *sql = [NSMutableString stringWithCapacity:[format length]]; NSMutableArray *arguments = [NSMutableArray array]; // 使用extractSQL函數將format和args轉化為sql和arguments供後面函數使用 [self extractSQL:format argumentsList:args intoString:sql arguments:arguments]; // 關閉args,與va_start成對出現 va_end(args);
至於extractSQL:這個函數其實就是將(“INSERT INTO myTable (%@) VALUES (%d)”, “age”,25)中的%s和%d這種符號變成”?”,然後將”age”和25加入到arguments中。具體實現如下:
- (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,那麼說明我替換的參數其實是一個字元串,所以使用arg =
[NSString stringWithUTF8String:]獲取到相應的arg作為參數值,至於%s/%c/%llu這些表示什麼,
那就屬於C語言的範疇,此處就不討論了。
*/ // 註意type va_arg(va_list arg_ptr,type)函數是根據傳入的type參數決定返回值類型的
// 另外它的作用是獲取下一個參數的地址 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原本是根據數據選擇合適的方式輸出(浮點數還是科學計數法),不過此處是用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是參數,那麼就使用?替換它,並將其對應參數值arg添加到arguments if (arg != nil) { [cleanedSQL appendString:@"?"]; [arguments addObject:arg]; } // 如果參數格式是%@,但此時arg是空,那麼就替換為NULL else if (add == (unichar)'@' && last == (unichar) '%') { [cleanedSQL appendFormat:@"NULL"]; } // 如果不是參數,就用原先字元串替換 else if (add != '\0') { [cleanedSQL appendFormat:@"%C", add]; } last = current; } }
5. - (void)bindObject:(id)obj toColumn:(int)idx inStatement:(sqlite3_stmt*)pStmt
上一篇僅僅對該函數進行簡單說明,該函數是用來在pStmt中綁定參數值到指定(根據idx)參數上。具體封裝的是sqlite3_bind*系列函數。
如果要使用sqlite3_bind*系列函數,需要指定三個參數,一個是正在使用的sqlite_stmt對象,一個是參數索引idx,還有一個就是需要綁定的參數值,此函數解決的關鍵就是根據obj判斷出其類型,然後調用相關的sqlite3_bind*函數,比如obj是int型,那麼就調用sqlite3_bind_int函數。又或者obj是NSData類型,那麼就調用sqlite_bind_blob函數。具體後面詳細解釋。
- (void)bindObject:(id)obj toColumn:(int)idx inStatement:(sqlite3_stmt*)pStmt { // 如果obj為指針為空,那麼就使用sqlite3_bind_null給該參數綁定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指針作為參數值,否則sqlite會綁定一個NULL指針給參數,而不是一個blob對象(Binary Large Object) bytes = ""; } // SQLITE_STATIC表示傳過來參數值的指針是不變的,所以完事後不需要銷毀它,與其相對的是SQLITE_TRANSIENT
sqlite3_bind_blob(pStmt, idx, bytes, (int)[obj length], SQLITE_STATIC); } // 如果obj是一個NSDate對象 else if ([obj isKindOfClass:[NSDate class]]) { // 如果你自定義了Date格式,那麼就將該NSDate轉化為你定義的格式,並綁定到參數上 // 如果沒有自定義Date格式,那麼預設使用timeIntervalSince1970來計算參數值進行綁定 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,@編譯器指令之一,返回一個給定類型編碼為一種內部表示的字元串(例如,@encode(int) → i),類似於 ANSI C 的 typeof 操作。蘋果的 Objective-C 運行時庫內部利用類型編碼幫助加快消息分發。 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); } }
6. openWithFlags:系列函數
除了前面提到過的open函數外,FMDB還為我們提供了openWithFlags:系列函數,其本質是封裝了sqlite3_open_v2。
int sqlite3_open_v2( const char *filename, /* 資料庫名稱 (UTF-8) */ sqlite3 **ppDb, /* 輸出: SQLite資料庫對象 */ int flags, /* 標識符 */ const char *zVfs /* 想要使用的VFS名稱 */ );對於sqlite3_open和sqlite3_open16函數,如果可能將以可讀可寫的方式打開資料庫,否則以只讀的方式打開資料庫。如果要打開的資料庫文件不存在,就新建一個。對於sqlite3_open_v2函數,情況就要複雜一些了,因為這個v2版本的函數強大就強大在它可以對打開(連接)資料庫的方式進行控制,具體是通過它的參數flags來完成。sqlite3_open_v2函數只支持UTF-8編碼的SQlite3資料庫文件。
如flags設置為SQLITE_OPEN_READONLY,則SQlite3資料庫文件以只讀的方式打開,如果該資料庫文件不存在,則sqlite3_open_v2函數執行失敗,返回一個error。如果flags設置為SQLITE_OPEN_READWRITE,則SQlite3資料庫文件以可讀可寫的方式打開,如果該資料庫文件本身被操作系統設置為防寫狀態,則以只讀的方式打開。如果該資料庫文件不存在,則sqlite3_open_v2函數執行失敗,返回一個error。如果flags設置為SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE,則SQlite3資料庫文件以可讀可寫的方式打開,如果該資料庫文件不存在則新建一個。這也是sqlite3_open和sqlite3_open16函數的預設行為。除此之外,flags還可以設置為其他標誌,具體可以查看SQlite官方文檔。
參數zVfs允許客戶應用程式命名一個虛擬文件系統(Virtual File System)模塊,用來與資料庫連接。VFS作為SQlite library和底層存儲系統(如某個文件系統)之間的一個抽象層,通常客戶應用程式可以簡單的給該參數傳遞一個NULL指針,以使用預設的VFS模塊。
對於UTF-8編碼的SQlite3資料庫文件,推薦使用sqlite3_open_v2函數進行連接,它可以對資料庫文件的打開和處理操作進行更多的控制。
7. FMResultSet其他的獲取結果方式
之前只提到過FMResultSet的resultSetWithStatement:、close、next函數。其實FMResultSet除了使用next獲取查詢結果外,還有很多其他的介面可以查詢到結果。
一系列的*ForColumn:和*ForColumnIndex:(*表示對應的數據類型)函數都是用來獲取查詢結果的。這裡值得註意的是*ForColumn:函數本質是調用相應的*ForColumnIndex:函數。比如:
- (int)intForColumn:(NSString*)columnName { return [self intForColumnIndex:[self columnIndexForName:columnName]]; }
上述函數實現內部做了一個轉化,就是利用columIndexForName:函數查詢到這個columnName對應的索引值。而這個columnIndexForName:本質是根據_columnNameToIndexMap屬性獲取到列名稱(columnName)的對應列號(columnIdx)。_columnNameToIndexMap是一個NSMutableDictionary對象。其中key表示的是指定結果集中對應列的名稱,value表示的是指定結果集中對應的列號(columnIdx)。所以我們這裡主要看下columnNameToIndexMap的實現:
- (NSMutableDictionary *)columnNameToIndexMap { if (!_columnNameToIndexMap) { // 找出由statement指定的結果集中列的數目 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:函數的實現。它的本質就是調用sqlite3_column_*(*表示對應的數據類型),也就是從statement中獲取到對應列號的數據,比如
- (int)intForColumnIndex:(int)columnIdx { return sqlite3_column_int([_statement statement], columnIdx); }
8. FMDB的加解密
FMDB中使用- [FMDatabase setKey:]和- [FMDatabase setKeyWithData:]輸入資料庫密碼以求驗證用戶身份,使用- [FMDatabase rekey:]和- [FMDatabase rekeyWithData:]來給資料庫設置密碼或者清除密碼。這兩類函數分別對sqlite3_key和sqlite3_rekey函數進行了封裝。
int sqlite3_key( sqlite3 *db, const void *pKey, int nKey)
db 是指定資料庫,pKey 是密鑰,nKey 是密鑰長度。例:sqlite3_key( db, "abc", 3);
sqlite3_key是輸入密鑰,如果資料庫已加密必須先執行此函數並輸入正確密鑰才能進行操作,如果資料庫沒有加密,執行此函數後進行資料庫操作反而會出現“此資料庫已加密或不是一個資料庫文件”的錯誤。
int sqlite3_rekey( sqlite3 *db, const void *pKey, int nKey)
參數同sqlite3_key。
sqlite3_rekey是變更密鑰或給沒有加密的資料庫添加密鑰或清空密鑰,變更密鑰或清空密鑰前必須先正確執行 sqlite3_key。在正確執行 sqlite3_rekey 之後在 sqlite3_close 關閉資料庫之前可以正常操作資料庫,不需要再執行 sqlite3_key。
清空密鑰為 sqlite3_rekey( db, NULL, 0)。
// 下麵的代碼比較簡單,就不過多解釋了。核心就是sqlite3_key和sqlite3_rekey這兩個函數
// 使用rekey:和setKey:之前先要將對應NSString對象轉化為NSData數據 - (BOOL)rekey:(NSString*)key { NSData *keyData = [NSData dataWithBytes:(void *)[key UTF8String] length:(NSUInteger)strlen([key UTF8String])]; return [self rekeyWithData:keyData]; } - (BOOL)rekeyWithData:(NSData *)keyData { #ifdef SQLITE_HAS_CODEC if (!keyData) { return NO; } int rc = sqlite3_rekey(_db, [keyData bytes], (int)[keyData length]); if (rc != SQLITE_OK) { NSLog(@"error on rekey: %d", rc); NSLog(@"%@", [self lastErrorMessage]); } return (rc == SQLITE_OK); #else #pragma unused(keyData) return NO; #endif } - (BOOL)setKey:(NSString*)key { NSData *keyData = [NSData dataWithBytes:[key UTF8String] length:(NSUInteger)strlen([key UTF8String])]; return [self setKeyWithData:keyData]; } - (BOOL)setKeyWithData:(NSData *)keyData { #ifdef SQLITE_HAS_CODEC if (!keyData) { return NO; } int rc = sqlite3_key(_db, [keyData bytes], (int)[keyData length]); return (rc == SQLITE_OK); #else #pragma unused(keyData) return NO; #endif }