从0开始弄一个面向OC数据库(二)

前言

首先,在上一篇文章从0开始弄一个面向OC数据库(一),我们实现了数据库的创建、打开、关闭、通过runtime获取模型所有成员变量建表功能。本次代码解决了一个上个版本代码存在的bug,当传targetId为nil的时候,数据库语句执行失败的问题,
其次,本篇文章要实现的功能有:

  • 传入模型,实现数据库的插入数据操作
  • 使用sql的API实现查询,在外层封装一个方法,传入模型类型即可查询数据库内该表下所有的数据,并封装成数组(NSArray <Model * > *)返回。
  • 修改数据库表内已存在的数据,在实现这个方法时,我们将创建并打开数据库、创建表格,插入数据、更新数据封装成一个方法,我们最终将这个方法整合的逻辑为:
    1、判断数据库内是否存在对应的表,没有则创建
    2、创建表时会执行打开数据库,如果没有数据库则创建再打开
    3、判断对应的表内是否存在主键相同的记录,存在则对该记录进行更新,不存在则进行插入操作
    这个方法的最终形态:如果是一个从零开始的状态,没有任何数据库,这个方法会替我们 创建对应的数据库,创建对应的表格,插入对应的数据,如果存在对应的数据库、表、数据,则进行更新
隔断.jpg

功能实现

1、插入数据操作

插入数据的sql语句可以分析为:
insert into 表名(字段1名称,字段2名称,字段3名称) values ('值1','值2','值3')
思路:
1.判断数据库内是否有对应表格,没有则创建(这一步先不做,因为我们目前还没有实现查询语句暂时无法判断,我们先在外面创建一个对应的表格,再执行插入操作)
2.获取模型的所有成员变量名称,通过KVC获取成员变量对应的值。
3.拼接sql插入语句并执行语句
贴上我们的代码:

#pragma mark 插入数据
+ (BOOL)insertModel:(id)model uid:(NSString *)uid targetId:(NSString *)targetId {
    // 获取表名
    Class cls = [model class];
    NSString *tableName = [CWModelTool tableName:cls targetId:targetId];
    // 1.判断数据库内是否有对应表格,没有则创建(这一步先不做,因为我们目前还没有实现查询语句,我们先在外面创建一个表格,再执行插入操作)
    // 2.插入数据
    // 获取类的所有成员变量的名称与类型
    NSDictionary *nameTypeDict = [CWModelTool classIvarNameAndTypeDic:cls];
    // 获取所有成员变量的名称,也就是sql语句字段名称
    NSArray *allIvarNames = nameTypeDict.allKeys;
    // 获取所有成员变量对应的值
    NSMutableArray *allIvarValues = [NSMutableArray array];
    for (NSString *ivarName in allIvarNames) {
        // 获取对应的值,暂时不考虑自定义模型和oc模型的情况
        id value = [model valueForKeyPath:ivarName];
        [allIvarValues addObject:value];
    }
    // insert into 表名(字段1,字段2,字段3) values ('值1','值2','值3')
    NSString *sql = [NSString stringWithFormat:@"insert into %@(%@) values('%@')",tableName,[allIvarNames componentsJoinedByString:@","],[allIvarValues componentsJoinedByString:@"','"]];
    
    return [CWDatabase execSQL:sql uid:uid];
}

对这个方法进行单元测试:

