1. 程式人生 > >FMDB使用,事務,多執行緒,加密等比較全面的用法。

FMDB使用,事務,多執行緒,加密等比較全面的用法。

一:簡單使用

既然FMBD是對資料庫的封裝,那基本功能應該包括 增、刪、改、查。

主要使用到的類為FMDatabase(資料庫)FMResultSet(查詢的結果)。

其中增、刪、改,三個方法都是使用FMDatabase類的executeUpdate方法也就是都算作資料的更新,

而查使用的是FMDatabase類的executeQuery方法查詢結果為FMResultSet,但在這還是將增,刪,改都單獨提了方法,方便區分。

一般使用時都會建立一個管理類對這些方法進行統一管理,需要注意的是該類應為單利,用來保證操作的是同一個資料庫。

這裡採用CGD方式建立了單利:建立DataManager

類繼承NSObject

在.h檔案中:

+ (id)shareDataManager;

宣告一個獲取單利物件的方法

在.m檔案中:

#import "DataManager.h"

static DataManager * manager;

@interface DataManager ()
@property(nonatomic, strong)FMDatabase * db;
@end

@implementation DataManager

+ (id)shareDataManager{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        manager = [[DataManager alloc] init];
    });
    return manager;
}

+ (id)allocWithZone:(struct _NSZone *)zone{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        manager = [super allocWithZone:zone];
    });
    return manager;
}

實現獲取單利物件的方法,並且聲明瞭一個FMDatabase的成員變數。

接下來就是對FMDatabase的操作:

(1)建立資料庫

DataManager.h檔案中宣告方法

//建立資料庫
- (BOOL)buildDatabase;

DataManager.m檔案中實現該方法

#pragma mark - 建立資料庫
- (BOOL)buildDatabase{
    NSString * docuPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
    NSString * dbPath = [docuPath stringByAppendingPathComponent:@"database.db"];
    self.db = [FMDatabase databaseWithPath:dbPath];
    BOOL openLFlag = [self.db open];
    if (!openLFlag) {
        NSLog(@"資料庫建立/開啟失敗");
        return NO;
    }  
}

前兩行程式碼是設定資料庫路徑及資料庫名稱,可用xxx.db和xxx.sqlite兩種格式 

[self.db open]是建立資料庫,如果資料庫已經存在則開啟。

(2)建立表格

一般來講我們會把建立表格的步驟放在建立資料庫裡資料庫開啟成功功能之後

 NSString * sql = @"create table if not exists person ('ID' INTEGER PRIMARY KEY AUTOINCREMENT,'name' TEXT NOT NULL,'phone' TEXT NOT NULL,'score' INTGEGER NOT NULL)";
    BOOL result = [self.db executeUpdate:sql];
    if (result) {
        NSLog(@"表格建立成功");
        return YES;
    }else{
        NSLog(@"表格建立失敗");
    }
    return NO;

表示建立一個person表格裡面包含四個欄位,ID為主鍵,int型別型資料,插入資料時主鍵如果不傳入則按照上一個的值自動➕1

(3)增

DataManager.h檔案中增加方法

//插入資料
- (BOOL)insertWithSql:(NSString *)sql;

DataManager.m檔案中實現

#pragma mark - 插入資料
- (BOOL)insertWithSql:(NSString *)sql{
    BOOL result = [self.db executeUpdate:sql];
    if (result) {
        NSLog(@"插入表格成功");
        return YES;
    }else{
        NSLog(@"插入表格失敗");
    }
    return NO;
}

如上面所說使用executeUpdate方法實現資料插入。

ViewController.m中呼叫:

