高性能Sqlite存储模型对象解密

1,809 阅读8分钟

前言

首先写这篇文章之前祝大家元旦快乐,然后自我介绍一下,我叫吴海超(WHC)在iOS领域有丰富的开发架构经验Github以后我也会以文章的形式分享具有实战意义的文章给大家,希望能够给大家有所帮助。

主题

好今天这篇文章我主要给大家讲讲Sqlite应用,我想大家应该都知道怎么去应用以及使用Sqlite(创建数据库,建表,增删改查。。。),而且这些步骤现在都已经是固定模式了,既然都已经是固定模式了那我们可不可以对这些固定模式操作进行一层封装让使用更方便高效?答案是那是肯定可以的,这个具体怎么封装稍后再讨论,我们先看看传统使用Sqlite方式。

传统方式使用Sqlite

  • 创建数据库
sqlite3 *database;
- (void)createSqlite {
   NSArray *documentArray = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
   NSString *documentPath = [documentArray firstObject];
   // 新建名称为whc.sqlite 数据文件存放在沙箱doc目录下面 
   NSString *path = [NSString stringWithFormat:@"%@/whc.sqlite",documentPath];
   /// 创建并且打开数据库
   sqlite3_open([[self path] UTF8String], &database);
}
  • 建表
- (void)createTable {
    const char *createSQL = "create table if not exists whc(id integer primary key autoincrement,name char,sex char)";
    sqlite3_exec(database, createSQL, NULL, NULL, NULL);
}
  • 增删改查
//<1> 插入记录Sql: "insert into whc (name,sex)values('whc','male')"
//<2> 删除记录Sql: "delete from whc where ...."
//<3> 修改记录Sql: "update whc set ... where ...."
//<4> 查询记录Sql: "select from whc ......"
//执行sql:
sqlite3_stmt * stmt;
/// 预执行Sql
sqlite3_prepare_v2(database, sql, -1, &stmt, nil);
/// 执行sql
sqlite3_step(stmt);
.... 
处理结果,然后关闭数据库

对传统方式分析

我可以看到这上面的步骤是我们可以经常看到的方式,虽然上面的精简了很多但是我认为还是很无聊,每创建一个数据库都要这样搞一遍效率实在很低而且不可靠一点都不灵活,其实大家都知道的CoreData也是很固定模式的,今年5月初哦不对现在是2017年了应该是去年5月,我就思考能不能一行代码操作数据库呢?我下班在回去的公交车上思前想后觉得完全可行的,后来我的方案是:[Runtime + Sqlite]技术架构。

第二天就开始实施尝试经过不断调试最后第一版本开发出来了WHC_ModelSqlite下面我看看怎么来一行代码操作Sqlite

WHC_ModelSqlite介绍

  • 目标: 替代直接使用Sqlite和CoreData
  • 架构: 线程安全,采用runtime技术和Sqlite Api完美结合打造
  • 易用: 告别繁琐sql语句的编写和CoreData复杂创建
  • 支持: (NSData,NSString,Int,double,float,Bool,char,NSNumber)类型
  • 强大: 支持模型嵌套模型类存储到数据库和多表嵌套联查
  • 智能: 智能根据数据库模型类提供的VERSION方法返回的版本号动态更新数据库字段(动态删除/添加)

先看看架构图:

首先WHC_ModelSqlite是不需要显示的去创建数据库和表而是通过Model类来决定数据库名称和表名以及字段名称和数据类型,好下面先做一个使用演示:
假设有这样一个模型Person类对象whc

Person * whc = [Person new];
whc.name = @"吴海超";
whc.age = 25;
whc.height = 180.0;
whc.weight = 140.0;
whc.isDeveloper = YES;
whc.sex = 'm';

// 嵌套car对象
whc.car = [Car new];
whc.car.name = @"撼路者";
whc.car.brand = @"大路虎";

// 嵌套school对象
whc.school = [School new];
whc.school.name = @"北京大学";
whc.school.personCount = 5000;

// school对象嵌套city对象
whc.school.city = [City new];
whc.school.city.name = @"北京";
whc.school.city.personCount = 1000;

我们怎么把person插入到数据库呢?看下面

/// 把上面创建好的whc对象插入数据库
[WHC_ModelSqlite insert:whc];

可能很多人在这里会有疑问?怎么没有创建数据库和表以及字段呢?首先WHC_ModelSqlite上面介绍以及说了是不需要这样操作的,为什么?那是因为WHC_ModelSqlite是基于Runtime技术开发的在上面执行[WHC_ModelSqlite insert:whc]时候会检查模型Person类是否已经创建了数据库,如果没有会为Person模型类创建,然后通过Runtime读取Person的属性名称以及类型然后通过类型映射把oc数据类型转换为Sqlite字段类型并且创建表(表名就是model类名),好上面对创建数据库表以及字段做了分析,那么whc对象是如何插入到数据库的?
在上面的检查工作都好做了insert会用runtime技术获取model对象whc所有属性名称类型以及对应的值,然后通过前面获取的属性名称和值拼接Sql插入语句然后再执行的,对没错步骤就是这样。然后过程看起来好像很简单但其实有很多细节要做控制,具体看参考源代码
插入代码片段