- (void)testInsertModel {
    // 创建表格
    BOOL result = [CWSqliteModelTool createSQLTable:[Student class] uid:@"Chavez" targetId:nil];
    XCTAssertTrue(result);
    
    Student *stu = [[Student alloc] init];
    stu.stuId = 10086;
    stu.name = @"Alibaba";
    stu.age = 16;
    stu.height = 165;
    // 插入数据
    BOOL result1 = [CWSqliteModelTool insertModel:stu uid:@"Chavez" targetId:nil];
    XCTAssertTrue(result1);
    
    Student *stu1 = [[Student alloc] init];
    stu1.stuId = 10010;
    stu1.name = @"Tencent";
    stu1.age = 17;
    stu1.height = 182;
    // 插入数据
    BOOL result2 = [CWSqliteModelTool insertModel:stu1 uid:@"Chavez" targetId:nil];
    XCTAssertTrue(result2);
    
    Student *stu2 = [[Student alloc] init];
    stu2.stuId = 10000;
    stu2.name = @"Baidu";
    stu2.age = 18;
    stu2.height = 180;
    // 插入数据
    BOOL result3 = [CWSqliteModelTool insertModel:stu2 uid:@"Chavez" targetId:nil];
    XCTAssertTrue(result3);
}

创建对应的数据库以及表格,向数据库插入3条数据,最终我们看到测试成功,并打开对应的数据库表格进行验证得到下图,成功!

插入数据成功.png

2、数据库查询

面向sql的API我们可以分为两类,一类为查询操作,一类为非查询操作,也就是执行语句,执行语句在之前已经实现了,现在我们来实现查询操作,首先提供思路:

  • 1.打开数据库
  • 2.预执行sql语句
  • 3.绑定数据
  • 4.执行遍历查询(在这里我们会将每一条数据封装成字典{字段类型key : 字段值value ...},然后加入数组中保存返回)
  • 5.重置
  • 6.释放资源,关闭数据库

sql3为查询提供了以下两个方法

// 预执行sql语句、准备语句
SQLITE_API int sqlite3_prepare_v2(
  sqlite3 *db,            /* 数据库的操作句柄 */
  const char *zSql,       /* sql语句 */
  int nByte,              /* 参数2sql语句取出多少字节的长度。-1为自动计算 找到\0结束符*/
  sqlite3_stmt **ppStmt,  /* 伴随指针的地址 */
  const char **pzTail     /* 参数2减去参数3剩余的sql语句*/
);

// 执行伴随指针,如果结果为SQLITE_ROW,表示还有下一条数据,伴随指针指向下一条数据,否则结束循环
SQLITE_API int sqlite3_step(sqlite3_stmt*);

在CWDatabase封装一个方法,执行查询操作

+ (NSMutableArray <NSMutableDictionary *>*)querySql:(NSString *)sql uid:(NSString *)uid {
    // 1、打开数据库
    if (![self openDB:uid]) {
        return nil;
    }
    // 2、预执行语句
    sqlite3_stmt *ppStmt     = 0x00; //伴随指针
    if (sqlite3_prepare_v2(cw_database, sql.UTF8String, -1, &ppStmt, nil) != SQLITE_OK) {
        NSLog(@"查询准备语句编译失败");
        return nil;
    }
    // 3、绑定数据,因为我们的sql语句中不带有?用来赋值,所以不需要进行绑定
    // 4、执行遍历查询
    NSMutableArray *rowDicArray = [NSMutableArray array];
    while (sqlite3_step(ppStmt) == SQLITE_ROW) { // SQLITE_ROW表示还有下一条数据
        // 获取有多少列(也就是一条数据有多少个字段)
        int columnCount = sqlite3_column_count(ppStmt);
        // 存储一条数据的所有字段名与值 的字典
        NSMutableDictionary *rowDict = [NSMutableDictionary dictionary];
        // 遍历数据库一条数据所有字段
        for (int i = 0; i < columnCount; i++) {
            // 获取字段名
            NSString *columnName = [NSString stringWithUTF8String:sqlite3_column_name(ppStmt, i)];
            // 获取字段名对应的类型
            int type = sqlite3_column_type(ppStmt, i);
            // 获取对应的值
            id value = nil;
            switch (type) {
                case SQLITE_INTEGER:
                    value = @(sqlite3_column_int(ppStmt, i));
                    break;
                case SQLITE_FLOAT:
                    value = @(sqlite3_column_double(ppStmt, i));
                    
                    break;
                case SQLITE_BLOB: // 二进制
                    value = CFBridgingRelease(sqlite3_column_blob(ppStmt, i));
                    break;
                case SQLITE_NULL:
                    value = @"";
                    break;
                case SQLITE3_TEXT:
                    value = [NSString stringWithUTF8String:(const char *)sqlite3_column_text(ppStmt, i)];
                    break;
                    
                default:
                    break;
            }
            [rowDict setValue:value forKey:columnName];
        }
        [rowDicArray addObject:rowDict];
    }
    // 5、重制(省略)
    // 6、释放资源,关闭数据库
    sqlite3_finalize(ppStmt);
    [self closeDB];
    
    return rowDicArray;
}