#pragma mark - 插入資料
- (void)insert{
    Person * person = [[Person alloc] init];
    person.ID = 5;
    person.name = @"小明";
    person.phone = @"13232432435";
    person.score = 99;
    //傳入主鍵ID
    NSString * insertSql1 = [NSString stringWithFormat:@"insert into 'person'(ID,name,phone,score) values (%d,'%@','%@',%d)",person.ID,person.name,person.phone,person.score];
    [[DataManager shareDataManager] insertWithSql:insertSql1];
    //未傳主鍵ID
    NSString * insertSql = [NSString stringWithFormat:@"insert into 'person'(name,phone,score) values ('%@','%@',%d)",person.name,person.phone,person.score];
    [[DataManager shareDataManager] insertWithSql:insertSql];
}

這個時候可以根據資料庫地址在Finder前往(command+shift+g)資料夾,使用Navicat Premium或其它軟體開啟資料庫會發現裡

面有個person表格,表格下有兩條資料

以上是比較常用的一種方法還有另外三種:

     1.
     BOOL result = [self.db executeUpdate:@"insert into 'person' (ID,namme,phone,score)values(?,?,?,?)",@100,@"小明",@"13023848239",89];
     2.
     BOOL result = [self.db executeUpdate:@"insert into 'person' (ID,name.phone,score) values(%ld,%@,%@,%ld)",112,@"小明",@"1324097923748",90];
     3.
     BOOL result = [self.db executeUpdate:@"insert into 'person' (ID,name,phone,score) values(?,?,?,?)" withArgumentsInArray:@[@111,@"小明",@"213798784",@90]];

 

(4)刪

DataManager.h中新增方法

//刪除資料
- (BOOL)deleteWithSql:(NSString *)sql;

DataManager.m中實現方法其實和插入的實現方法一樣

#pragma mark - 刪除資料
- (BOOL)deleteWithSql:(NSString *)sql{
    BOOL result = [self.db executeUpdate:sql];
    if (result) {
        NSLog(@"刪除資料成功");
        return YES;
    }else{
        NSLog(@"刪除資料失敗");
    }
    return NO;
}

ViewController.m中呼叫:

#pragma mark - 刪除資料
- (void)delete{
    NSString * deleteSql = [NSString stringWithFormat:@"delete from 'person' where ID = %d",5];
    [[DataManager shareDataManager] deleteWithSql:deleteSql];
}

以上方法將會刪除person表內ID為5的一條資料,呼叫後重新整理資料庫,會發現只剩ID為6的一條資料

刪除的語句也有另外三種寫法:

     1、
     BOOL result = [self.db executeUpdate:@"delete from 'person' where ID = ?",@5];
     2、
     BOOL result = [self.db executeUpdate:@"delete from 'person' where ID = %d",5];
     3、
     BOOL result = [self.db executeUpdate:@"delete from 'person' where ID = ?" withArgumentsInArray:@[@5]];

(5)改

DataManager.h中新增方法

//更新資料
- (BOOL)updateWithSql:(NSString *)sql;

DataManager.m中實現方法

#pragma mark - 更新資料
- (BOOL)updateWithSql:(NSString *)sql{
    BOOL result = [self.db executeUpdate:sql];
    if (result) {
        NSLog(@"資料更新成功");
        return YES;
    }else{
        NSLog(@"資料更新失敗");
    }
    return NO;
}

ViewController.m中呼叫:

#pragma mark - 更新資料
- (void)update{
    NSString * updateSql = [NSString stringWithFormat:@"update 'person' set score = 100 where name = 'Shan'"];
    [[DataManager shareDataManager] updateWithSql:updateSql];
}

再執行更新方法之前,可以試著往裡面新插入一條nameShan的資料,設定score90

然後執行該語句,會發現nameShan的那條資料的score值變為了100

(6)查

這個時候就用到了FMResultSet這個類可以檢視該類的.h檔案發現裡面有很多方法,而我們常用的

- (BOOL)next;

- (int)intForColumnIndex:(int)columnIdx;

- (NSString * _Nullable)stringForColumn:(NSString*)columnName;

等等的

其中第一個方法類似於遍歷,而剩下的常用方法基本都為取值,會在應用中見到用法。

