阅读 448

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

前言

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

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

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

功能实现

数据库更新、数据迁移

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

首先判断是否需要更新
-- 获取数据库对应的表格创建时的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这些我们就不详细说了。

如何使用事务

在开始执行sql语句之前,我们开启事务,然后逐条执行sql语句,如果某一条sql语句执行失败,则进行回滚,当执行回滚时,之前执行的操作会被取消,数据库会回到开始事务的阶段,当所有sql语句都执行成功之后提交事务即可。

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

理论补充完了,现在我们开始上代码,用代码一一实以上的思路

首先获取数据库表格的所有字段,在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];
        
    }
    // 6、删除原来的表格
    NSString *deleteOldTable = [NSString stringWithFormat:@"drop table if exists %@",tableName];
    [execSqls addObject:deleteOldTable];
    // 7、修改临时表格的名字
    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侧滑框架: 一行代码集成超低耦合的侧滑功能

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