前言
首先,在上一篇文章从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耦合的仿QQ侧滑框架: 一行代码集成超低耦合的侧滑功能
啦啦啦啦。。生命不止。。推广不断?