還是一樣在在DataManager.h中宣告方法

//查詢資料
- (FMResultSet * )selectWithSql:(NSString *)sql;

此函式帶了一個返回值也就是將查詢結果返回

DataManager.m中實現方法

#pragma mark - 查詢資料
- (FMResultSet * )selectWithSql:(NSString *)sql{
    [self.db open];
    FMResultSet * result = [self.db executeQuery:sql];
    if (result) {
        NSLog(@"查詢成功");
        return result;
    }else{
        NSLog(@"沒有查詢到資料");
    }
    return nil;
}

ViewController.m中呼叫:

#pragma mark - 查詢
- (void)select{
    NSString * selectSql = [NSString stringWithFormat:@"select * from 'person' where ID = 5"];
    FMResultSet * result = [[DataManager shareDataManager] selectWithSql:selectSql];
    NSMutableArray * arrayM = @[].mutableCopy;
    while ([result next]) {
        Person * person = [[Person alloc] init];
        person.ID = [result intForColumn:@"ID"];
        person.name = [result stringForColumn:@"name"];
        person.phone = [result stringForColumn:@"phone"];
        person.score = [result intForColumn:@"score"];
        [arrayM addObject:person];
    }
    NSLog(@"%@",arrayM);
}

表示在person表中查詢ID為5的資料,因為ID為主鍵,所以這裡最多隻會查詢到一條資料,

但如果sql語句這樣寫:@"select * from 'person' where name = '小明'"則會返回所有name小明的資料,

如果想要查詢該表內的全部資料使用 @"select * from 'person'"

還可以查詢同時滿足多個條件的資料@"select * from 'person' where ID = 5 and name = '小明'"

另外還有模糊查詢,集合查詢,去重查詢等等。

二.事務(Transaction)

首先事務有4大特性,原子性,一致性,隔離性,永續性。反正大家都這麼說,至於原因還是需要從程式碼當中進行驗證。

另外事務應用的場景,應該是有大批量資料需要進行插入或修改的時候。

下面寫兩個方法想表格中插入500條數,其中一個方法使用事務,而另一個就使用普通的插入語句。

同樣還是寫在了DataManager類中

.h中宣告兩個方法

//再事務中處理事情
- (void)handleTransaction;

//未在事務中處理
- (void)handleNoTransaction;

.m中實現

#pragma mark - 用事務處理一系列資料庫操作
- (void)handleTransaction{
    NSString * documentPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
    NSString * dbPath = [documentPath stringByAppendingPathComponent:@"text1.db"];
    FMDatabase * db = [FMDatabase databaseWithPath:dbPath];
    if (![db open]) {
        return;
    }
    BOOL result = [db executeUpdate:@"create table if not exists text1 ('ID' integer primary key autoincrement,name text,age integer)"];
    if (!result) {
        NSLog(@"建立表格失敗");
    }
    [db beginTransaction];
    NSDate * begin = [NSDate date];
    BOOL rollBack = NO;
    @try {
        for (int i = 0; i < 500; i ++) {
            NSString * name = [NSString stringWithFormat:@"text_%d",i];
            NSInteger age = i + 10;
            NSInteger ID = i;
            NSString * insertSql = [NSString stringWithFormat:@"insert into 'text1' (ID,name,age) values(%ld,'%@',%ld)",ID,name,age];
            BOOL result = [db executeUpdate:insertSql];
            if (!result) {
                NSLog(@"插入失敗");
                rollBack = YES;
                return;
            }
        }
    } @catch (NSException *exception) {
        rollBack = YES;
    } @finally {
        if (!rollBack) {
            [db commit];
        }else{
            [db rollback];
        }
    }
    NSDate * end = [NSDate date];
    NSTimeInterval time = [end timeIntervalSinceDate:begin];
    NSLog(@"在事務中執行插入任務所需時間 === %f",time);
}

