從0開始弄一個面向OC資料庫(三)--資料庫升級,資料遷移,刪除資料

___發表於2017-12-17

前言

首先,在上一篇文章從0開始弄一個面向OC資料庫(二),講解了如何向資料庫儲存或更新一個模型、如何查詢資料庫裡面的資料。其次,本篇要說的內容有:

  • 資料庫更新、資料遷移。
  • 刪除資料

使用場景: 隨著專案的迭代,資料庫的內容會越來越多,假如有一天,儲存資料庫的資料欄位增加或者減少怎麼辦?比如第一個版本,我們儲存了學生的姓名,學號,年齡,成績。到了第10個版本,我們要多儲存一項學生的身高,甚至還要再儲存學生的體重、性別等等。。怎麼辦?難道要把之前的資料庫表刪了,重新建一個資料庫表,然後重新插入資料嗎?如果我錄入了1萬個學生的資料,重新開始工作量非常大,之前的資料也會丟失。所以!我們必須要實現資料庫更新,以及資料遷移。要增欄位就增,要減就減,更新一下就好了。。刪除資料的場景我們就不多說了,有個學生轉學了,得把他的資料移除吧~

從0開始弄一個面向OC資料庫(三)--資料庫升級,資料遷移,刪除資料

功能實現

資料庫更新、資料遷移

當使用者對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側滑框架: 一行程式碼整合超低耦合的側滑功能

啦啦啦啦。。生命不止。。推廣不斷?

相關文章