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

數(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è)圖:

自動(dòng)建庫(kù)建表.png

注意:??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ò)誤!

image.png

原因是:查詢結(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,或者等我下一次分享。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容