#pragma mark - 未使用事務執行一系列操作
- (void)handleNoTransaction{
    NSString * ducumentPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
    NSString * dbPath = [ducumentPath stringByAppendingPathComponent:@"test2.db"];
    FMDatabase * db = [FMDatabase databaseWithPath:dbPath];
    if (![db open]) {
        return;
    }
    BOOL result = [db executeUpdate:@"create table if not exists text2('ID' integer primary key autoincrement,name text,age integer)"];
    if (!result) {
        [db close];
    }
    NSDate * begin = [NSDate date];
    for (int i = 0; i < 500; i ++) {
        NSString * name = [NSString stringWithFormat:@"text_%d",i];
        NSInteger age = i + 10;
        NSInteger ID = i;
        NSString * insertSql = [NSString stringWithFormat:@"insert into 'text2' (ID,name,age) values(%ld,'%@',%ld)",ID,name,age];
        BOOL result = [db executeUpdate:insertSql];
        if (!result) {
            NSLog(@"插入失敗");
            return;
        }
    }
    NSDate * end = [NSDate date];
    NSTimeInterval  time = [end timeIntervalSinceDate:begin];
    NSLog(@"不在事務中執行插入任務所需時間===%f",time);
}

ViewController.m中呼叫

#pragma mark - 在事務中執行
- (void)handleTransaction{
    [[DataManager shareDataManager] handleTransaction];
}

#pragma mark - 不在事務中執行
- (void)handleNoTransaction{
    [[DataManager shareDataManager] handleNoTransaction];
}

分別呼叫兩個方法會列印如下資訊

可以看出如果兩個語句都執行成功的話,在事務中執行的語句所花費的時間要遠小於不在事務中執行。

(注意不要在語句直接加上過多的NSLog該語句會花費大量時間影響測試結果)

而且開啟兩個資料庫後會發現兩個表的資料都是按順序插入到對應的表中的。

如果在事務的方法中

NSString * insertSql = [NSString stringWithFormat:@"insert into 'text1' (ID,name,age) values(%ld,'%@',%ld)",ID,name,age];

這句程式碼之下插入如下程式碼:

 if (i == 200 ) {
                insertSql = [NSString stringWithFormat:@"insert into 'text1' (ID,name,age) values(%d,'%@',%ld)",1,name,age];
            }

因為ID為主鍵不能重複,所以該句程式碼插入時一定會失敗,執行完開啟資料庫後會發現text1表裡面一條資料也沒有,這就說明了事務的原子性,一個成功則都成功,一個失敗則都失敗。

三.多執行緒

如果對資料的操作都在同一執行緒則上面的FMDatabase就可以滿足需求,但是如果在不同執行緒下操作資料庫,那麼FMDatabase的缺陷就會體現出來了,接下來進行一下測試

DataManager.h中新增如下方法

//多執行緒不使用FMDatabaseQueue
- (void)buildDatabaseNotWithQueue;

DataManager.m中實現

- (void)buildDatabaseNotWithQueue{
    NSString * docuPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
    NSString * path = [docuPath stringByAppendingPathComponent:@"student1.sqlite"];
    NSLog(@"%@",path);
    FMDatabase * db = [FMDatabase databaseWithPath:path];
    dispatch_queue_t queuet1 = dispatch_queue_create("queuet1", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queuet1, ^{
        NSLog(@"%@",[NSThread currentThread]);
        for (int i = 0; i < 50; i ++) {
            if ([db open]) {
                BOOL result = [db executeUpdate:@"create table if not exists student (ID integer primary key autoincrement,name text not null,age integer not null);"];
                if (result) {
                    NSString * insertSql1 = [NSString stringWithFormat:@"insert into student (ID,name,age) values (%d,'洋洋——%d',%d)",i+100,i,i];
                    BOOL res = [db executeUpdate:insertSql1];
                    if (!res) {
                        NSLog(@"1插入失敗 ----%d",i);
                    }
                }
            }
        }
    });
    
    dispatch_async(queuet1, ^{
        NSLog(@"%@",[NSThread currentThread]);
        for (int i = 0; i < 50;i ++) {
            if ([db open]) {
                BOOL result = [db executeUpdate:@"create table if not exists student (ID integer primary key autoincrement,name text not null,age integer not null);"];
                if (result) {
                    NSString * insertSql2 = [NSString stringWithFormat:@"insert into student (ID,name,age) values (%d,'靜靜——%d',%d)",i,i,i];
                    BOOL res = [db executeUpdate:insertSql2];
                    if (!res) {
                        NSLog(@"2插入失敗---%d",i);
                    }
                }
            }
        }
    });
}

