iOS數(shù)據(jù)庫技術進階

iOS數(shù)據(jù)庫技術進階
http://www.reibang.com/p/217a769f184e

數(shù)據(jù)庫的技術選型一直是個令人頭痛的問題,之前很長一段時間我都是使用的FMDB,做一些簡單的封裝木羹。有使用過synchronized同步甥郑,也有用FMDB的DB Queue總之,差強人意彤恶。

缺點

FMDB只是將SQLite的C接口封裝成了ObjC接口诫硕,沒有做太多別的優(yōu)化坦辟,即所謂的膠水代碼(Glue Code)。使用過程需要用大量的代碼拼接SQL章办、拼裝Object锉走,每一個表都要寫一堆增刪改查,并不方便藕届。

數(shù)據(jù)庫版本升級不友好挪蹭,特別是第一次安裝時會把所有的數(shù)據(jù)庫升級代碼都跑一遍。

優(yōu)化

1.自動建表建庫:開發(fā)者可以便捷地定義數(shù)據(jù)庫表和索引休偶,并且無須寫一坨膠水代碼拼裝對象梁厉。

2.開發(fā)者無須拼接字符串,即可完成SQL的條件踏兜、排序词顾、過濾、更新等等語句碱妆。

3.多線程高并發(fā):基本的增刪查改等接口都支持多線程訪問肉盹,開發(fā)者無需操心線程安全問題

4.多級緩存,表數(shù)據(jù)疹尾,db信息上忍,隊列等等,都會緩存航棱。

以上說的幾點我會用微信讀書開源的GYDatacenter為例睡雇,在下面詳細介紹其原理和邏輯。

更加深入的優(yōu)化:
加密:WCDB提供基于SQLCipher的數(shù)據(jù)庫加密饮醇。
損壞修復: WCDB內建了Repair Kit用于修復損壞的數(shù)據(jù)庫。
反注入: WCDB內建了對SQL注入的保護
騰訊前一陣開源的微信數(shù)據(jù)庫WCDB包括了上面的所有優(yōu)化
我以前分享的SQLite大量數(shù)據(jù)表的優(yōu)化

如果你對ObjC的Runtime不是很了解秕豫,建議你先閱讀我以前寫的:RunTime理解與實戰(zhàn)之后再閱讀下面的內容

GYDatacenter

1.可以用pod或者Carthage集成GYDatacenter朴艰。然后使你的模型類繼承GYModelObject:

@interface Employee : GYModelObject
@property (nonatomic, readonly, assign) NSInteger employeeId;
@property (nonatomic, readonly, strong) NSString *name;
@property (nonatomic, readonly, strong) NSDate *dateOfBirth;
@property (nonatomic, readonly, strong) Department *department;
@end

2.重寫父類的方法

+ (NSString *)dbName {
    return @"GYDataCenterTests";
}

+ (NSString *)tableName {
    return @"Employee";
}

+ (NSString *)primaryKey {
    return @"employeeId";
}

+ (NSArray *)persistentProperties {
    static NSArray *properties = nil;
    if (!properties) {
        properties = @[
                       @"employeeId",
                       @"name",
                       @"dateOfBirth",
                       @"department"
                       ];
    });
    return properties;
}

3.然后你就可以像下面這樣保存和查詢model類的數(shù)據(jù)了:

Employee *employee = ...
[employee save];

