從0開始弄一個面向OC資料庫(一)--開啟、關閉資料庫,動態建表

___發表於2017-12-14

近期又學習了一下資料庫的東西,決定花時間封裝一個資料庫,看了一些原始碼,感覺有一定的可行度,所以先把第一篇(本篇)文章發上來。啥都沒有發上來幹啥捏?有時候容易半途而廢,先發上來,斷自己後路!一定得寫,不然就是說話不算話了。

100婚

1、我們要做什麼?

這個文章,我們要從0開始封裝一個面向OC物件的資料庫,想了解怎麼做的,可以一起陪伴一下,所有的流程細節我都會寫在文章內,因為我也第一次搞這個東西,有興趣的話我們們可以一起討論和提升。

2、做成什麼樣子?

當然是期望做到簡()單()運算元據庫,不需要背語句,也不需要解析模型,類似realm這種騷操作[realm addObject:obj];obj就給你存到資料庫了。

額
當然,我們不能和行業航母去比較,但是封裝出來的東西一定要簡單適用,安全可靠。我們封裝的資料庫,操作必須簡單,功能必須全面(增刪查改一樣不能少,資料庫遷移,資料庫欄位改名也得支援,多執行緒安全也得考慮,最重要的是常用的資料型別我們都得支援)。

3、簡單介紹

(本來說好的只發文章斷自己後路,但是還是先實現一部分功能,免得顯得太空洞了) 本篇主要實現的功能有:

  • 1、整個庫的結構設計;
  • 2、開啟並建立資料庫、關閉資料庫;
  • 3、根據模型物件,建立對應的資料庫表格; 在做功能之前,先簡單的介紹一下資料庫相關的東西:
    資料庫.png
    以上的圖表示,名稱為CWDB的資料庫,裡面有一張Student53class的表,表有的欄位為age,stuId,score,height,name。也可以解釋為CWDB這個資料庫裡面存了53班每個學生的年齡,學號,成績,身高,名字,可惜的是53班目前沒有一個學生=。= 一個資料庫裡面可以有多張名字不重複的表,一個表裡面可以有多條主鍵不一致的資料,每條資料指定一個欄位為主鍵當唯一標識。 檢視資料庫這裡我用了一個破解版的工具,現在提供給大家: 連結: https://pan.baidu.com/s/1eSIpTBW 密碼: 4rjm

4、開始動手

首先建立一個工程,並向工程中拖入libsqlite3.0.tbd這個庫

1、庫的結構設計

結構.png

2、開啟並建立資料庫、關閉資料庫

a、開啟並建立資料庫 sqlite3 向我們提供了這個介面,用來執行開啟資料庫操作,第一個引數為資料庫存的路徑,第二個引數為sqlite3的操作連線。 如果資料庫路徑下沒有資料庫,則建立一個資料庫並開啟,如果有則直接開啟資料庫

SQLITE_API int sqlite3_open(
  const char *filename,   /* Database filename (UTF-8) */
  sqlite3 **ppDb          /* OUT: SQLite db handle */
);
複製程式碼

我們封裝一個方法執行建立並開啟資料庫的程式碼,當傳uid的時候會以uid命名資料庫,如果沒傳將會預設資料庫名稱為CWDB,路徑我們寫在CWDatabase.m下,因為是測試階段,所以路徑設定在桌面上。需要自行修改路徑

+ (BOOL)openDB:(NSString *)uid {
    // 資料庫名稱
    NSString *dbName = @"CWDB.sqlite";
    if (uid.length != 0) {
        dbName = [NSString stringWithFormat:@"%@.sqlite", uid];
    }
    // 資料庫路徑
    NSString *dbPath = [kCWDBCachePath stringByAppendingPathComponent:dbName];
    // 開啟資料庫
    int result = sqlite3_open(dbPath.UTF8String, &cw_database);
    if (result != SQLITE_OK) {
        NSLog(@"開啟資料庫失敗! : %d",result);
        return NO;
    }
    // 檢測當前連線的資料庫是否處於busy狀態,處於則會回撥CWDBBusyCallBack
    sqlite3_busy_handler(cw_database, &CWDBBusyCallBack, (void *)(cw_database));
    
    return YES;
}
複製程式碼

b、關閉資料庫 傳一個sqlite3的操作連線即可以將連線關閉

SQLITE_API int sqlite3_close(sqlite3*);
複製程式碼

帖上我們對應的程式碼

+ (void)closeDB {
    if (cw_database) {
        sqlite3_close(cw_database);
        cw_database = nil;
    }
}
複製程式碼

