从0开始弄一个面向OC数据库(三)--数据库升级,数据迁移,删除数据

前言

首先,在上一篇文章从0开始弄一个面向OC数据库(二),讲解了如何向数据库保存或更新一个模型、如何查询数据库里面的数据。其次,本篇要说的内容有:git

  • 数据库更新、数据迁移。
  • 删除数据

使用场景: 随着项目的迭代,数据库的内容会愈来愈多,假若有一天,保存数据库的数据字段增长或者减小怎么办?好比第一个版本,咱们保存了学生的姓名,学号,年龄,成绩。到了第10个版本,咱们要多保存一项学生的身高,甚至还要再保存学生的体重、性别等等。。怎么办?难道要把以前的数据库表删了,从新建一个数据库表,而后从新插入数据吗?若是我录入了1万个学生的数据,从新开始工做量很是大,以前的数据也会丢失。因此!咱们必需要实现数据库更新,以及数据迁移。要增字段就增,要减就减,更新一下就行了。。删除数据的场景咱就很少说了,有个学生转学了,得把他的资料移除吧~ github

功能实现

数据库更新、数据迁移

当用户对model进行insertOrUpdate的时候,若是这个model里新增了成员变量或者删除了成员变量,这时候咱们去进行保存数据是会失败的,由于保存的模型的字段和数据库表结构的字段对应不上。这时候咱们就须要进行数据更新。要实现数据库更新,得先缕一缕咱们的思路:sql

首先判断是否须要更新
-- 获取数据库对应的表格建立时的sql语句 从中拿到全部的字段        获得A数组
-- 获取模型中的全部成员变量                                  获得B数组
-- 比较AB数组 若是相等 则不须要更新表 不相等则更新表,而且迁移数据

而后进行迁移数据步骤
-- 根据model的字段,建立一个新的临时表格。
create table if not exists cwstu_tmp(stuNum integer, name text, age integer, address text, primary key(stuNum));
-- 从原来的表格里面,将主键存在的数据从原来的表格插入至新的临时表格
--insert into cwstu_tmp(stuNum) select stuNum from CWStu;
-- 经过主键将老表对应字段的值更新到新表内。
--update cwstu_tmp set name = (select name from cwstu where cwstu_tmp.stuNum = cwstu.stuNum);
-- update cwstu_tmp set age = (select age from cwstu where cwstu_tmp.stuNum = cwstu.stuNum);
-- 删除原有的表格
-- drop table if exists cwstu;
-- 更改临时表格的名字,用户并不知道其实咱们偷天换日了
-- alter table cwstu_tmp rename to cwstu;
复制代码

以上的语句要所有执行成功,数据迁移才算完成,若是执行到一半失败,那么数据库里面可能就会平白无故多了一个临时表,和一些半完成的数据,显然咱们要避免这个问题,因而咱们使用到数据库事务数据库

简单介绍一下数据库事务:数组

通常咱们经常使用的方法有3个 BEGIN TRANSACTION(开始事务) COMMIT TRANSACTION(提交事务)ROLLBACK TRANSACTION(回滚) 而后事务有4个基本属性ACID这些咱们就不详细说了。安全

如何使用事务bash

在开始执行sql语句以前,咱们开启事务,而后逐条执行sql语句,若是某一条sql语句执行失败,则进行回滚,当执行回滚时,以前执行的操做会被取消,数据库会回到开始事务的阶段,当全部sql语句都执行成功以后提交事务便可。多线程

探究数据库是如何进行数据回滚的呢?sqlitie数据库回滚是经过回滚日志实现的,全部事务进行的修改都会先记录到这个回滚日志中,而后在对数据库中的对应行进行写入,进行回滚时,会根据回滚日志滚回以前的状态,打个比方:SVN、git每次提交都会有log,当有一天你想要回退到某个版本,只须要选在对应的log记录revert就能够了,sqlite的回滚相似这样。。还有一个注意点,事务操做必定要是同一个数据库,以及同一个数据库操做句柄。框架

理论补充完了,如今咱们开始上代码,用代码一一实以上的思路ide

首先获取数据库表格的全部字段,在CWSqliteTableTool封装一个方法