employee = [Employee objectForId:@1];
NSArray *employees = [Employee objectsWhere:@"WHERE employeeId < ? 
ORDER BY employeeId" arguments:@[ @10 ]];

model的核心是實現(xiàn)GYModelObjectProtocol協(xié)議

#import <Foundation/Foundation.h>

typedef NS_ENUM(NSUInteger, GYCacheLevel) {
    GYCacheLevelNoCache,   //不緩存
    GYCacheLevelDefault,    //內存不足優(yōu)先清理
    GYCacheLevelResident  //常駐
};

@protocol GYModelObjectProtocol <NSObject>

@property (nonatomic, getter=isCacheHit, readonly) BOOL cacheHit;
@property (nonatomic, getter=isFault, readonly) BOOL fault;
@property (nonatomic, getter=isSaving, readonly) BOOL saving;
@property (nonatomic, getter=isDeleted, readonly) BOOL deleted;

+ (NSString *)dbName;     //db名
+ (NSString *)tableName;  //表名
+ (NSString *)primaryKey; //主鍵
+ (NSArray *)persistentProperties; //入表的列

+ (NSDictionary *)propertyTypes;   //屬性類型
+ (NSDictionary *)propertyClasses; //屬性所在的類
+ (NSSet *)relationshipProperties; //關聯(lián)屬性

+ (GYCacheLevel)cacheLevel; //緩存級別

+ (NSString *)fts; //虛表

@optional
+ (NSArray *)indices; //索引
+ (NSDictionary *)defaultValues; //列 默認值

+ (NSString *)tokenize; //虛表令牌

@end

自動建表建庫观蓄,更新表字段

只要你定義好了你的模型類的屬性,當你準備對表數(shù)據(jù)進行增刪改查操作的時候祠墅,你不需要手動的去創(chuàng)建表和數(shù)據(jù)庫侮穿,你應該調用統(tǒng)一的建表接口。
如果表已經(jīng)建好了毁嗦,當你增加表字段(persistent properties)亲茅,GYDataCenter也會自動給你更新。我畫了一個圖:

注意:??However, GYDataCenter CANNOT delete or rename an existing column. If you plan to do so, you need to create a new table and migrate the data yourself.
GYDataCenter不能刪除和重命名已經(jīng)存在的列狗准,如果你一定要這么做克锣,需要重新建一張表并migrate數(shù)據(jù)。

GYDataCenter還可以自動的管理索引(indices)腔长,緩存(cacheLevel)袭祟,事務等等。

以查詢?yōu)槔?/p>