c、進行單元測試 選擇如下圖建立一個單元測試的類

unitTest.png
然後寫上我們的測試程式碼
test.png
點選箭頭所指向的框框,則只能測試本函式,通過第43行的斷言來判斷result是否為YES,是YES則測試通過,然後框框內會變成一個綠色的勾,測試不通過則會變成紅色的叉,然後我們看看我們對應的位置有沒有成功建立一個資料庫,最終我們發現測試通過。

3、根據模型物件,建立對應的資料庫表格;

a、呼叫sqlite3的API建立表格 sqlite為我們提供下面這個方法在執行sql語句

//資料庫執行語句
SQLITE_API int sqlite3_exec(
  sqlite3*,                                  /* sqlite3的操作連線 */
  const char *sql,                           /* SQL語句 */
  int (*callback)(void*,int,char**,char**),  /* 回撥函式 */
  void *,                                    /* 第一個引數的回撥 */
  char **errmsg                              /* 錯誤資訊 */
);
複製程式碼

做資料庫執行語句時,我們的邏輯是:

  • 1.開啟資料庫
  • 2.資料庫執行語句
  • 3.關閉資料庫 我們在CWDatabase封裝一個方法,用來執行資料庫操作,第一個引數為需要執行的sql語句,第二個引數為userId,用來開啟對應的資料庫
+ (BOOL)execSQL:(NSString *)sql uid:(NSString *)uid {
    // 開啟資料庫
    if (![self openDB:uid]) {
        return NO;
    }
    // 執行語句
    char *errmsg = nil;
    int result = sqlite3_exec(cw_database, sql.UTF8String, nil, nil, &errmsg);
    // 關閉資料庫
    [self closeDB];
    // 執行語句失敗則丟擲錯誤資訊
    if (result != SQLITE_OK) {
        NSLog(@"exec sql error : %s",errmsg);
        return NO;
    }
    return YES;
}
複製程式碼

同樣的,我們對這個方法進行單元測試,在這裡我們需要自己寫sql的執行語句,測試傳uid與不傳uid兩種情況,並斷言會成功

- (void)testOpenDBAndExceSql {
    NSString *sql = @"create table if not exists Student(id integer , name text not null, age integer, score real,primary key(id))";
    
    BOOL result = [CWDatabase execSQL:sql uid:nil];
    XCTAssertEqual(YES, result);
    
    BOOL result1 = [CWDatabase execSQL:sql uid:@"Chavez"];
    XCTAssertEqual(YES, result1);
}
複製程式碼

最終成功建立對應的兩個資料庫以及表格

表格.png
b、面向模型來建立資料庫表格 做完以上步驟,我們成功的通過呼叫sqlite3的API準確的建立了一個表格,但是還是要背sql語句,和我們一開始的初衷相違背,我們需要的是簡單、簡單、簡單、無腦操作。。所以我們需要想個辦法來省去背sql語句這一步驟。 簡單分析一下建立表格的sql語句,我們會發現可以分成下面的結構

create table if not exists Student(id integer , name text not null, age integer, score real,primary key(id))
create table if not exists 表名(欄位1 欄位1型別,欄位2 欄位2型別 ....., primary key(欄位))
複製程式碼

其中欄位和欄位型別,可以對應成操作模型的成員變數以及成員變數的型別,所以,我們通過runtime的方法,獲取到模型的所有成員變數以及所有成員變數對應的型別。 我們在CWModelTool這個類裡面封裝一個方法來獲取模型所有成員變數的型別以及名稱,封裝成一個字典返回 字典的型別為 {成員變數名稱(key) :成員變數型別(value)}

+ (NSDictionary *)classIvarNameAndTypeDic:(Class)cls {
    unsigned int outCount = 0;
    Ivar *varList = class_copyIvarList(cls, &outCount);
    NSMutableDictionary *nameTypeDic = [NSMutableDictionary dictionary];
    
    for (int i = 0; i < outCount; i++) {
        Ivar ivar = varList[i];
        // 1.獲取成員變數名稱
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        
        if ([ivarName hasPrefix:@"_"]) {
            ivarName = [ivarName substringFromIndex:1];
        }
        
        // 2.獲取成員變數型別 @\"
        NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
        
        type = [type stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"@\""]];
        
        [nameTypeDic setValue:type forKey:ivarName];
        
    }
    
    return nameTypeDic;
}
複製程式碼

然後我們進行單元測試,建立CWModelToolTests的單元測試並建立一個student的模型,模型的成員變數為

