首先,在上一篇文章从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 这种多条件的,反正思路都是同样的,就是多干点苦力活罢了~
在此,咱们将数据库更新、数据迁移操做合并到了插入数据的方法内,成为了用户背后默默付出的女人,而后数据删除这种对目前的咱们来讲小意思的东西也实现了。下一篇文章,咱们要实现复杂数据类型和对象的存储,好比NSArray,NSDictionary,NSObject,CGRect,UIImage等....以及数组内嵌套模型,嵌套字典等等。。。而后最后的文章咱们会对多线程安全进行处理,欢迎围观。
github地址 本次的代码,tag为1.2.0,你能够在release下找到对应的tag下载下来
最后以为有用的同窗,但愿能给本文点个喜欢,给github点个star以资鼓励,谢谢你们。
PS: 由于我也是一边封装,一边写文章。效率可能比较低,问题也会有,欢迎你们向我抛issue,有更好的思路也欢迎你们留言!
最后再为你们提供上两篇文章的地址。
以及一个0耦合的仿QQ侧滑框架: 一行代码集成超低耦合的侧滑功能
啦啦啦啦。。生命不止。。推广不断😁