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

近期又学习了一下数据库的东西,决定花时间封装一个数据库,看了一些源码,感觉有一定的可行度,所以先把第一篇(本篇)文章发上来。啥都没有发上来干啥捏?有时候容易半途而废,先发上来,断自己后路!一定得写,不然就是说话不算话了。
100婚
1、我们要做什么?

这个文章,我们要从0开始封装一个面向OC对象的数据库,想了解怎么做的,可以一起陪伴一下,所有的流程细节我都会写在文章内,因为我也第一次搞这个东西,有兴趣的话咱们可以一起讨论和提升。

2、做成什么样子?

当然是期望做到简()单()操作数据库,不需要背语句,也不需要解析模型,类似realm这种骚操作[realm addObject:obj];obj就给你存到数据库了。


当然,我们不能和行业航母去比较,但是封装出来的东西一定要简单适用,安全可靠。我们封装的数据库,操作必须简单,功能必须全面(增删查改一样不能少,数据库迁移,数据库字段改名也得支持,多线程安全也得考虑,最重要的是常用的数据类型我们都得支持)。

3、简单介绍

(本来说好的只发文章断自己后路,但是还是先实现一部分功能,免得显得太空洞了)
本篇主要实现的功能有:

  • 1、整个库的结构设计;
  • 2、打开并创建数据库、关闭数据库;
  • 3、根据模型对象,创建对应的数据库表格;
    在做功能之前,先简单的介绍一下数据库相关的东西:
    数据库.png

    以上的图表示,名称为CWDB的数据库,里面有一张Student53class的表,表有的字段为age,stuId,score,height,name。也可以解释为CWDB这个数据库里面存了53班每个学生的年龄,学号,成绩,身高,名字,可惜的是53班目前没有一个学生=。=
    一个数据库里面可以有多张名字不重复的表,一个表里面可以有多条主键不一致的数据,每条数据指定一个字段为主键当唯一标识。
    查看数据库这里我用了一个破解版的工具,现在提供给大家:
    链接: https://pan.baidu.com/s/1eSIpTBW 密码: 4rjm
3、开始动手

首先创建一个工程,并向工程中拖入libsqlite3.0.tbd这个库

1、库的结构设计
结构.png
2、打开并创建数据库、关闭数据库

a、打开并创建数据库
sqlite3 向我们提供了这个接口,用来执行打开数据库操作,第一个参数为数据库存的路径,第二个参数为sqlite3的操作连接。
如果数据库路径下没有数据库,则创建一个数据库并打开,如果有则直接打开数据库

SQLITE_API int sqlite3_open(
  const char *filename,   /* Database filename (UTF-8) */
  sqlite3 **ppDb          /* OUT: SQLite db handle */
);

我们封装一个方法执行创建并打开数据库的代码,当传uid的时候会以uid命名数据库,如果没传将会默认数据库名称为CWDB,路径我们写在CWDatabase.m下,因为是测试阶段,所以路径设置在桌面上。需要自行修改路径

+ (BOOL)openDB:(NSString *)uid {
    // 数据库名称
    NSString *dbName = @"CWDB.sqlite";
    if (uid.length != 0) {
        dbName = [NSString stringWithFormat:@"%@.sqlite", uid];
    }
    // 数据库路径
    NSString *dbPath = [kCWDBCachePath stringByAppendingPathComponent:dbName];
    // 打开数据库
    int result = sqlite3_open(dbPath.UTF8String, &cw_database);
    if (result != SQLITE_OK) {
        NSLog(@"打开数据库失败! : %d",result);
        return NO;
    }
    // 检测当前连接的数据库是否处于busy状态,处于则会回调CWDBBusyCallBack
    sqlite3_busy_handler(cw_database, &CWDBBusyCallBack, (void *)(cw_database));
    
    return YES;
}

b、关闭数据库
传一个sqlite3的操作连接即可以将连接关闭

SQLITE_API int sqlite3_close(sqlite3*);

帖上我们对应的代码

+ (void)closeDB {
    if (cw_database) {
        sqlite3_close(cw_database);
        cw_database = nil;
    }
}

c、进行单元测试

选择如下图创建一个单元测试的类
unitTest.png

