iOS基于runtime實現的簡單ORM

原創文章轉載請注明出處

今天一個朋友讓我完善一下簡歷,把以前做過的項目內容寫詳細一些,我說好吧。翻到12年的一個產品,那是第一次寫iOS App,當時還在iOS 4.3時代,網上的資料少得可憐。在評估數據庫方案的時候,先把在當初還不是很成熟的Core Data給排除了,那就只剩SQLite可選。當時最火的SQLite框架就是FMDB了,但是看著FMDB那又長又臭的CreateTable方法,還有臃腫的包,哥幾個決定自己動手改寫一個Persistence Manager。

講真,一直到現在我也沒有用過FMDB,自己寫的東西已經足夠應付開發需求了性能也不差,后來為了跨平臺又把數據庫改成了Realm。

言歸正傳,我們的Persistence Manager其實非常簡單,只有4個類。

Paste_Image.png

RWLock是為了數據同步實現的讀寫鎖。
PersistenceObject是數據庫元數據的基類,提供了簡單的save和ignore方法。

#import <Foundation/Foundation.h>

@interface PersistenceObject : NSObject
{
    NSInteger _id;
}

@property (nonatomic, assign) NSInteger _id;

+ (NSArray *)transients;
- (NSInteger)save;

@end

PersistenceHelper提供了一些庫表操作的工具函數,比如建表,數據映射等。

#import <Foundation/Foundation.h>
#import <sqlite3.h>

@class PersistenceObject;

@interface PersistenceHelper : NSObject

+ (NSString*) tableName:(Class)class;

+ (NSString*) columnName:(NSString*)propertyName;
+ (NSString*) propertyName:(NSString*)columnName;

+ (NSDictionary*)fields:(Class)class;

+ (NSString*) genDDL:(Class)class;

+ (void)mappingToStatement:(PersistenceObject*)object statement:(sqlite3_stmt*)statement;

+ (void)mappingToObject:(sqlite3_stmt *)statement object:(PersistenceObject *)object;

@end

PersistenceManager封裝了一些SQLite的接口

@class PersistenceObject;

@interface PersistenceManager : NSObject
{
    sqlite3 *database;
    NSMutableArray *tables;
    
    int _transactionLevel;
}

+ (PersistenceManager*)sharedManager;

- (void)databaseNotMatch;
- (BOOL)execSQL:(NSString*)sql;

- (void)startTransaction: (NSString *) tag;
- (void)commitTransaction: (NSString *) tag;
- (void)rollbackTransaction: (NSString *) tag;

- (NSInteger)insert:(PersistenceObject*)object;
- (NSInteger)update:(PersistenceObject*)object;
- (NSInteger)deleteObject:(PersistenceObject *)object;

- (NSArray*)execQuery:(NSString*)sql;
- (NSArray*)execQuery:(Class)class sql:(NSString *)sql;
- (NSArray*)execQuery:(Class)class selection:(NSString*)selection selectionArgs:(NSArray*)selectionArgs groupBy:(NSString*)groupBy orderBy:(NSString*)orderBy limit:(NSInteger)limit;
- (NSArray*)execQuery:(Class)class selection:(NSString*)selection selectionArgs:(NSArray*)selectionArgs groupBy:(NSString*)groupBy orderBy:(NSString*)orderBy limit:(NSInteger)limit skip: (NSInteger) skip;

- (NSInteger) queryCount:(Class) class withWhereClause:(NSString*) whereClause;
- (void) execDropTableSql:(Class) class;

@end

今天要講的就是PersistenceHelper的mappingToStatement和mappingToObject方法,介紹如何將sqlite3_stmt和PersistenceObject進行映射。

做過Java的都知道反射的強大,iOS的run time提供了一套獲取class屬性的接口。

因此我們可以通過runtime的方法將PersistenceObject的屬性列表取出來,保存為一個NSDictionary,key是propName,Value是數據類型。

