數(shù)據(jù)庫的技術(shù)選型一直是個令人頭痛的問題票腰,之前很長一段時間我都是使用的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ù)庫加密。
損壞修復(fù): WCDB內(nèi)建了Repair Kit用于修復(fù)損壞的數(shù)據(jù)庫列赎。
反注入: WCDB內(nèi)建了對SQL注入的保護
騰訊前一陣開源的微信數(shù)據(jù)庫WCDB包括了上面的所有優(yōu)化
我以前分享的SQLite大量數(shù)據(jù)表的優(yōu)化
如果你對ObjC的Runtime不是很了解宏悦,建議你先閱讀我以前寫的:RunTime理解與實戰(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.重寫父類的方法
+ (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, //內(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; //緩存級別
+ (NSString *)fts; //虛表
@optional
+ (NSArray *)indices; //索引
+ (NSDictionary *)defaultValues; //列 默認值
+ (NSString *)tokenize; //虛表令牌
@end
自動建表建庫包吝,更新表字段
只要你定義好了你的模型類的屬性饼煞,當你準備對表數(shù)據(jù)進行增刪改查操作的時候,你不需要手動的去創(chuàng)建表和數(shù)據(jù)庫诗越,你應(yīng)該調(diào)用統(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)薇搁,事務(wù)等等斋扰。
以查詢?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)生一個錯誤僻澎!
原因是:查詢結(jié)果FMResultSet是按index解析通過KVC賦值modelClass的屬性。
你可以通過指定arguments或者調(diào)用指定的聯(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。
緩存
多級緩存:我們應(yīng)該在數(shù)據(jù)表(model)攘滩,數(shù)據(jù)庫信息帅刊,dbQueue,等建立多級緩存漂问。model用字典緩存赖瞒,其他可以用dispatch_get_specific或者運行時objc_setAssociatedObject關(guān)聯(lián)。
當然蚤假,緩存必須是可控的栏饮。有一個全局控制開關(guā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(關(guān)聯(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伤哺。減少您的應(yīng)用程序使用的內(nèi)存數(shù)量,提高查詢速度燕侠。
有興趣的可以研究騰訊前一陣開源的微信數(shù)據(jù)庫WCDB,或者等我下一次分享立莉。