然后写上我们的测试代码
test.png

点击箭头所指向的框框,则只能测试本函数,通过第43行的断言来判断result是否为YES,是YES则测试通过,然后框框内会变成一个绿色的勾,测试不通过则会变成红色的叉,然后我们看看我们对应的位置有没有成功创建一个数据库,最终我们发现测试通过。

3、根据模型对象,创建对应的数据库表格;

a、调用sqlite3的API创建表格
sqlite为我们提供下面这个方法在执行sql语句

//数据库执行语句
SQLITE_API int sqlite3_exec(
  sqlite3*,                                  /* sqlite3的操作连接 */
  const char *sql,                           /* SQL语句 */
  int (*callback)(void*,int,char**,char**),  /* 回调函数 */
  void *,                                    /* 第一个参数的回调 */
  char **errmsg                              /* 错误信息 */
);

做数据库执行语句时,我们的逻辑是:

  • 1.打开数据库
  • 2.数据库执行语句
  • 3.关闭数据库
    我们在CWDatabase封装一个方法,用来执行数据库操作,第一个参数为需要执行的sql语句,第二个参数为userId,用来打开对应的数据库
+ (BOOL)execSQL:(NSString *)sql uid:(NSString *)uid {
    // 打开数据库
    if (![self openDB:uid]) {
        return NO;
    }
    // 执行语句
    char *errmsg = nil;
    int result = sqlite3_exec(cw_database, sql.UTF8String, nil, nil, &errmsg);
    // 关闭数据库
    [self closeDB];
    // 执行语句失败则抛出错误信息
    if (result != SQLITE_OK) {
        NSLog(@"exec sql error : %s",errmsg);
        return NO;
    }
    return YES;
}

同样的,我们对这个方法进行单元测试,在这里我们需要自己写sql的执行语句,测试传uid与不传uid两种情况,并断言会成功

- (void)testOpenDBAndExceSql {
    NSString *sql = @"create table if not exists Student(id integer , name text not null, age integer, score real,primary key(id))";
    
    BOOL result = [CWDatabase execSQL:sql uid:nil];
    XCTAssertEqual(YES, result);
    
    BOOL result1 = [CWDatabase execSQL:sql uid:@"Chavez"];
    XCTAssertEqual(YES, result1);
}

最终成功创建对应的两个数据库以及表格

表格.png

b、面向模型来创建数据库表格
做完以上步骤,我们成功的通过调用sqlite3的API准确的创建了一个表格,但是还是要背sql语句,和我们一开始的初衷相违背,我们需要的是简单、简单、简单、无脑操作。。所以我们需要想个办法来省去背sql语句这一步骤。
简单分析一下创建表格的sql语句,我们会发现可以分成下面的结构

create table if not exists Student(id integer , name text not null, age integer, score real,primary key(id))
create table if not exists 表名(字段1 字段1类型,字段2 字段2类型 ....., primary key(字段))

其中字段和字段类型,可以对应成操作模型的成员变量以及成员变量的类型,所以,我们通过runtime的方法,获取到模型的所有成员变量以及所有成员变量对应的类型。
我们在CWModelTool这个类里面封装一个方法来获取模型所有成员变量的类型以及名称,封装成一个字典返回 字典的类型为 {成员变量名称(key) :成员变量类型(value)}

+ (NSDictionary *)classIvarNameAndTypeDic:(Class)cls {
    unsigned int outCount = 0;
    Ivar *varList = class_copyIvarList(cls, &outCount);
    NSMutableDictionary *nameTypeDic = [NSMutableDictionary dictionary];
    
    for (int i = 0; i < outCount; i++) {
        Ivar ivar = varList[i];
        // 1.获取成员变量名称
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        
        if ([ivarName hasPrefix:@"_"]) {
            ivarName = [ivarName substringFromIndex:1];
        }
        
        // 2.获取成员变量类型 @\"
        NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
        
        type = [type stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"@\""]];
        
        [nameTypeDic setValue:type forKey:ivarName];
        
    }
    
    return nameTypeDic;
}

然后我们进行单元测试,创建CWModelToolTests的单元测试并创建一个student的模型,模型的成员变量为