+ (NSDictionary *)fields:(Class)class
{
    // Recurse up the classes, but stop at NSObject. Each class only reports its own properties, not those inherited from its superclass
    NSMutableDictionary *theProps;
    
    if ([class superclass] != [NSObject class])
        theProps = (NSMutableDictionary *)[self fields:[class superclass]];
    else
        theProps = [NSMutableDictionary dictionary];
    
    unsigned int outCount;
    
    objc_property_t *propList = class_copyPropertyList(class, &outCount);
    
    // Loop through properties and add declarations for the create
    for (int i=0; i < outCount; i++)
    {
        objc_property_t oneProp = propList[i];
        
        NSString *propName = [NSString stringWithUTF8String:property_getName(oneProp)];
        NSString *attrs = [NSString stringWithUTF8String:property_getAttributes(oneProp)];
        
        // Read only attributes are assumed to be derived or calculated
        if ([attrs rangeOfString:@",R,"].location == NSNotFound)
        {
            NSArray *attrParts = [attrs componentsSeparatedByString:@","];
            if (attrParts != nil)
            {
                if ([attrParts count] > 0)
                {
                    NSString *propType = [[attrParts objectAtIndex:0] substringFromIndex:1];
                    [theProps setObject:propType forKey:propName];
                }
            }
        }
    }
    
    free(propList);
    return theProps;
}

因為iOS的數據類型是有限的,我們可以將其和SQLite的數據類型一一對應。接下來就可以根據屬性名和屬性類型來進行映射了,當然也可以實現自動創建SQLite的Table,這里就不貼出太多的代碼。

+ (void)mappingToStatement:(PersistenceObject *)object statement:(sqlite3_stmt *)statement
{
    NSDictionary *props = [PersistenceHelper fields:[object class]];
    NSArray *transients = [[object class] transients];
    
    int index = 1;
    
    for (NSString *propName in props)
    {
        if ([transients containsObject:propName]) continue;
        
        NSString *propType = [props objectForKey:propName];
        
        if (![propName isEqualToString:@"_id"]) {
            id value = [object valueForKey:propName];
            
            if (!value || [value isKindOfClass:[NSNull class]])
            {
                sqlite3_bind_null(statement, index++);
            }
            else if ([propType isEqualToString:@"i"] || // int
                     [propType isEqualToString:@"I"] || // unsigned int
                     [propType isEqualToString:@"l"] || // long
                     [propType isEqualToString:@"L"] || // usigned long
                     [propType isEqualToString:@"q"] || // long long
                     [propType isEqualToString:@"Q"] || // unsigned long long
                     [propType isEqualToString:@"s"] || // short
                     [propType isEqualToString:@"S"] || // unsigned short
                     [propType isEqualToString:@"B"])   // bool
            {
                sqlite3_bind_int64(statement, index++, [value longLongValue]);
            }
            else if ([propType isEqualToString:@"f"] || // float
                     [propType isEqualToString:@"d"] )  // double
            {
                sqlite3_bind_double(statement, index++, [value doubleValue]);
            }
            else if ([propType isEqualToString:@"c"] || // char
                     [propType isEqualToString:@"C"] ) // unsigned char
                
            {
                sqlite3_bind_int(statement, index++, [value intValue]);
            }
            else if ([propType hasPrefix:@"@"] ) // Object
            {
                NSString *className = [propType substringWithRange:NSMakeRange(2, [propType length]-3)];
                
                if([className isEqualToString:@"NSString"])
                {
                    sqlite3_bind_text(statement, index++, [value UTF8String], -1, NULL);
                }
                else if([className isEqualToString:@"NSNumber"])
                {
                    sqlite3_bind_double(statement, index++, [value doubleValue]);
                }
                else if([className isEqualToString:@"NSDate"])
                {
                    sqlite3_bind_int64(statement, index++, [value timeIntervalSince1970]);
                }
                else if([className isEqualToString:@"NSData"])
                {
                    sqlite3_bind_blob(statement, index++, [value bytes], [value length], NULL);
                }
                else
                {
                    index++;
                    KLOGV(@"PersistenceHelper", @"Unknow Object Type: %@", className);
                }
            }
        }
    }
}