NSArray *employees = [Employee objectsWhere:@"WHERE employeeId < 
? ORDER BY employeeId"  arguments:@[ @10 ]];

不需要為每一個表去寫查詢接口捞附,只要在參數(shù)中寫好條件即可巾乳。開發(fā)者無須拼接字符串,即可完成SQL的條件鸟召、排序胆绊、過濾、更新等等語句欧募。

思考

NSArray *array = [DeptUser objectsWhere:
@",userinfo where deptUsers.uid = userinfo.uid" arguments:nil];

如果參試這樣寫聯(lián)合查詢辑舷,會產(chǎn)生一個錯誤!

原因是:查詢結果FMResultSet是按index解析通過KVC賦值modelClass的屬性槽片。
你可以通過指定arguments或者調用指定的聯(lián)合查詢接口joinObjectsWithLeftProperties來解決這個問題

下面是我在其源碼中寫了一個按字段解析方式來驗證這個問題何缓,有興趣的可以看一下。

- (void)setProperty:(NSString *)property ofObject:object withResultSet:(FMResultSet *)resultSet{
    Class<GYModelObjectProtocol> modelClass = [object class];
    GYPropertyType propertyType = [[[modelClass propertyTypes] objectForKey:property] unsignedIntegerValue];
    Class propertyClass;
    if (propertyType == GYPropertyTypeRelationship) {
        propertyClass = [[modelClass propertyClasses] objectForKey:property];
        propertyType = [[[propertyClass propertyTypes] objectForKey:[propertyClass primaryKey]] unsignedIntegerValue];
    }

    id value = nil;
    if (![self needSerializationForType:propertyType]) {
        if (propertyType == GYPropertyTypeDate) {
            value = [resultSet dateForColumn:property];
        } else {
            value = [resultSet objectForColumnName:property];
        }
    } else {
        NSData *data = [resultSet dataForColumn:property];
        if (data.length) {
            if (propertyType == GYPropertyTypeTransformable) {
                Class propertyClass = [[modelClass propertyClasses] objectForKey:property];
                value = [propertyClass reverseTransformedValue:data];
            } else {
                value = [self valueAfterDecodingData:data];
            }
            if (!value) {
                NSAssert(NO, @"database=%@, table=%@, property=%@", [modelClass dbName], [modelClass tableName], property);
            }
        }
    }
    if ([value isKindOfClass:[NSNull class]]) {
        value = nil;
    }

    if (propertyClass) {
        id cache = [_cacheDelegate objectOfClass:propertyClass id:value];
        if (!cache) {
            cache = [[(Class)propertyClass alloc] init];
            [cache setValue:value forKey:[propertyClass primaryKey]];
            [cache setValue:@YES forKey:@"fault"];
            [_cacheDelegate cacheObject:cache];
        }
        value = cache;
    }
    if (value) {
        [object setValue:value forKey:property];
    }
}

多線程高并發(fā)

多線程同步FMDB已經(jīng)給我們提供了FMDatabaseQueue还栓,我們可以進一步封裝為同步和異步 碌廓。

- (void)asyncInDatabase:(void (^)(FMDatabase *db))block {
    FMDatabaseQueue *currentSyncQueue = (__bridge id)dispatch_get_specific(kDatabaseQueueSpecificKey);

    FMDBRetain(self);

    dispatch_block_t task = ^() {

        FMDatabase *db = [self database];
        block(db);

        if ([db hasOpenResultSets]) {
            NSLog(@"Warning: there is at least one open result set around after performing [FMDatabaseQueue asyncInDatabase:]");

#ifdef DEBUG
            NSSet *openSetCopy = FMDBReturnAutoreleased([[db valueForKey:@"_openResultSets"] copy]);
            for (NSValue *rsInWrappedInATastyValueMeal in openSetCopy) {
                FMResultSet *rs = (FMResultSet *)[rsInWrappedInATastyValueMeal pointerValue];
                NSLog(@"query: '%@'", [rs query]);
            }
#endif
        }
    };

    if (currentSyncQueue == self) {
        task();
    } else {
        dispatch_async(_queue, task);
    }

    FMDBRelease(self);
}

- (void)syncInDatabase:(void (^)(FMDatabase *db))block {
    FMDatabaseQueue *currentSyncQueue = (__bridge id)dispatch_get_specific(kDatabaseQueueSpecificKey);

    FMDBRetain(self);

    dispatch_block_t task = ^() {

        FMDatabase *db = [self database];
        block(db);

        if ([db hasOpenResultSets]) {
            NSLog(@"Warning: there is at least one open result set around after performing [FMDatabaseQueue syncInDatabase:]");

#ifdef DEBUG
            NSSet *openSetCopy = FMDBReturnAutoreleased([[db valueForKey:@"_openResultSets"] copy]);
            for (NSValue *rsInWrappedInATastyValueMeal in openSetCopy) {
                FMResultSet *rs = (FMResultSet *)[rsInWrappedInATastyValueMeal pointerValue];
                NSLog(@"query: '%@'", [rs query]);
            }
#endif
        }
    };

    if (currentSyncQueue == self) {
        task();
    } else {
        dispatch_sync(_queue, task);
    }

    FMDBRelease(self);
}

FMDatabaseQueue已經(jīng)幫你解決了線程同步死鎖問題,并且并行隊列剩盒,可以高并發(fā)谷婆。微信讀書的開發(fā)團體之前也說過暫時并沒有遇到隊列瓶頸,相信FMDatabaseQueue還是值得信賴的辽聊。
dispatch_sync(queue,task) 會阻塞當前線程, 直到queue完成了你給的task纪挎。

緩存

多級緩存:我們應該在數(shù)據(jù)表(model),數(shù)據(jù)庫信息跟匆,dbQueue异袄,等建立多級緩存。model用字典緩存玛臂,其他可以用dispatch_get_specific或者運行時objc_setAssociatedObject關聯(lián)烤蜕。
當然封孙,緩存必須是可控的。有一個全局控制開關和清除機制讽营,我們查詢時虎忌,如果model有緩存就取緩存,沒有就讀DB并添加緩存橱鹏。并且在更新表數(shù)據(jù)或者刪除時膜蠢,更新緩存。

注意?? 默認屬性都是readonly莉兰,因為每一個數(shù)據(jù)表都會有一個緩存挑围。如果你需要改變model的屬性值,你必須加同步鎖??贮勃√叭牵或者像下面這樣:

@interface Employee : GYModelObject
@property (atomic, assign) NSInteger employeeId;
@property (atomic, strong) NSString *name;
@property (atomic, strong) NSDate *dateOfBirth;
@property (atomic, strong) Department *department;
@end

Relationship & Faulting

像前面的Employee類,Department類是它的一個屬性寂嘉,這叫做Relationship(關聯(lián))奏瞬。需要在.m文件中動態(tài)實現(xiàn)

@dynamic department;
//use
[employee save];
[employee.department save];

當你查詢Employee時,Department屬性僅僅是用Department表的主鍵當占位符泉孩。當你employee.department.xx訪問department的屬性時硼端,會自動動態(tài)的加載department的信息,這叫做Faulting寓搬。減少您的應用程序使用的內存數(shù)量,提高查詢速度珍昨。

有興趣的可以研究騰訊前一陣開源的微信數(shù)據(jù)庫WCDB,或者等我下一次分享句喷。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末镣典,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子唾琼,更是在濱河造成了極大的恐慌兄春,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件锡溯,死亡現(xiàn)場離奇詭異赶舆,居然都是意外死亡,警方通過查閱死者的電腦和手機祭饭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進店門芜茵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人倡蝙,你說我怎么就攤上這事九串。” “怎么了悠咱?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵蒸辆,是天一觀的道長征炼。 經(jīng)常有香客問我析既,道長躬贡,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任眼坏,我火速辦了婚禮拂玻,結果婚禮上,老公的妹妹穿的比我還像新娘宰译。我一直安慰自己檐蚜,他們只是感情好,可當我...
    茶點故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布沿侈。 她就那樣靜靜地躺著闯第,像睡著了一般。 火紅的嫁衣襯著肌膚如雪缀拭。 梳的紋絲不亂的頭發(fā)上咳短,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天,我揣著相機與錄音蛛淋,去河邊找鬼咙好。 笑死,一個胖子當著我的面吹牛褐荷,可吹牛的內容都是我干的勾效。 我是一名探鬼主播,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼叛甫,長吁一口氣:“原來是場噩夢啊……” “哼层宫!你這毒婦竟也來了?” 一聲冷哼從身側響起其监,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤萌腿,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后棠赛,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體哮奇,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年睛约,在試婚紗的時候發(fā)現(xiàn)自己被綠了鼎俘。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡辩涝,死狀恐怖贸伐,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情怔揩,我是刑警寧澤捉邢,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布脯丝,位于F島的核電站,受9級特大地震影響伏伐,放射性物質發(fā)生泄漏宠进。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一藐翎、第九天 我趴在偏房一處隱蔽的房頂上張望材蹬。 院中可真熱鬧,春花似錦吝镣、人聲如沸堤器。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽闸溃。三九已至,卻和暖如春拱撵,著一層夾襖步出監(jiān)牢的瞬間辉川,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工裕膀, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留员串,地道東北人。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓昼扛,卻偏偏與公主長得像寸齐,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子抄谐,可洞房花燭夜當晚...
    茶點故事閱讀 45,044評論 2 355

推薦閱讀更多精彩內容

  • 數(shù)據(jù)庫的技術選型一直是個令人頭痛的問題渺鹦,之前很長一段時間我都是使用的FMDB,做一些簡單的封裝蛹含。有使用過synch...
    wu大維閱讀 3,624評論 9 55
  • 概論 所謂的持久化毅厚,就是將數(shù)據(jù)保存到硬盤中,使得在應用程序或機器重啟后可以繼續(xù)訪問之前保存的數(shù)據(jù)浦箱。在iOS開發(fā)中吸耿,...
    Leeson1989閱讀 1,916評論 4 1
  • 今天七號,長假馬上就要結束了酷窥。 安靜的假期里咽安,回頭一看,做了好多事兒…… > 1.新媒體運營在線課程學習蓬推,累計時間...
    殷春燕閱讀 222評論 0 0
  • RACCommand RACCommand 屬性與方法
    李瀟南閱讀 538評論 0 0
  • 樹下躺著一直鯨 人問妆棒,你從哪來 它說,來自那片金色的海洋 人問,那金色多令人神往糕珊,你又怎舍得那個地方 它說动分,你不知...
    沒有哪座不能逾越的山閱讀 122評論 0 0