@interface Student : NSObject<CWModelProtocol>
{
    float score;
}
@property (nonatomic,assign) int stuId; // 学号
@property (nonatomic,copy) NSString *name;
@property (nonatomic,assign) int age;
@property (nonatomic,assign) int height;
@end

然后我们在CWModelToolTests写一个单元测试的方法

- (void)testIvarNameTypeDict {
    NSDictionary *dict = [CWModelTool classIvarNameAndTypeDic:[Student class]];
    NSLog(@"Student------%@",dict);
    XCTAssertNotNil(dict);
}

然后我们运行这个测试函数,在控制台得到如下打印:

2017-12-07 16:41:23.934525+0800 CWDB[34996:3867985] Student------{
    age = i;
    height = i;
    name = NSString;
    score = f;
    stuId = i;
}

与对应模型的成员变量一致,测试通过。
获取了对应的成员变量的字典后,我们需要将这个字典转换成sql对应的语句,下面加粗的部分
create table if not exists 表名(字段1 字段1类型,字段2 字段2类型 ....., primary key(字段))
在此之前,我们还要进行另一个转换,因为数据库里面对应的类型和OC的类型并不一样,所以要变一变

暂时不考虑OC对象(数组,字典 等...)以及自定义对象的情况

 OC                                      数据库
 i :         整型                        integer
 q:          long                       integer
 Q:          long long                  integer   
 B:          bool                       integer
 d:          double                     real
 f:          float                      real
 NSString:   字符串                      text      
 NSData:     二进制                      blob   

我们封装一个函数来进行字典的转换,我们要得到的字典类型{成员变量名称(key) :成员变量对应数据库的类型(value)}

+ (NSDictionary *)classIvarNameAndSqlTypeDic:(Class)cls {
    // 获取模型的所有成员变量
    NSMutableDictionary *classDict = [[self classIvarNameAndTypeDic:cls] mutableCopy];
    
    [classDict enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL * _Nonnull stop) {
        // 对应的数据库的类型重新赋值
        classDict[key] = [self getSqlType:obj];
    }];
    return classDict;
}

// oc类型转换到数据库的类型
+ (NSString*)getSqlType:(NSString*)type{
    if([type isEqualToString:@"i"]||[type isEqualToString:@"I"]||
       [type isEqualToString:@"s"]||[type isEqualToString:@"S"]||
       [type isEqualToString:@"q"]||[type isEqualToString:@"Q"]||
       [type isEqualToString:@"b"]||[type isEqualToString:@"B"]||
       [type isEqualToString:@"c"]||[type isEqualToString:@"C"]|
       [type isEqualToString:@"l"]||[type isEqualToString:@"L"]) {
        return @"integer";
    }else if([type isEqualToString:@"f"]||[type isEqualToString:@"F"]||
             [type isEqualToString:@"d"]||[type isEqualToString:@"D"]){
        return @"real";
    }else if ([type isEqualToString:@"NSData"]) {
        return @"blob";
    }else{
        return @"text";
    }
}

这里我们就不在贴测试的代码了,反正是成功的。
然后我们将以上方法获取的字典转换成我们需要的sql的字符串,也就是这种类型 字段1 字段1类型,字段2 字段2类型 .....声明主键后面在拼接

+ (NSString *)sqlColumnNamesAndTypesStr:(Class)cls {
    NSDictionary *sqlDict = [[self classIvarNameAndSqlTypeDic:cls] mutableCopy];
    NSMutableArray *nameTypeArr = [NSMutableArray array];

    [sqlDict enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL * _Nonnull stop) {
        [nameTypeArr addObject:[NSString stringWithFormat:@"%@ %@",key,obj]];
    }];
    
    return [nameTypeArr componentsJoinedByString:@","];
}

同理。。这里我们也不测试了。反正是成功的
create table if not exists 表名(字段1 字段1类型,字段2 字段2类型 ....., primary key(字段))
这段语句,我们还差一个表名和主键没有获取下面我们给CWModelTool封装一个方法来获取表名,表名我们是通过模型的类名拼接targetid组成的。

+ (NSString *)tableName:(Class)cls targetId:(NSString *)targetId {
    return [NSString stringWithFormat:@"%@%@",NSStringFromClass(cls),targetId];
}