+ (void)insert:(id)model_object {
    /// 如果当前有其他数据库的操作那么等待
    dispatch_semaphore_wait([self shareInstance].dsema, DISPATCH_TIME_FOREVER);
    @autoreleasepool {
        [[self shareInstance].sub_model_info removeAllObjects];
        [self insertModelObject:model_object];
    }
    dispatch_semaphore_signal([self shareInstance].dsema);
}

/// 处理插入模型对象
+ (sqlite_int64)insertModelObject:(id)model_object {
    /// 扫描model对象的详细信息(runtime)
    NSDictionary * sub_model_objects_info = [self scanSubModelObject:model_object];
    if (sub_model_objects_info.count > 0) {
        [sub_model_objects_info enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
            /// 分析处理model对象的子model对象
            sqlite_int64 _id = [self insertModelObject:obj];
            [[self shareInstance].sub_model_info setObject:@(_id) forKey:key];
        }];
    }
    /// 插入model对象并且返回_id
    return [self commonInsertSubModelObject:model_object];
}

+ (sqlite_int64)commonInsertSubModelObject:(id)sub_model_object {
    sqlite_int64 _id = -1;
    /// 创建表以及打开表创建字段
    if ([self openTable:[sub_model_object class]]) {
       /// 开启事务
        [self execSql:@"BEIGIN"];
        /// 插入model对象
        [self commonInsert:sub_model_object index:-1];
        /// 提交事务
        [self execSql:@"COMMIT"];
        /// 获取表最大的id并返回
        _id = [self getModelMaxIdWithClass:[sub_model_object class]];
        /// 关闭数据库
        [self close];
    }
    return _id;
}

/// 执行插入操作
+ (void)commonInsert:(id)model_object index:(NSInteger)index {
    sqlite3_stmt * pp_stmt = nil;
    /// 分析model信息(属性名称/类型)
    NSDictionary * field_dictionary = [self parserModelObjectFieldsWithModelClass:[model_object class]];
    /// 获取表名
    NSString * table_name = NSStringFromClass([model_object class]);
    /// 下面都是构建sql insert插入语句模块
    __block NSString * insert_sql = [NSString stringWithFormat:@"INSERT INTO %@ (",table_name];
    NSArray * field_array = field_dictionary.allKeys;
    NSMutableArray * value_array = [NSMutableArray array];
    NSMutableArray * insert_field_array = [NSMutableArray array];
    [field_array enumerateObjectsUsingBlock:^(NSString *  _Nonnull field, NSUInteger idx, BOOL * _Nonnull stop) {
        WHC_PropertyInfo * property_info = field_dictionary[field];
        [insert_field_array addObject:field];
        insert_sql = [insert_sql stringByAppendingFormat:@"%@,",field];
        id value = [model_object valueForKey:field];
        id subModelKeyId = [self shareInstance].sub_model_info[property_info.name];
        if ((value && subModelKeyId == nil) || index == _NO_HANDLE_KEY_ID) {
            [value_array addObject:value];
        }else {
            switch (property_info.type) {
                case _Data: {
                    [value_array addObject:[NSData data]];
                }
                    break;
                case _String: {
                    [value_array addObject:@""];
                }
                    break;
                case _Number: {
                    [value_array addObject:@(0.0)];
                }
                    break;
                case _Model: {
                    if ([subModelKeyId isKindOfClass:[NSArray class]]) {
                        [value_array addObject:subModelKeyId[index]];
                    }else {
                        if (subModelKeyId) {
                            [value_array addObject:subModelKeyId];
                        }else {
                            [value_array addObject:@(_NO_HANDLE_KEY_ID)];
                        }
                    }
                }
                    break;
                case _Int: {
                    id sub_model_main_key_object = [self shareInstance].sub_model_info[property_info.name];
                    if (sub_model_main_key_object != nil) {
                        if (index != -1) {
                            [value_array addObject:sub_model_main_key_object[index]];
                        }else {
                            [value_array addObject:sub_model_main_key_object];
                        }
                    }else {
                        NSNumber * value = @(((int64_t (*)(id, SEL))(void *) objc_msgSend)((id)model_object, property_info.getter));
                        [value_array addObject:value];
                    }
                }
                    break;
                case _Boolean: {
                    NSNumber * value = @(((Boolean (*)(id, SEL))(void *) objc_msgSend)((id)model_object, property_info.getter));
                    [value_array addObject:value];
                }
                    break;
                case _Char: {
                    NSNumber * value = @(((int8_t (*)(id, SEL))(void *) objc_msgSend)((id)model_object, property_info.getter));
                    [value_array addObject:value];
                }
                    break;
                case _Double: {
                    NSNumber * value = @(((double (*)(id, SEL))(void *) objc_msgSend)((id)model_object, property_info.getter));
                    [value_array addObject:value];
                }
                    break;
                case _Float: {
                    NSNumber * value = @(((float (*)(id, SEL))(void *) objc_msgSend)((id)model_object, property_info.getter));
                    [value_array addObject:value];
                }
                    break;
                default:
                    break;
            }
        }
    }];

    insert_sql = [insert_sql substringWithRange:NSMakeRange(0, insert_sql.length - 1)];
    insert_sql = [insert_sql stringByAppendingString:@") VALUES ("];

    [field_array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        insert_sql = [insert_sql stringByAppendingString:@"?,"];
    }];
    insert_sql = [insert_sql substringWithRange:NSMakeRange(0, insert_sql.length - 1)];
    insert_sql = [insert_sql stringByAppendingString:@")"];

    /// 准备执行插入sql
    if (sqlite3_prepare_v2(_whc_database, [insert_sql UTF8String], -1, &pp_stmt, nil) == SQLITE_OK) {
        [field_array enumerateObjectsUsingBlock:^(NSString *  _Nonnull field, NSUInteger idx, BOOL * _Nonnull stop) {
        /// 进行值得绑定
            WHC_PropertyInfo * property_info = field_dictionary[field];
            id value = value_array[idx];
            int index = (int)[insert_field_array indexOfObject:field] + 1;
            switch (property_info.type) {
                case _Data:
                    sqlite3_bind_blob(pp_stmt, index, [value bytes], (int)[value length], SQLITE_TRANSIENT);
                    break;
                case _String:
                    sqlite3_bind_text(pp_stmt, index, [value UTF8String], -1, SQLITE_TRANSIENT);
                    break;
                case _Number:
                    sqlite3_bind_double(pp_stmt, index, [value doubleValue]);
                    break;
                case _Model:
                    sqlite3_bind_int64(pp_stmt, index, (sqlite3_int64)[value integerValue]);
                    break;
                case _Int:
                    sqlite3_bind_int64(pp_stmt, index, (sqlite3_int64)[value longLongValue]);
                    break;
                case _Boolean:
                    sqlite3_bind_int(pp_stmt, index, [value boolValue]);
                    break;
                case _Char:
                    sqlite3_bind_int(pp_stmt, index, [value intValue]);
                    break;
                case _Float:
                    sqlite3_bind_double(pp_stmt, index, [value floatValue]);
                    break;
                case _Double:
                    sqlite3_bind_double(pp_stmt, index, [value doubleValue]);
                    break;
                default:
                    break;
            }
        }];
        if (sqlite3_step(pp_stmt) != SQLITE_DONE) {
            sqlite3_finalize(pp_stmt);
        }
    }else {
        [self log:@"Sorry存储数据失败,建议检查模型类属性类型是否符合规范"];
    }
}

