iOS基于runtime实现的简单ORM

原创文章转载请注明出处

今天一个朋友让我完善一下简历,把以前做过的项目内容写详细一些,我说好吧。翻到12年的一个产品,那是第一次写iOS App,当时还在iOS 4.3时代,网上的资料少得可怜。在评估数据库方案的时候,先把在当初还不是很成熟的Core Data给排除了,那就只剩SQLite可选。当时最火的SQLite框架就是FMDB了,但是看着FMDB那又长又臭的CreateTable方法,还有臃肿的包,哥几个决定自己动手改写一个Persistence Manager。

讲真,一直到现在我也没有用过FMDB,自己写的东西已经足够应付开发需求了性能也不差,后来为了跨平台又把数据库改成了Realm。

言归正传,我们的Persistence Manager其实非常简单,只有4个类。

iOS基于runtime实现的简单ORM_第1张图片
Paste_Image.png

RWLock是为了数据同步实现的读写锁。
PersistenceObject是数据库元数据的基类,提供了简单的save和ignore方法。

#import 

@interface PersistenceObject : NSObject
{
    NSInteger _id;
}

@property (nonatomic, assign) NSInteger _id;

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

@end

PersistenceHelper提供了一些库表操作的工具函数,比如建表,数据映射等。

#import 
#import 

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

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

你可能感兴趣的:(iOS基于runtime实现的简单ORM)