數(shù)據(jù)庫(kù)的技術(shù)選型一直是個(gè)令人頭痛的問(wèn)題,之前很長(zhǎng)一段時(shí)間我都是使用的FMDB,做一些簡(jiǎn)單的封裝。有使用過(guò)synchronized同步,也有用FMDB的DB Queue總之,差強(qiáng)人意。
缺點(diǎn)
FMDB只是將SQLite的C接口封裝成了ObjC接口,沒(méi)有做太多別的優(yōu)化,即所謂的膠水代碼(Glue Code)。使用過(guò)程需要用大量的代碼拼接SQL、拼裝Object,每一個(gè)表都要寫(xiě)一堆增刪改查,并不方便。
數(shù)據(jù)庫(kù)版本升級(jí)不友好,特別是第一次安裝時(shí)會(huì)把所有的數(shù)據(jù)庫(kù)升級(jí)代碼都跑一遍。
優(yōu)化
1.自動(dòng)建表建庫(kù):開(kāi)發(fā)者可以便捷地定義數(shù)據(jù)庫(kù)表和索引,并且無(wú)須寫(xiě)一坨膠水代碼拼裝對(duì)象。
2.開(kāi)發(fā)者無(wú)須拼接字符串,即可完成SQL的條件、排序、過(guò)濾、更新等等語(yǔ)句。
3.多線程高并發(fā):基本的增刪查改等接口都支持多線程訪問(wèn),開(kāi)發(fā)者無(wú)需操心線程安全問(wèn)題
4.多級(jí)緩存,表數(shù)據(jù),db信息,隊(duì)列等等,都會(huì)緩存。
以上說(shuō)的幾點(diǎn)我會(huì)用微信讀書(shū)開(kāi)源的GYDatacenter為例,在下面詳細(xì)介紹其原理和邏輯。
更加深入的優(yōu)化:
加密:WCDB提供基于SQLCipher的數(shù)據(jù)庫(kù)加密。
損壞修復(fù): WCDB內(nèi)建了Repair Kit用于修復(fù)損壞的數(shù)據(jù)庫(kù)。
反注入: WCDB內(nèi)建了對(duì)SQL注入的保護(hù)
騰訊前一陣開(kāi)源的微信數(shù)據(jù)庫(kù)WCDB包括了上面的所有優(yōu)化
我以前分享的SQLite大量數(shù)據(jù)表的優(yōu)化
如果你對(duì)ObjC的Runtime不是很了解,建議你先閱讀我以前寫(xiě)的:RunTime理解與實(shí)戰(zhàn)之后再閱讀下面的內(nèi)容
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.重寫(xiě)父類的方法
+ (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的核心是實(shí)現(xiàn)GYModelObjectProtocol協(xié)議
#import <Foundation/Foundation.h>
typedef NS_ENUM(NSUInteger, GYCacheLevel) {
GYCacheLevelNoCache, //不緩存
GYCacheLevelDefault, //內(nèi)存不足優(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; //關(guān)聯(lián)屬性
+ (GYCacheLevel)cacheLevel; //緩存級(jí)別
+ (NSString *)fts; //虛表
@optional
+ (NSArray *)indices; //索引
+ (NSDictionary *)defaultValues; //列 默認(rèn)值
+ (NSString *)tokenize; //虛表令牌
@end
自動(dòng)建表建庫(kù),更新表字段
只要你定義好了你的模型類的屬性,當(dāng)你準(zhǔn)備對(duì)表數(shù)據(jù)進(jìn)行增刪改查操作的時(shí)候,你不需要手動(dòng)的去創(chuàng)建表和數(shù)據(jù)庫(kù),你應(yīng)該調(diào)用統(tǒng)一的建表接口。
如果表已經(jīng)建好了,當(dāng)你增加表字段(persistent properties),GYDataCenter也會(huì)自動(dòng)給你更新。我畫(huà)了一個(gè)圖:
注意:??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還可以自動(dòng)的管理索引(indices),緩存(cacheLevel),事務(wù)等等。
以查詢?yōu)槔?/p>
NSArray *employees = [Employee objectsWhere:@"WHERE employeeId <
? ORDER BY employeeId" arguments:@[ @10 ]];
不需要為每一個(gè)表去寫(xiě)查詢接口,只要在參數(shù)中寫(xiě)好條件即可。開(kāi)發(fā)者無(wú)須拼接字符串,即可完成SQL的條件、排序、過(guò)濾、更新等等語(yǔ)句。
思考
NSArray *array = [DeptUser objectsWhere:
@",userinfo where deptUsers.uid = userinfo.uid" arguments:nil];
如果參試這樣寫(xiě)聯(lián)合查詢,會(huì)產(chǎn)生一個(gè)錯(cuò)誤!
原因是:查詢結(jié)果FMResultSet是按index解析通過(guò)KVC賦值modelClass的屬性。
你可以通過(guò)指定arguments或者調(diào)用指定的聯(lián)合查詢接口joinObjectsWithLeftProperties來(lái)解決這個(gè)問(wèn)題
下面是我在其源碼中寫(xiě)了一個(gè)按字段解析方式來(lái)驗(yàn)證這個(gè)問(wèn)題,有興趣的可以看一下。
- (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,我們可以進(jìn)一步封裝為同步和異步 。
- (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)幫你解決了線程同步死鎖問(wèn)題,并且并行隊(duì)列,可以高并發(fā)。微信讀書(shū)的開(kāi)發(fā)團(tuán)體之前也說(shuō)過(guò)暫時(shí)并沒(méi)有遇到隊(duì)列瓶頸,相信FMDatabaseQueue還是值得信賴的。
dispatch_sync(queue,task) 會(huì)阻塞當(dāng)前線程, 直到queue完成了你給的task。
緩存
多級(jí)緩存:我們應(yīng)該在數(shù)據(jù)表(model),數(shù)據(jù)庫(kù)信息,dbQueue,等建立多級(jí)緩存。model用字典緩存,其他可以用dispatch_get_specific或者運(yùn)行時(shí)objc_setAssociatedObject關(guān)聯(lián)。
當(dāng)然,緩存必須是可控的。有一個(gè)全局控制開(kāi)關(guān)和清除機(jī)制,我們查詢時(shí),如果model有緩存就取緩存,沒(méi)有就讀DB并添加緩存。并且在更新表數(shù)據(jù)或者刪除時(shí),更新緩存。
注意?? 默認(rèn)屬性都是readonly,因?yàn)槊恳粋€(gè)數(shù)據(jù)表都會(huì)有一個(gè)緩存。如果你需要改變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類是它的一個(gè)屬性,這叫做Relationship(關(guān)聯(lián))。需要在.m文件中動(dòng)態(tài)實(shí)現(xiàn)
@dynamic department;
//use
[employee save];
[employee.department save];
當(dāng)你查詢Employee時(shí),Department屬性僅僅是用Department表的主鍵當(dāng)占位符。當(dāng)你employee.department.xx訪問(wèn)department的屬性時(shí),會(huì)自動(dòng)動(dòng)態(tài)的加載department的信息,這叫做Faulting。減少您的應(yīng)用程序使用的內(nèi)存數(shù)量,提高查詢速度。
有興趣的可以研究騰訊前一陣開(kāi)源的微信數(shù)據(jù)庫(kù)WCDB,或者等我下一次分享。