有關(guān)FMDB的理解以及源碼分析

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具體是怎么完成多線程的:


image.png

[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)境中的最佳實踐。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容