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端,找时间再写吧,注解+反射拿到现在来看都不是什么高深的技术。

我是咕咕鸡,一个还在不停学习的全栈工程师。
热爱生活,喜欢跑步,家庭是我不断向前进步的动力。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,133评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,682评论 3 390
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,784评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,508评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,603评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,607评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,604评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,359评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,805评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,121评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,280评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,959评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,588评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,206评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,442评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,193评论 2 367
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,144评论 2 352

推荐阅读更多精彩内容

  • Swift版本点击这里欢迎加入QQ群交流: 594119878最新更新日期:18-09-17 About A cu...
    ylgwhyh阅读 25,359评论 7 249
  • 属性 设置内容 设置字体颜色 设置对齐方式 设置字体大小 背景颜色 换行模式 设置最小收缩比例 设置行数 设置文字...
    JerryLMJ阅读 2,884评论 0 4
  • #龙木子日记# 2017.9.29 第31日 1.于我而言,不同的音乐就是我不同的世界,在不同的世界里游离一番,人...
    龙木子阅读 221评论 0 0
  • 我和小自行车六岁就认识了,我管它小名儿叫小洋车。 记得它刚来我家和我作伴的时候,我正在趴在院子里的小凳上写作业,爸...
    我数123木头人阅读 427评论 0 0