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

前言

首先,在上一篇文章從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 這種多條件的,反正思路都是同樣的,就是多幹點苦力活罷了~

4.本篇結束

在此,咱們將數據庫更新、數據遷移操做合併到了插入數據的方法內,成爲了用戶背後默默付出的女人,而後數據刪除這種對目前的咱們來講小意思的東西也實現了。下一篇文章,咱們要實現複雜數據類型和對象的存儲,好比NSArray,NSDictionary,NSObject,CGRect,UIImage等....以及數組內嵌套模型,嵌套字典等等。。。而後最後的文章咱們會對多線程安全進行處理,歡迎圍觀。

github地址 本次的代碼,tag爲1.2.0,你能夠在release下找到對應的tag下載下來

最後以爲有用的同窗,但願能給本文點個喜歡,給github點個star以資鼓勵,謝謝你們。

PS: 由於我也是一邊封裝,一邊寫文章。效率可能比較低,問題也會有,歡迎你們向我拋issue,有更好的思路也歡迎你們留言!

最後再爲你們提供上兩篇文章的地址。

從0開始弄一個面向OC數據庫(一)

從0開始弄一個面向OC數據庫(二)

以及一個0耦合的仿QQ側滑框架: 一行代碼集成超低耦合的側滑功能

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

相關文章
相關標籤/搜索