首先,在上一篇文章從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側滑框架: 一行代碼集成超低耦合的側滑功能
啦啦啦啦。。生命不止。。推廣不斷😁