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