@interface Student : NSObject<CWModelProtocol>
{
    float score;
}
@property (nonatomic,assign) int stuId; // 學號
@property (nonatomic,copy) NSString *name;
@property (nonatomic,assign) int age;
@property (nonatomic,assign) int height;
@end
複製程式碼

然後我們在CWModelToolTests寫一個單元測試的方法

- (void)testIvarNameTypeDict {
    NSDictionary *dict = [CWModelTool classIvarNameAndTypeDic:[Student class]];
    NSLog(@"Student------%@",dict);
    XCTAssertNotNil(dict);
}
複製程式碼

然後我們執行這個測試函式,在控制檯得到如下列印:

2017-12-07 16:41:23.934525+0800 CWDB[34996:3867985] Student------{
    age = i;
    height = i;
    name = NSString;
    score = f;
    stuId = i;
}
複製程式碼

與對應模型的成員變數一致,測試通過。 獲取了對應的成員變數的字典後,我們需要將這個字典轉換成sql對應的語句,下面加粗的部分 create table if not exists 表名(欄位1 欄位1型別,欄位2 欄位2型別 ....., primary key(欄位)) 在此之前,我們還要進行另一個轉換,因為資料庫裡面對應的型別和OC的型別並不一樣,所以要變一變

暫時不考慮OC物件(陣列,字典 等...)以及自定義物件的情況

 OC                                      資料庫
 i :         整型                        integer
 q:          long                       integer
 Q:          long long                  integer   
 B:          bool                       integer
 d:          double                     real
 f:          float                      real
 NSString:   字串                      text      
 NSData:     二進位制                      blob   
複製程式碼

我們封裝一個函式來進行字典的轉換,我們要得到的字典型別**{成員變數名稱(key) :成員變數對應資料庫的型別(value)}**

+ (NSDictionary *)classIvarNameAndSqlTypeDic:(Class)cls {
    // 獲取模型的所有成員變數
    NSMutableDictionary *classDict = [[self classIvarNameAndTypeDic:cls] mutableCopy];
    
    [classDict enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL * _Nonnull stop) {
        // 對應的資料庫的型別重新賦值
        classDict[key] = [self getSqlType:obj];
    }];
    return classDict;
}

// oc型別轉換到資料庫的型別
+ (NSString*)getSqlType:(NSString*)type{
    if([type isEqualToString:@"i"]||[type isEqualToString:@"I"]||
       [type isEqualToString:@"s"]||[type isEqualToString:@"S"]||
       [type isEqualToString:@"q"]||[type isEqualToString:@"Q"]||
       [type isEqualToString:@"b"]||[type isEqualToString:@"B"]||
       [type isEqualToString:@"c"]||[type isEqualToString:@"C"]|
       [type isEqualToString:@"l"]||[type isEqualToString:@"L"]) {
        return @"integer";
    }else if([type isEqualToString:@"f"]||[type isEqualToString:@"F"]||
             [type isEqualToString:@"d"]||[type isEqualToString:@"D"]){
        return @"real";
    }else if ([type isEqualToString:@"NSData"]) {
        return @"blob";
    }else{
        return @"text";
    }
}
複製程式碼

這裡我們就不在貼測試的程式碼了,反正是成功的。 然後我們將以上方法獲取的字典轉換成我們需要的sql的字串,也就是這種型別 **欄位1 欄位1型別,欄位2 欄位2型別 .....**宣告主鍵後面在拼接

+ (NSString *)sqlColumnNamesAndTypesStr:(Class)cls {
    NSDictionary *sqlDict = [[self classIvarNameAndSqlTypeDic:cls] mutableCopy];
    NSMutableArray *nameTypeArr = [NSMutableArray array];

    [sqlDict enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL * _Nonnull stop) {
        [nameTypeArr addObject:[NSString stringWithFormat:@"%@ %@",key,obj]];
    }];
    
    return [nameTypeArr componentsJoinedByString:@","];
}
複製程式碼

同理。。這裡我們也不測試了。反正是成功的 create table if not exists 表名(欄位1 欄位1型別,欄位2 欄位2型別 ....., primary key(欄位)) 這段語句,我們還差一個表名和主鍵沒有獲取下面我們給CWModelTool封裝一個方法來獲取表名,表名我們是通過模型的類名拼接targetid組成的。

+ (NSString *)tableName:(Class)cls targetId:(NSString *)targetId {
    return [NSString stringWithFormat:@"%@%@",NSStringFromClass(cls),targetId];
}
複製程式碼

