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

近期又學(xué)習(xí)了一下數(shù)據(jù)庫的東西,決定花時間封裝一個數(shù)據(jù)庫,看了一些源碼,感覺有一定的可行度,所以先把第一篇(本篇)文章發(fā)上來。啥都沒有發(fā)上來干啥捏?有時候容易半途而廢,先發(fā)上來,斷自己后路!一定得寫,不然就是說話不算話了。
100婚
1、我們要做什么?

這個文章,我們要從0開始封裝一個面向OC對象的數(shù)據(jù)庫,想了解怎么做的,可以一起陪伴一下,所有的流程細(xì)節(jié)我都會寫在文章內(nèi),因為我也第一次搞這個東西,有興趣的話咱們可以一起討論和提升。

2、做成什么樣子?

當(dāng)然是期望做到簡()單()操作數(shù)據(jù)庫,不需要背語句,也不需要解析模型,類似realm這種騷操作[realm addObject:obj];obj就給你存到數(shù)據(jù)庫了。


當(dāng)然,我們不能和行業(yè)航母去比較,但是封裝出來的東西一定要簡單適用,安全可靠。我們封裝的數(shù)據(jù)庫,操作必須簡單,功能必須全面(增刪查改一樣不能少,數(shù)據(jù)庫遷移,數(shù)據(jù)庫字段改名也得支持,多線程安全也得考慮,最重要的是常用的數(shù)據(jù)類型我們都得支持)。

3、簡單介紹

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

  • 1、整個庫的結(jié)構(gòu)設(shè)計;
  • 2、打開并創(chuàng)建數(shù)據(jù)庫、關(guān)閉數(shù)據(jù)庫;
  • 3、根據(jù)模型對象,創(chuàng)建對應(yīng)的數(shù)據(jù)庫表格;
    在做功能之前,先簡單的介紹一下數(shù)據(jù)庫相關(guān)的東西:
    數(shù)據(jù)庫.png

    以上的圖表示,名稱為CWDB的數(shù)據(jù)庫,里面有一張Student53class的表,表有的字段為age,stuId,score,height,name。也可以解釋為CWDB這個數(shù)據(jù)庫里面存了53班每個學(xué)生的年齡,學(xué)號,成績,身高,名字,可惜的是53班目前沒有一個學(xué)生=。=
    一個數(shù)據(jù)庫里面可以有多張名字不重復(fù)的表,一個表里面可以有多條主鍵不一致的數(shù)據(jù),每條數(shù)據(jù)指定一個字段為主鍵當(dāng)唯一標(biāo)識。
    查看數(shù)據(jù)庫這里我用了一個破解版的工具,現(xiàn)在提供給大家:
    鏈接: https://pan.baidu.com/s/1eSIpTBW 密碼: 4rjm
3、開始動手

首先創(chuàng)建一個工程,并向工程中拖入libsqlite3.0.tbd這個庫

1、庫的結(jié)構(gòu)設(shè)計
結(jié)構(gòu).png
2、打開并創(chuàng)建數(shù)據(jù)庫、關(guān)閉數(shù)據(jù)庫

a、打開并創(chuàng)建數(shù)據(jù)庫
sqlite3 向我們提供了這個接口,用來執(zhí)行打開數(shù)據(jù)庫操作,第一個參數(shù)為數(shù)據(jù)庫存的路徑,第二個參數(shù)為sqlite3的操作連接。
如果數(shù)據(jù)庫路徑下沒有數(shù)據(jù)庫,則創(chuàng)建一個數(shù)據(jù)庫并打開,如果有則直接打開數(shù)據(jù)庫

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

我們封裝一個方法執(zhí)行創(chuàng)建并打開數(shù)據(jù)庫的代碼,當(dāng)傳uid的時候會以uid命名數(shù)據(jù)庫,如果沒傳將會默認(rèn)數(shù)據(jù)庫名稱為CWDB,路徑我們寫在CWDatabase.m下,因為是測試階段,所以路徑設(shè)置在桌面上。需要自行修改路徑

+ (BOOL)openDB:(NSString *)uid {
    // 數(shù)據(jù)庫名稱
    NSString *dbName = @"CWDB.sqlite";
    if (uid.length != 0) {
        dbName = [NSString stringWithFormat:@"%@.sqlite", uid];
    }
    // 數(shù)據(jù)庫路徑
    NSString *dbPath = [kCWDBCachePath stringByAppendingPathComponent:dbName];
    // 打開數(shù)據(jù)庫
    int result = sqlite3_open(dbPath.UTF8String, &cw_database);
    if (result != SQLITE_OK) {
        NSLog(@"打開數(shù)據(jù)庫失敗! : %d",result);
        return NO;
    }
    // 檢測當(dāng)前連接的數(shù)據(jù)庫是否處于busy狀態(tài),處于則會回調(diào)CWDBBusyCallBack
    sqlite3_busy_handler(cw_database, &CWDBBusyCallBack, (void *)(cw_database));
    
    return YES;
}