// 获取表的全部字段名,排序后返回
+ (NSArray *)allTableColumnNames:(NSString *)tableName uid:(NSString *)uid {
    
    NSString *queryCreateSqlStr = [NSString stringWithFormat:@"select sql from sqlite_master where type = 'table' and name = '%@'",tableName];
    NSArray *dictArr = [CWDatabase querySql:queryCreateSqlStr uid:uid];
    NSMutableDictionary *dict = dictArr.firstObject;
//    NSLog(@"---------------%@",dict);
    NSString *createSql = dict[@"sql"];
    if (createSql.length == 0) {
        return nil;
    }
    // sql = "CREATE TABLE Student(age integer,stuId integer,score real,height integer,name text, primary key(stuId))";
    createSql = [createSql stringByReplacingOccurrencesOfString:@"\"" withString:@""];
    createSql = [createSql stringByReplacingOccurrencesOfString:@"\n" withString:@""];
    createSql = [createSql stringByReplacingOccurrencesOfString:@"\t" withString:@""];
    
    NSString *nameTypeStr = [createSql componentsSeparatedByString:@"("][1];
    NSArray *nameTypeArray = [nameTypeStr componentsSeparatedByString:@","];
    
    NSMutableArray *names = [NSMutableArray array];
    
    for (NSString *nameType in nameTypeArray) {
        // 去掉主键
        if ([nameType containsString:@"primary"]) {
            continue;
        }
        // 压缩掉字符串里面的 @“ ”  只压缩两端的
        NSString *nameType2 = [nameType stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@" "]];
        
        // age integer
        NSString *name = [nameType2 componentsSeparatedByString:@" "].firstObject;
        [names addObject:name];
    }
    
    [names sortUsingComparator:^NSComparisonResult(NSString *obj1, NSString *obj2) {
        return [obj1 compare:obj2];
    }];
    
    return names;
}
复制代码

而后再获取模型中的全部成员变量,在CWModelTool内

+ (NSArray *)allIvarNames:(Class)cls {
    NSDictionary *dict = [self classIvarNameAndTypeDic:cls];
    NSArray *names = dict.allKeys;
    // 排序
    names = [names sortedArrayUsingComparator:^NSComparisonResult(id  _Nonnull obj1, id  _Nonnull obj2) {
        return [obj1 compare:obj2];
    }];
    return names;
}
复制代码

比较两个数组是够相等,相等则不须要更新,不然进行数据库表更新

// 数据库表是否须要更新
+ (BOOL)isTableNeedUpdate:(Class)cls uid:(NSString *)uid targetId:(NSString *)targetId {
    
    NSArray *modelNames = [CWModelTool allIvarNames:cls];
    
    NSString *tableName = [CWModelTool tableName:cls targetId:targetId];
    NSArray *tableNames = [self allTableColumnNames:tableName uid:uid];
    
    return ![modelNames isEqualToArray:tableNames];
}
复制代码

判断数据库属否须要更新作完了,咱们接下来要实现一个方法用事务控制并一次执行多个sql语句,在CWDatabase内:

#pragma mark - 事务
+ (void)beginTransaction:(NSString *)uid {
    [self execSQL:@"BEGIN TRANSACTION" uid:uid];
}

+ (void)commitTransaction:(NSString *)uid {
     [self execSQL:@"COMMIT TRANSACTION" uid:uid];
}

+ (void)rollBackTransaction:(NSString *)uid {
     [self execSQL:@"ROLLBACK TRANSACTION" uid:uid];
}

// 执行多个sql语句
+ (BOOL)execSqls:(NSArray <NSString *>*)sqls uid:(NSString *)uid {
    // 事务控制全部语句必须返回成功,才算执行成功
    [self beginTransaction:uid];
    
    for (NSString *sql in sqls) {
        BOOL result = [self execSQL:sql uid:uid];
        if (result == NO) {
            [self rollBackTransaction:uid];
            return NO;
        }
    }
    [self commitTransaction:uid];
    return YES;
}
复制代码

作完以上步骤,接下来咱们主要来完成数据迁移的多个sql语句的拼接,而后执行。

#pragma mark - 更新数据库表结构、字段更名、数据迁移
// 更新表并迁移数据
+ (BOOL)updateTable:(Class)cls uid:(NSString *)uid targetId:(NSString *)targetId{
    
    // 1.建立一个拥有正确结构的临时表
    // 1.1 获取表格名称
    NSString *tmpTableName = [CWModelTool tmpTableName:cls targetId:targetId];
    NSString *tableName = [CWModelTool tableName:cls targetId:targetId];
    
    if (![cls respondsToSelector:@selector(primaryKey)]) {
        NSLog(@"若是想要操做这个模型,必需要实现+ (NSString *)primaryKey;这个方法,来告诉我主键信息");
        return NO;
    }
    
    // 保存全部须要执行的sql语句
    NSMutableArray *execSqls = [NSMutableArray array];
    
    NSString *primaryKey = [cls primaryKey];
    // 1.2 获取一个模型里面全部的字段,以及类型
    NSString *createTableSql = [NSString stringWithFormat:@"create table if not exists %@(%@, primary key(%@))",tmpTableName,[CWModelTool sqlColumnNamesAndTypesStr:cls],primaryKey];
    
    [execSqls addObject:createTableSql];
    
    // 2.根据主键插入数据
    //--insert into cwstu_tmp(stuNum) select stuNum from CWStu;
    NSString *inserPrimaryKeyData = [NSString stringWithFormat:@"insert into %@(%@) select %@ from %@",tmpTableName,primaryKey,primaryKey,tableName];
    
    [execSqls addObject:inserPrimaryKeyData];
    
    // 3.根据主键,把全部的数据插入到怕新表里面去
    NSArray *oldNames = [CWSqliteTableTool allTableColumnNames:tableName uid:uid];
    NSArray *newNames = [CWModelTool allIvarNames:cls];
    
    // 4.获取改名字典
    NSDictionary *newNameToOldNameDic = @{};
    if ([cls respondsToSelector:@selector(newNameToOldNameDic)]) {
        newNameToOldNameDic = [cls newNameToOldNameDic];
    }
    
    for (NSString *columnName in newNames) {
        NSString *oldName = columnName;
        // 找映射的旧的字段名称
        if ([newNameToOldNameDic[columnName] length] != 0) {
            if ([oldNames containsObject:newNameToOldNameDic[columnName]]) {
                oldName = newNameToOldNameDic[columnName];
            }
        }
        // 若是老表包含了新的列名,应该从老表更新到临时表格里面
        if ((![oldNames containsObject:columnName] && [columnName isEqualToString:oldName]) ) {
            continue;
        }
        // --update cwstu_tmp set name = (select name from cwstu where cwstu_tmp.stuNum = cwstu.stuNum);
        // 5.更新数据
        NSString *updateSql = [NSString stringWithFormat:@"update %@ set %@ = (select %@ from %@ where %@.%@ = %@.%@)",tmpTableName,columnName,oldName,tableName,tmpTableName,primaryKey,tableName,primaryKey];
        
        [execSqls addObject:updateSql];
        
    }
    // 六、删除原来的表格
    NSString *deleteOldTable = [NSString stringWithFormat:@"drop table if exists %@",tableName];
    [execSqls addObject:deleteOldTable];
    // 七、修改临时表格的名字
    NSString *renameTableName = [NSString stringWithFormat:@"alter table %@ rename to %@",tmpTableName,tableName];
    [execSqls addObject:renameTableName];
    
    BOOL result = [CWDatabase execSqls:execSqls uid:uid];
    
    [CWDatabase closeDB];
    
    return result;
}
复制代码