在获取主键这里,有两种常用的方式,一种是设计一个自动增长的主键,另一种是学习realm的方式,通过代理让用户为模型返回一个主键,这里我们使用后者。在CWModelProtocol声明一个协议方法,且这个方法是必须实现的

@protocol CWModelProtocol <NSObject>

@required
/**
 操作模型必须实现的方法,通过这个方法获取主键信息
 
 @return 主键字符串
 */
+ (NSString *)primaryKey;

@end

接下来,我们封装创建数据库表格的最终方法
在CWSqliteModelTool内,封装一个方法

// uid用来确认哪个数据库,targetId用来区分数据库表名
+ (BOOL)createSQLTable:(Class)cls uid:(NSString *)uid targetId:(NSString *)targetId {
    // 创建数据库表的语句
    // create table if not exists 表名(字段1 字段1类型(约束),字段2 字段2类型(约束)....., primary key(字段))
    // 获取数据库表名
    NSString *tableName = [CWModelTool tableName:cls targetId:targetId];
    
    if (![cls respondsToSelector:@selector(primaryKey)]) {
        NSLog(@"如果想要操作这个模型,必须要实现+ (NSString *)primaryKey;这个方法,来告诉我主键信息");
        return NO;
    }
    // 获取主键
    NSString *primaryKey = [cls primaryKey];
    if (!primaryKey) {
        NSLog(@"你需要指定一个主键来创建数据库表");
        return NO;
    }
    
    NSString *createTableSql = [NSString stringWithFormat:@"create table if not exists %@(%@, primary key(%@))",tableName,[CWModelTool sqlColumnNamesAndTypesStr:cls],primaryKey];
    
    return [CWDatabase execSQL:createTableSql uid:uid];
}

然后。。我们来进行单元测试
新建一个CWSqliteModelToolTests单元测试类,用来测试CWSqliteModelTool的所有方法,然后新建一个Student模型,遵守CWModelProtocol协议,实现必须要的协议方法。

Student.h
#import <Foundation/Foundation.h>
#import "CWModelProtocol.h"

@interface Student : NSObject<CWModelProtocol>
{
    float score;
}
@property (nonatomic,assign) int stuId; // 学号
@property (nonatomic,copy) NSString *name;
@property (nonatomic,assign) int age;
@property (nonatomic,assign) int height;
@end


Student.m
#import "Student.h"

@implementation Student
// 返回主键信息
+ (NSString *)primaryKey {
    return @"stuId";
}

@end

测试创建数据库表格方法

- (void)testCreateSQLTable {
    BOOL result = [CWSqliteModelTool createSQLTable:[Student class] uid:@"CWDB" targetId:@"53class"];
    XCTAssertTrue(result);
}

运行之后得到如下结果


image.png

在对应的路径下,创建了CWDB数据库,并在数据库里面创建一张Student53class的表,表的列名与Student模型的成员变量一一对应,测试通过!

用户如果要创建一个表,只需要调用这个方法

+ (BOOL)createSQLTable:(Class)cls uid:(NSString *)uid targetId:(NSString *)targetId;

将模型的类型,用户id(可以为空)以及目标id(可以为空)传过来,我们就会创建对应的数据库并打开,解析模型,创建对应的表格,关闭数据库。

4、本篇结束

在此,我们通过调用sqlite的API,通过runtime,将创建数据库表格的操作用非常简洁的API开放出来,目前还是很成功的,在下一篇文章,我们会实现数据库插入、查询、更新操作。。在更后面的文章,我们会实现删除、存储模型内嵌套OC对象,以及数组内嵌套自定义模型,以及多线程安全等的处理。。

每一章的代码我会上传到github上。。并打tag作为一个节点。欢迎大家下载并查找漏洞,因为。我也是第一次封装。一起学习,一起进步。

github地址
tag为1.0.0,你可以在下图的位置找到他,并下载下来。

image.png

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

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

目前第二篇文章已经出炉,地址:从0开始弄一个面向OC数据库(二)

最后再为大家推荐一个0耦合的侧滑框架。
一行代码集成超低耦合的侧滑功能

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

推荐阅读更多精彩内容