好上面对开源库WHC_ModelSqlite的插入操作做了详细分析,由于篇幅限制下面我其他操作简要进行演示。

查询

/// 查询所有记录
NSArray * personArray = [WHC_ModelSqlite query:[Person class]];
/// 查询name字段为whc并且age等于25的小伙子
[WHC_ModelSqlite query:[Person class] where:@"name = 'whc' and age = 25"];
///对person数据表查询并且根据age自动降序或者升序排序
[WHC_ModelSqlite query:[Person class] order:@"by age desc/asc"];
/// 对person数据表查询并且并且限制查询数量为8
[WHC_ModelSqlite query:[Person class] limit:@"8"];
/// 对person数据表查询并且对查询列表偏移8并且限制查询数量为8
[WHC_ModelSqlite query:[Person class] limit:@"8 offset 8"];

上面api具体代码实现可以参考源代码

更新

/// 把记录name等于whc或者age小于等于30的更新为现在的模型whc对象
[WHC_ModelSqlite update:whc where:@"name = 'whc' OR age <= 30"];

有没有很先进。。。。。perfect

删除

/// 删除记录age等于25并且name等于whc的记录
[WHC_ModelSqlite delete:[Person class] where:@"age = 25 AND name = 'whc'"];

清空

/// 清空数据库Person所有记录
[WHC_ModelSqlite clear:[Person class]];

删除数据库

/// 删除Person数据库
[WHC_ModelSqlite removeModel:[Person class]];

删除所有数据库

/// 删除所有模型数据库
[WHC_ModelSqlite removeAllModel];

获取数据库本地路径

NSString * path = [WHC_ModelSqlite localPathWithModel:[Person class]];

获取数据库本地版本号

NSString * path = [WHC_ModelSqlite versionWithModel:[Person class]];

注意

当模型类有新增/删除属性的时候需要在模型类里定义类方法(+ (NSString*)VERSION)修改模型类(数据库)版本号来表明有字段更新操作,库会根据这个VERSION变更智能检查并自动更新数据库字段,无需手动更新数据库字段。

结束

  • 如果您在使用过程中有任何问题,欢迎issue me! - 很乐意为您解答任何相关问题!
  • 与其给我点star,不如向我狠狠地抛来一个BUG!
  • 如果您想要更多的接口来自定义或者建议/意见,欢迎issue me!我会根据大家的需求提供更多的接口!

谢谢您的阅读,WHC_ModelSqlite开源地址:github.com/netyouli/WH…
本人其他优秀开源项目Github