做完了面向sql的查询之后,我们要将这个方法封装一下,面向模型。我们在CWSqliteModelTool封装一个方法

#pragma mark 查询数据
// 查询表内所有数据
+ (NSArray *)queryAllModels:(Class)cls uid:(NSString *)uid targetId:(NSString *)targetId {
    
    NSString *tableName = [CWModelTool tableName:cls targetId:targetId];
    
    NSString *sql = [NSString stringWithFormat:@"select * from %@", tableName];
    
    NSArray <NSDictionary *>*results = [CWDatabase querySql:sql uid:uid];
    return [self parseResults:results withClass:cls];
}

// 解析数据
+ (NSArray *)parseResults:(NSArray <NSDictionary *>*)results withClass:(Class)cls  {
    
    NSMutableArray *models = [NSMutableArray array];
    for (NSDictionary *dict in results) {
        id model = [[cls alloc] init];
        // dict类型为{字段类型 : 字段值} 遍历为模型赋值
        [dict enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
            id value = obj;
            [model setValue:value forKeyPath:key];
        }];
        
        [models addObject:model];
    }
    return models;
}

然后我们测试查询函数

- (void)testQueryModels {
    NSArray *models = [CWSqliteModelTool queryAllModels:[Student class] uid:@"Chavez" targetId:nil];
    NSLog(@"query models : %@",models);
    XCTAssertNotNil(models);
}

目前Chavez数据库的Student表一共存在3条我们刚刚插入进去的数据,执行查询的结果如果与数据库内的数据对应,则代表成功,最终我们得到如下打印:

// 数据里有3个模型,每个模型对应的成员变量以及值都对应上了,打印出来的不是Student模型的地址是因为我们重写了Student模型的description函数
2017-12-10 09:37:22.016582+0800 CWDB[2638:355147] query models : (
    " stuId = 10000 , name = Baidu , age = 18 , height = 180 ,score = 0.000000",
    " stuId = 10010 , name = Tencent , age = 17 , height = 182 ,score = 0.000000",
    " stuId = 10086 , name = Alibaba , age = 16 , height = 165 ,score = 0.000000"
)

测试结果表示我们的方法完全OK!

3、更新数据

在文章的开头,我们已经为这个方法做了很多的介绍了,我们要把创建数据库、创建表格、插入数据、更新数据都整合在这个方法内,先说一下实现的步骤:

  • 1.获取表名,判断数据库是否存在对应的表,不存在则创建
  • 2.根据主键,判断数据库内是否存在记录
  • 3.如果数据库内不存在记录,进行数据插入操作
  • 4.如果数据库内存在记录,进行数据更新操作
    首先我们来解决步骤1的问题,如何判断数据库内是否存在对应的表:我们要引入一个概念sqlite的系统表sqlite_master,每一个 SQLite 数据库都有一个叫 sqlite_master 的表, 它定义数据库的模式。这个表里面存储着当前数据库中所有表的相关信息,比如表的名称、用于创建此表的sql语句、索引、索引所属的表、创建索引的sql语句等,所以我们只要对sqlite_master执行查询操作即可,在CWSqliteTableTool内封装一个方法:
+ (BOOL)isTableExists:(NSString *)tableName uid:(NSString *)uid{
    // 去sqlite_master这个表里面去查询创建此索引的sql语句
    NSString *queryCreateSqlStr = [NSString stringWithFormat:@"select sql from sqlite_master where type = 'table' and name = '%@'",tableName];
    
    NSMutableArray *resultArray = [CWDatabase querySql:queryCreateSqlStr uid:uid];
    return resultArray.count > 0;
}

最后,贴上我们整合出来的方法:

#pragma mark 插入或者更新数据
+ (BOOL)insertOrUpdateModel:(id)model uid:(NSString *)uid targetId:(NSString *)targetId {
    // 获取表名
    Class cls = [model class];
    NSString *tableName = [CWModelTool tableName:cls targetId:targetId];
    
    // 判断数据库是否存在对应的表,不存在则创建
    if (![CWSqliteTableTool isTableExists:tableName uid:uid]) {
        [self createSQLTable:cls uid:uid targetId:targetId];
    }
    
    // 根据主键,判断数据库内是否存在记录
    // 判断对象是否返回主键信息
    if (![cls respondsToSelector:@selector(primaryKey)]) {
        NSLog(@"如果想要操作这个模型,必须要实现+ (NSString *)primaryKey;这个方法,来告诉我主键信息");
        return NO;
    }
    // 获取主键
    NSString *primaryKey = [cls primaryKey];
    if (!primaryKey) {
        NSLog(@"你需要指定一个主键来创建数据库表");
        return NO;
    }
    // 模型中的主键的值
    id primaryValue = [model valueForKeyPath:primaryKey];
    //  查询语句:  NSString *checkSql = @"select * from 表名 where 主键 = '主键值' ";
    NSString * checkSql = [NSString stringWithFormat:@"select * from %@ where %@ = '%@'",tableName,primaryKey,primaryValue];
    
    // 执行查询语句,获取结果
    NSArray *result = [CWDatabase querySql:checkSql uid:uid];
    // 获取类的所有成员变量的名称与类型
    NSDictionary *nameTypeDict = [CWModelTool classIvarNameAndTypeDic:cls];
    // 获取所有成员变量的名称,也就是sql语句字段名称
    NSArray *allIvarNames = nameTypeDict.allKeys;
    // 获取所有成员变量对应的值
    NSMutableArray *allIvarValues = [NSMutableArray array];
    for (NSString *ivarName in allIvarNames) {
        // 获取对应的值,暂时不考虑自定义模型和oc模型的情况
        id value = [model valueForKeyPath:ivarName];
        [allIvarValues addObject:value];
    }
    // 字段1=字段1值 allIvarNames[i]=allIvarValues[I]
    NSMutableArray *ivarNameValueArray = [NSMutableArray array];
    NSInteger count = allIvarNames.count;
    for (int i = 0; i < count; i++) {
        NSString *name = allIvarNames[I];
        id value = allIvarValues[I];
        NSString *ivarNameValue = [NSString stringWithFormat:@"%@='%@'",name,value];
        [ivarNameValueArray addObject:ivarNameValue];
    }
    
    NSString *execSql = @"";
    if (result.count > 0) { // 表内存在记录,更新
        // update 表名 set 字段1='字段1值',字段2='字段2的值'...where 主键 = '主键值'
        execSql = [NSString stringWithFormat:@"update %@ set %@ where %@ = '%@'",tableName,[ivarNameValueArray componentsJoinedByString:@","],primaryKey,primaryValue];
    }else { // 表内不存在记录,插入
        // insert into 表名(字段1,字段2,字段3) values ('值1','值2','值3')
        execSql = [NSString stringWithFormat:@"insert into %@(%@) values('%@')",tableName,[allIvarNames componentsJoinedByString:@","],[allIvarValues componentsJoinedByString:@"','"]];
    }
    return [CWDatabase execSQL:execSql uid:uid];
}

然后我们通过单元测试来测试这个方法,首先我们测试在数据不存在的时候的插入操作:

// 向Chavez数据库的“Student国防科技大学”表内插入两条数据
- (void)testCreateTableAndInsertModel {
    Student *stu = [[Student alloc] init];
    stu.stuId = 110;
    stu.name = @"中国公安";
    stu.age = 100;
    stu.height = 190;
    BOOL result = [CWSqliteModelTool insertOrUpdateModel:stu uid:@"Chavez" targetId:@"国防科技大学"];
    XCTAssertTrue(result);
    
    Student *stu1 = [[Student alloc] init];
    stu1.stuId = 119;
    stu1.name = @"中国火警";
    stu1.age = 101;
    stu1.height = 200;
    BOOL result1 = [CWSqliteModelTool insertOrUpdateModel:stu1 uid:@"Chavez" targetId:@"国防科技大学"];
    XCTAssertTrue(result1);
}

我们打开数据库软件,按command+R刷新软件,查看结果如下

image.png

插入数据测试成功,然后我们再为这个方法来写一个更新数据的单元测试:

// 我们把数据库里面stuId为110的中国公安的数据修改,还是调用同样的方法
- (void)testUpdateModel {
    
    Student *stu = [[Student alloc] init];
    stu.stuId = 110;
    stu.name = @"中国公安警察支队";
    stu.age = 90;
    stu.height = 189;
    
    BOOL result = [CWSqliteModelTool insertOrUpdateModel:stu uid:@"Chavez" targetId:@"国防科技大学"];
    XCTAssertTrue(result);
}

运行之后将数据库软件进行刷新,得到如下表格,发现stuId为110的数据已经按照我们最新的模型进行了修改,测试通过。
image.png
最终,我们将数据库的创建、打开、建表、插入、更新操作都封装成了一个方法,非常符合我们之前要求的,简单、简单、简单无脑。

如果你想要保存一条数据到本地数据库,只需要执行这个方法:

+ (BOOL)insertOrUpdateModel:(id)model uid:(NSString *)uid targetId:(NSString *)targetId;

如果你想要更新本地数据库的一条数据,也只需要执行上面的方法,丝毫不需要关心数据库是如何创建的,数据库表是如何创建的等。。


如果你想要查询数据库某个表里面的所有数据,只需要执行这个方法:

+ (NSArray *)queryAllModels:(Class)cls uid:(NSString *)uid targetId:(NSString *)targetId;

后期我们会为查询增加一些条件,来满足更多的查询场景。

4.本篇结束

在此,数据库的增删查改我们通过封装出来的两个方法,便捷的实现了其中3项,一些拓展的功能选择在后面再完善吧。
在下一篇文章,我们会实现数据库的删除、数据库迁移,以及再完善一下细节,在更后面的文章,我们会实现模型嵌套对象,数组、字典潜逃对象的情况以及多线程安全的处理。。

github地址
本次的代码,tag为1.1.0,你可以在release下找到对应的tag下载下来

最后觉得有用的同学,希望能给本文点个喜欢,给github点个star以资鼓励,谢谢大家。

PS: 因为我也是一边封装,一边写文章。效率可能比较低,问题也会有,欢迎大家向我抛issue,有更好的思路也欢迎大家留言!

最后再为大家提供上一篇文章的地址。从0开始弄一个面向OC数据库(一)

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

推荐阅读更多精彩内容

  • 1、我们要做什么? 这个文章,我们要从0开始封装一个面向OC对象的数据库,想了解怎么做的,可以一起陪伴一下,所有的...
    cw_阅读 1,277评论 2 6
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,398评论 25 707
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,560评论 18 399
  • 有种课本,叫“别人家”的性教育课本……来,这篇很值得“拭目以待”! ---------------------- ...
    大观家庭阅读 1,893评论 0 0
  • 青年比少年年长五岁,一年暮秋,少年拍拍身上的尘土,跑到青年身边,抢过他手里的书丢在低上。 “叶夕照,我要见云栖!”...
    墨鱼哥哥阅读 536评论 0 0