b、關(guān)閉數(shù)據(jù)庫
傳一個sqlite3的操作連接即可以將連接關(guān)閉

SQLITE_API int sqlite3_close(sqlite3*);

帖上我們對應(yīng)的代碼

+ (void)closeDB {
    if (cw_database) {
        sqlite3_close(cw_database);
        cw_database = nil;
    }
}

c、進行單元測試

選擇如下圖創(chuàng)建一個單元測試的類
unitTest.png

然后寫上我們的測試代碼
test.png

點擊箭頭所指向的框框,則只能測試本函數(shù),通過第43行的斷言來判斷result是否為YES,是YES則測試通過,然后框框內(nèi)會變成一個綠色的勾,測試不通過則會變成紅色的叉,然后我們看看我們對應(yīng)的位置有沒有成功創(chuàng)建一個數(shù)據(jù)庫,最終我們發(fā)現(xiàn)測試通過。

3、根據(jù)模型對象,創(chuàng)建對應(yīng)的數(shù)據(jù)庫表格;

a、調(diào)用sqlite3的API創(chuàng)建表格
sqlite為我們提供下面這個方法在執(zhí)行sql語句

//數(shù)據(jù)庫執(zhí)行語句
SQLITE_API int sqlite3_exec(
  sqlite3*,                                  /* sqlite3的操作連接 */
  const char *sql,                           /* SQL語句 */
  int (*callback)(void*,int,char**,char**),  /* 回調(diào)函數(shù) */
  void *,                                    /* 第一個參數(shù)的回調(diào) */
  char **errmsg                              /* 錯誤信息 */
);

做數(shù)據(jù)庫執(zhí)行語句時,我們的邏輯是:

  • 1.打開數(shù)據(jù)庫
  • 2.數(shù)據(jù)庫執(zhí)行語句
  • 3.關(guān)閉數(shù)據(jù)庫
    我們在CWDatabase封裝一個方法,用來執(zhí)行數(shù)據(jù)庫操作,第一個參數(shù)為需要執(zhí)行的sql語句,第二個參數(shù)為userId,用來打開對應(yīng)的數(shù)據(jù)庫
+ (BOOL)execSQL:(NSString *)sql uid:(NSString *)uid {
    // 打開數(shù)據(jù)庫
    if (![self openDB:uid]) {
        return NO;
    }
    // 執(zhí)行語句
    char *errmsg = nil;
    int result = sqlite3_exec(cw_database, sql.UTF8String, nil, nil, &errmsg);
    // 關(guān)閉數(shù)據(jù)庫
    [self closeDB];
    // 執(zhí)行語句失敗則拋出錯誤信息
    if (result != SQLITE_OK) {
        NSLog(@"exec sql error : %s",errmsg);
        return NO;
    }
    return YES;
}

同樣的,我們對這個方法進行單元測試,在這里我們需要自己寫sql的執(zhí)行語句,測試傳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);
}

最終成功創(chuàng)建對應(yīng)的兩個數(shù)據(jù)庫以及表格

表格.png

b、面向模型來創(chuàng)建數(shù)據(jù)庫表格
做完以上步驟,我們成功的通過調(diào)用sqlite3的API準(zhǔn)確的創(chuàng)建了一個表格,但是還是要背sql語句,和我們一開始的初衷相違背,我們需要的是簡單、簡單、簡單、無腦操作。。所以我們需要想個辦法來省去背sql語句這一步驟。
簡單分析一下創(chuàng)建表格的sql語句,我們會發(fā)現(xiàn)可以分成下面的結(jié)構(gòu)

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(字段))

其中字段和字段類型,可以對應(yīng)成操作模型的成員變量以及成員變量的類型,所以,我們通過runtime的方法,獲取到模型的所有成員變量以及所有成員變量對應(yīng)的類型。
我們在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;
}

然后我們進行單元測試,創(chuàng)建CWModelToolTests的單元測試并創(chuàng)建一個student的模型,模型的成員變量為

@interface Student : NSObject<CWModelProtocol>
{
    float score;
}
@property (nonatomic,assign) int stuId; // 學(xué)號
@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);
}

然后我們運行這個測試函數(shù),在控制臺得到如下打?。?/p>

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

與對應(yīng)模型的成員變量一致,測試通過。
獲取了對應(yīng)的成員變量的字典后,我們需要將這個字典轉(zhuǎn)換成sql對應(yīng)的語句,下面加粗的部分
create table if not exists 表名(字段1 字段1類型,字段2 字段2類型 ....., primary key(字段))
在此之前,我們還要進行另一個轉(zhuǎn)換,因為數(shù)據(jù)庫里面對應(yīng)的類型和OC的類型并不一樣,所以要變一變