在獲取主鍵這裡,有兩種常用的方式,一種是設計一個自動增長的主鍵,另一種是學習realm的方式,通過代理讓使用者為模型返回一個主鍵,這裡我們使用後者。在CWModelProtocol宣告一個協議方法,且這個方法是必須實現的

@protocol CWModelProtocol <NSObject>

@required
/**
 操作模型必須實現的方法,通過這個方法獲取主鍵資訊
 
 @return 主鍵字串
 */
+ (NSString *)primaryKey;

@end
複製程式碼

接下來,我們封裝建立資料庫表格的最終方法 在CWSqliteModelTool內,封裝一個方法

// uid用來確認哪個資料庫,targetId用來區分資料庫表名
+ (BOOL)createSQLTable:(Class)cls uid:(NSString *)uid targetId:(NSString *)targetId {
    // 建立資料庫表的語句
    // create table if not exists 表名(欄位1 欄位1型別(約束),欄位2 欄位2型別(約束)....., primary key(欄位))
    // 獲取資料庫表名
    NSString *tableName = [CWModelTool tableName:cls targetId:targetId];
    
    if (![cls respondsToSelector:@selector(primaryKey)]) {
        NSLog(@"如果想要操作這個模型,必須要實現+ (NSString *)primaryKey;這個方法,來告訴我主鍵資訊");
        return NO;
    }
    // 獲取主鍵
    NSString *primaryKey = [cls primaryKey];
    if (!primaryKey) {
        NSLog(@"你需要指定一個主鍵來建立資料庫表");
        return NO;
    }
    
    NSString *createTableSql = [NSString stringWithFormat:@"create table if not exists %@(%@, primary key(%@))",tableName,[CWModelTool sqlColumnNamesAndTypesStr:cls],primaryKey];
    
    return [CWDatabase execSQL:createTableSql uid:uid];
}
複製程式碼

然後。。我們來進行單元測試 新建一個CWSqliteModelToolTests單元測試類,用來測試CWSqliteModelTool的所有方法,然後新建一個Student模型,遵守CWModelProtocol協議,實現必須要的協議方法。

Student.h
#import <Foundation/Foundation.h>
#import "CWModelProtocol.h"

@interface Student : NSObject<CWModelProtocol>
{
    float score;
}
@property (nonatomic,assign) int stuId; // 學號
@property (nonatomic,copy) NSString *name;
@property (nonatomic,assign) int age;
@property (nonatomic,assign) int height;
@end


Student.m
#import "Student.h"

@implementation Student
// 返回主鍵資訊
+ (NSString *)primaryKey {
    return @"stuId";
}

@end
複製程式碼

測試建立資料庫表格方法

- (void)testCreateSQLTable {
    BOOL result = [CWSqliteModelTool createSQLTable:[Student class] uid:@"CWDB" targetId:@"53class"];
    XCTAssertTrue(result);
}
複製程式碼

執行之後得到如下結果

image.png
在對應的路徑下,建立了CWDB資料庫,並在資料庫裡面建立一張Student53class的表,表的列名與Student模型的成員變數一一對應,測試通過!

使用者如果要建立一個表,只需要呼叫這個方法

+ (BOOL)createSQLTable:(Class)cls uid:(NSString *)uid targetId:(NSString *)targetId;
複製程式碼

將模型的型別,使用者id(可以為空)以及目標id(可以為空)傳過來,我們就會建立對應的資料庫並開啟,解析模型,建立對應的表格,關閉資料庫。

5、本篇結束

在此,我們通過呼叫sqlite的API,通過runtime,將建立資料庫表格的操作用非常簡潔的API開放出來,目前還是很成功的,在下一篇文章,我們會實現資料庫插入、查詢、更新操作。。在更後面的文章,我們會實現刪除、儲存模型內巢狀OC物件,以及陣列內巢狀自定義模型,以及多執行緒安全等的處理。。

每一章的程式碼我會上傳到github上。。並打tag作為一個節點。歡迎大家下載並查詢漏洞,因為。我也是第一次封裝。一起學習,一起進步。

github地址 tag為1.0.0,你可以在下圖的位置找到他,並下載下來。

image.png

最後覺得有用的同學,希望能給本文點個喜歡,給github點個star以資鼓勵,謝謝大家。

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

目前第二篇文章已經出爐,地址:從0開始弄一個面向OC資料庫(二)

最後再為大家推薦一個0耦合的側滑框架。 一行程式碼整合超低耦合的側滑功能

相關文章