ViewController.m中呼叫

#pragma mark - 不使用多執行緒
- (void)buildDBNotWithFMDatabaseQueue{
    [[DataManager shareDataManager] buildDatabaseNotWithQueue];
}

會發現列印臺會列印如下資料

再開啟資料庫的student

發現插入的資料遠遠不夠100條,說明大多數資料都插入失敗了。

所以說FMDatabase不能保證執行緒安全,這個時候就需要使用FMDatabaseQueue了。

同樣在DataManager.h中新增方法

//多執行緒處理
- (void)buildDatabaseWithQueue;

DataManager.m中實現

#pragma mark - 多執行緒保證執行緒安全FMDatabaseQueue
- (void)buildDatabaseWithQueue{
    NSDate * begin = [NSDate date];
    NSString * docuPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
    NSString * path = [docuPath stringByAppendingPathComponent:@"student.sqlite"];
    NSLog(@"%@",path);
    FMDatabaseQueue * queue = [FMDatabaseQueue databaseQueueWithPath:path];
    dispatch_queue_t queuet1 = dispatch_queue_create("queuet1", DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_t group = dispatch_group_create();
    dispatch_group_async(group, queuet1, ^{
        NSLog(@"%@",[NSThread currentThread]);
        for (int i = 0; i < 50; i ++) {
            [queue inDatabase:^(FMDatabase * _Nonnull db) {
                if ([db open]) {
                    BOOL result = [db executeUpdate:@"create table if not exists student (ID integer primary key autoincrement,name text not null,age inteter not null);"];
                    if (result) {
                        NSString * insertSql1 = [NSString stringWithFormat:@"insert into student (name,age) values ('洋洋_%d',25%d)",i,i];
                        BOOL res = [db executeUpdate:insertSql1];
                        if (!res) {
                            NSLog(@"插入失敗 ------------%d",i);
                        }
                    }
                }
            }];
        }
    });
    
    dispatch_group_async(group, queuet1, ^{
        NSLog(@"%@",[NSThread currentThread]);
        for (int i = 0; i < 50; i ++) {
            [queue inDatabase:^(FMDatabase * _Nonnull db) {
                if ([db open]) {
                    BOOL result = [db executeUpdate:@"create table if not exists student (ID integer primary key autoincrement,name text not null,age integer not null);"];
                    if (result) {
                        NSString * insertSql2 = [NSString stringWithFormat:@"insert into student (name,age) values ('靜靜_%d',25%d)",i,i];
                        BOOL res = [db executeUpdate:insertSql2];
                        if (!res) {
                            NSLog(@"2插入失敗 ------------%d",i);
                        }
                    }
                }
            }];
        }
    });
    dispatch_group_notify(group, queuet1, ^{
        NSDate * end = [NSDate date];
        NSTimeInterval time = [end timeIntervalSinceDate:begin];
        NSLog(@"在多執行緒執行插入100條用時%f",time);
    });
}

執行完成後檢視資料庫student

注意這裡並沒有傳入主鍵ID而是讓其自動增長的,根據插入的先後順序。

根據表格發現數據是交替的,說明兩個執行緒同時開始進行,並且都將資料全部插入表格,

既然是兩個執行緒同時進行,時間上應該也會小於單執行緒插入100條資料所花費的時間。

這裡只是一個例子,同樣可以將FMDatabaseQueue也參照FMDatabase封裝出增,刪,改,查等方法。

四:資料庫加密

這裡以FMDatabase為例,在不改動FMDatabase類的基礎上,我們需要建立一個新的類起名為EncryptDatabase繼承FMDatabase

然後定義兩個方法在.h檔案中

+ (id)databaseWithPath:(NSString *)inPath encrytKey:(NSString *)encrytKey;

- (id)initWithPath:(NSString *)path encrytKey:(NSString *)encrytKey

每個方法都有一個引數,加密字串。

.m中實現該方法,並重寫- (BOOL)open  - (BOOL)openWithFlags:(int)flags vfs:(NSString *)vfsName 方法。

.m檔案中所有程式碼

#import "EncryptDatabase.h"
@interface EncryptDatabase()
{
    NSString * _encryptKey;
}
@end

@implementation EncryptDatabase
+ (id)databaseWithPath:(NSString *)inPath encrytKey:(NSString *)encrytKey{
    return [[[self class] alloc] initWithPath:inPath encrytKey:encrytKey];
}

- (id)initWithPath:(NSString *)path encrytKey:(NSString *)encrytKey{
    if (self = [super initWithPath:path]) {
        _encryptKey = encrytKey;
    }
    return self;
}

#pragma mark - 重寫父類open方法
- (BOOL)open{
    BOOL res = [super open];
    if (res && _encryptKey) {
        [self setKey:_encryptKey];
    }
    return res;
}

- (BOOL)openWithFlags:(int)flags vfs:(NSString *)vfsName{
    BOOL res = [super openWithFlags:flags vfs:vfsName];
    if (res && _encryptKey) {
        [self setKey:_encryptKey];
    }
    return res;
}
@end

接下來使用程式碼呼叫呼叫,將EncryptDatabase.h匯入到DataManager中,在DataManager.h中宣告建立加密資料庫的方法

//建立加密資料庫
- (void)buildFMDatabaseEncrypt;

DataManager.m中實現

- (void)buildFMDatabaseEncrypt{
    NSString * docuPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
    NSString * path = [docuPath stringByAppendingPathComponent:@"encrypt_test.db"];
    NSLog(@"%@",path);
    EncryptDatabase * db = [EncryptDatabase databaseWithPath:path encrytKey:ENCRYPTKEY];
    if ([db open]) {
         BOOL result = [db executeUpdate:@"create table if not exists student (ID integer primary key autoincrement,name text not null,age integer not null);"];
        if (result) {
            for (int i = 0; i < 50; i ++) {
                NSString * insertSql1 = [NSString stringWithFormat:@"insert into student (ID,name,age) values (%d,'噹噹%d',%d)",i,i,i];
                BOOL res = [db executeUpdate:insertSql1];
                if (res) {
                    NSLog(@"插入成功 --%d",i);
                }else{
                    NSLog(@"插入失敗 ---------%d",i);
                }
            }
        }
    }
    
    FMResultSet * result = [db executeQuery:@"select * from 'student' where ID = 1"];
    if (result) {
        while ([result next]) {
            NSString * name = [result stringForColumn:@"name"];
            NSLog(@"%@",name);
        }
    }else{
        NSLog(@"查詢失敗");
    }
}

附帶了一個查詢語句

ViewController.m中呼叫

#pragma mark - 建立加密資料庫
- (void)buildEncryptDatabase{
    [[DataManager shareDataManager] buildFMDatabaseEncrypt];
}

會發現可以查詢成功並打印出name的值,接下里複製資料庫地址然後開啟會有這樣的提示

說檔案是被加密的,或者不是資料庫。這就說明資料庫加密成功。