暫時不考慮OC對象(數(shù)組,字典 等...)以及自定義對象的情況

 OC                                      數(shù)據(jù)庫
 i :         整型                        integer
 q:          long                       integer
 Q:          long long                  integer   
 B:          bool                       integer
 d:          double                     real
 f:          float                      real
 NSString:   字符串                      text      
 NSData:     二進制                      blob   

我們封裝一個函數(shù)來進行字典的轉(zhuǎn)換,我們要得到的字典類型{成員變量名稱(key) :成員變量對應(yīng)數(shù)據(jù)庫的類型(value)}

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

// oc類型轉(zhuǎn)換到數(shù)據(jù)庫的類型
+ (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";
    }
}

這里我們就不在貼測試的代碼了,反正是成功的。
然后我們將以上方法獲取的字典轉(zhuǎn)換成我們需要的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];
}

在獲取主鍵這里,有兩種常用的方式,一種是設(shè)計一個自動增長的主鍵,另一種是學(xué)習(xí)realm的方式,通過代理讓用戶為模型返回一個主鍵,這里我們使用后者。在CWModelProtocol聲明一個協(xié)議方法,且這個方法是必須實現(xiàn)的

@protocol CWModelProtocol <NSObject>

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

@end

接下來,我們封裝創(chuàng)建數(shù)據(jù)庫表格的最終方法
在CWSqliteModelTool內(nèi),封裝一個方法

// uid用來確認(rèn)哪個數(shù)據(jù)庫,targetId用來區(qū)分?jǐn)?shù)據(jù)庫表名
+ (BOOL)createSQLTable:(Class)cls uid:(NSString *)uid targetId:(NSString *)targetId {
    // 創(chuàng)建數(shù)據(jù)庫表的語句
    // create table if not exists 表名(字段1 字段1類型(約束),字段2 字段2類型(約束)....., primary key(字段))
    // 獲取數(shù)據(jù)庫表名
    NSString *tableName = [CWModelTool tableName:cls targetId:targetId];
    
    if (![cls respondsToSelector:@selector(primaryKey)]) {
        NSLog(@"如果想要操作這個模型,必須要實現(xiàn)+ (NSString *)primaryKey;這個方法,來告訴我主鍵信息");
        return NO;
    }
    // 獲取主鍵
    NSString *primaryKey = [cls primaryKey];
    if (!primaryKey) {
        NSLog(@"你需要指定一個主鍵來創(chuàng)建數(shù)據(jù)庫表");
        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協(xié)議,實現(xiàn)必須要的協(xié)議方法。

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

@interface Student : NSObject<CWModelProtocol>
{
    float score;
}
@property (nonatomic,assign) int stuId; // 學(xué)號
@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

測試創(chuàng)建數(shù)據(jù)庫表格方法

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

運行之后得到如下結(jié)果


image.png

在對應(yīng)的路徑下,創(chuàng)建了CWDB數(shù)據(jù)庫,并在數(shù)據(jù)庫里面創(chuàng)建一張Student53class的表,表的列名與Student模型的成員變量一一對應(yīng),測試通過!

用戶如果要創(chuàng)建一個表,只需要調(diào)用這個方法

+ (BOOL)createSQLTable:(Class)cls uid:(NSString *)uid targetId:(NSString *)targetId;

將模型的類型,用戶id(可以為空)以及目標(biāo)id(可以為空)傳過來,我們就會創(chuàng)建對應(yīng)的數(shù)據(jù)庫并打開,解析模型,創(chuàng)建對應(yīng)的表格,關(guān)閉數(shù)據(jù)庫。

4、本篇結(jié)束

在此,我們通過調(diào)用sqlite的API,通過runtime,將創(chuàng)建數(shù)據(jù)庫表格的操作用非常簡潔的API開放出來,目前還是很成功的,在下一篇文章,我們會實現(xiàn)數(shù)據(jù)庫插入、查詢、更新操作。。在更后面的文章,我們會實現(xiàn)刪除、存儲模型內(nèi)嵌套OC對象,以及數(shù)組內(nèi)嵌套自定義模型,以及多線程安全等的處理。。

每一章的代碼我會上傳到github上。。并打tag作為一個節(jié)點。歡迎大家下載并查找漏洞,因為。我也是第一次封裝。一起學(xué)習(xí),一起進步。

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

image.png

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

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

目前第二篇文章已經(jīng)出爐,地址:從0開始弄一個面向OC數(shù)據(jù)庫(二)

最后再為大家推薦一個0耦合的側(cè)滑框架。
一行代碼集成超低耦合的側(cè)滑功能

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容