测试代码就不贴了,最终测试是没问题的,固然咱们还有一部分工做没有完成,为了使用咱们框架的人更方便,咱们必须把这个方法整合到插入或者更新数据那个方法里面,也就是说,当用户保存一条数据时,咱们先给他判断是否须要更新数据库表结构,若是须要,咱们进行乾坤大挪移默默的帮他把数据库迁移了,而后再进行数据插入或更新。。就像每个成功的男人背后都有一个默默付出的女人,咱们就给用户来当这个女人吧~😁咱们在以前封装的insertOrUpdateModel:方法内增长一段代码

#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];
    }else { // 若是表格存在,则检测表格是否须要更新
        if ([CWSqliteTableTool isTableNeedUpdate:cls uid:uid targetId:targetId] ) {
            BOOL result = [self updateTable:cls uid:uid targetId:targetId];
            if (!result) {
                NSLog(@"更新数据库表结构失败!插入或更新数据失败!");
                return NO;
            }
        }
    }
    // 这里是之前的逻辑......
}
复制代码

数据删除

咱们把复杂的流程实现以后,数据删除相对咱们来讲,简直是小菜一碟。。很少BB,直接上代码

// 根据模型的主键来删除
+ (BOOL)deleteModel:(id)model uid:(NSString *)uid targetId:(NSString *)targetId {
    Class cls = [model class];
    NSString *tableName = [CWModelTool tableName:cls targetId:targetId];
    if (![cls respondsToSelector:@selector(primaryKey)]) {
        NSLog(@"若是想要操做这个模型,必需要实现+ (NSString *)primaryKey;这个方法,来告诉我主键信息");
        return NO;
    }
    NSString *primaryKey = [cls primaryKey];
    id primaryValue = [model valueForKeyPath:primaryKey];
    NSString *deleteSql = [NSString stringWithFormat:@"delete from %@ where %@ = '%@'",tableName,primaryKey,primaryValue];
    
    // 执行数据库
    BOOL result = [CWDatabase execSQL:deleteSql uid:uid];
    // 关闭数据库
    [CWDatabase closeDB];
    
    return result;
}
复制代码

上面就是进行删除的一个场景,为了方便用户,咱们固然要封装更多的场景,这个也很是简单,无非就是拼接一下sql语句delete from %@ where %@ = '%@'还能够加and,or 这种多条件的,反正思路都是同样的,就是多干点苦力活罢了~

4.本篇结束

在此,咱们将数据库更新、数据迁移操做合并到了插入数据的方法内,成为了用户背后默默付出的女人,而后数据删除这种对目前的咱们来讲小意思的东西也实现了。下一篇文章,咱们要实现复杂数据类型和对象的存储,好比NSArray,NSDictionary,NSObject,CGRect,UIImage等....以及数组内嵌套模型,嵌套字典等等。。。而后最后的文章咱们会对多线程安全进行处理,欢迎围观。

github地址 本次的代码,tag为1.2.0,你能够在release下找到对应的tag下载下来

最后以为有用的同窗,但愿能给本文点个喜欢,给github点个star以资鼓励,谢谢你们。

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

最后再为你们提供上两篇文章的地址。

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

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

以及一个0耦合的仿QQ侧滑框架: 一行代码集成超低耦合的侧滑功能

啦啦啦啦。。生命不止。。推广不断😁

相关文章
相关标签/搜索