+ (void)mappingToObject:(sqlite3_stmt *)statement object:(PersistenceObject *)object
{
    NSDictionary *theProps = [self fields:[object class]];
    
    for (int i=0; i <  sqlite3_column_count(statement); i++)
    {
        NSString *columnName = [NSString stringWithUTF8String:sqlite3_column_name(statement, i)];
        
        NSString *propName = [self propertyName:columnName];
        
        NSString *columnType = [theProps valueForKey:propName];
        
        if (!columnType) {
            break;
        }
        
        if ([columnType isEqualToString:@"i"] || // int
            [columnType isEqualToString:@"l"] || // long
            [columnType isEqualToString:@"q"] || // long long
            [columnType isEqualToString:@"s"] || // short
            [columnType isEqualToString:@"B"] || // bool or _Bool
            [columnType isEqualToString:@"I"] || // unsigned int
            [columnType isEqualToString:@"L"] || // usigned long
            [columnType isEqualToString:@"Q"] || // unsigned long long
            [columnType isEqualToString:@"S"])   // unsigned short
        {
            long long value = sqlite3_column_int64(statement, i);
            NSNumber *colValue = [NSNumber numberWithLongLong:value];
            [object setValue:colValue forKey:propName];
        }
        else if ([columnType isEqualToString:@"f"] || // float
                 [columnType isEqualToString:@"d"] )  // double
        {
            double value = sqlite3_column_double(statement, i);
            NSNumber *colVal = [NSNumber numberWithDouble:value];
            [object setValue:colVal forKey:propName];
        }
        else if ([columnType isEqualToString:@"c"] ||   // char
                 [columnType isEqualToString:@"C"] ) // unsigned char
        {
            NSInteger value = sqlite3_column_int(statement, i);
            NSNumber *colValue = [NSNumber numberWithInt:value];
            [object setValue:colValue forKey:propName];
        }
        else if ([columnType hasPrefix:@"@"] ) // Object
        {
            NSString *className = [columnType substringWithRange:NSMakeRange(2, [columnType length]-3)];
            
            if([className isEqualToString:@"NSString"])
            {
                const char *colVal = (const char *)sqlite3_column_text(statement, i);
                
                if (colVal != NULL)
                {
                    NSString *colValString = [NSString stringWithUTF8String:colVal];
                    [object setValue:colValString forKey:propName];
                }
            }
            else if([className isEqualToString:@"NSNumber"])
            {
                double value = sqlite3_column_double(statement, i);
                NSNumber *colVal = [NSNumber numberWithDouble:value];
                [object setValue:colVal forKey:propName];
            }
            else if([className isEqualToString:@"NSDate"])
            {
                long long value = sqlite3_column_int64(statement, i);
                NSDate *colValue = [NSDate dateWithTimeIntervalSince1970:value];
                [object setValue:colValue forKey:propName];
            }
            else if([className isEqualToString:@"NSData"])
            {
                const void* value = sqlite3_column_blob(statement, i);
                if (value != NULL)
                {
                    int length = sqlite3_column_bytes(statement, i);   
                    NSData *colValue = [NSData dataWithBytes:value length:length];
                    [object setValue:colValue forKey:propName];
                }
            }
            else
            {
                KLOGV(@"PersistenceHelper", @"Unknow Object Type: %@", className);
            }
        }
    }
}

好多年前的代碼了,估計現在要跑起來還要改一些代碼。:)

至于Android端,找時間再寫吧,注解+反射拿到現在來看都不是什么高深的技術。

我是咕咕雞,一個還在不停學習的全棧工程師。
熱愛生活,喜歡跑步,家庭是我不斷向前進步的動力。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Swift版本點擊這里歡迎加入QQ群交流: 594119878最新更新日期:18-09-17 About A cu...
    ylgwhyh閱讀 25,573評論 7 249
  • 屬性 設置內容 設置字體顏色 設置對齊方式 設置字體大小 背景顏色 換行模式 設置最小收縮比例 設置行數 設置文字...
    JerryLMJ閱讀 2,902評論 0 4
  • #龍木子日記# 2017.9.29 第31日 1.于我而言,不同的音樂就是我不同的世界,在不同的世界里游離一番,人...
    龍木子閱讀 228評論 0 0
  • 我和小自行車六歲就認識了,我管它小名兒叫小洋車。 記得它剛來我家和我作伴的時候,我正在趴在院子里的小凳上寫作業,爸...
    我數123木頭人閱讀 526評論 0 0