從0開始弄一個面向OC數據庫(一)--打開、關閉數據庫,動態建表

近期又學習了一下數據庫的東西,決定花時間封裝一個數據庫,看了一些源碼,感受有必定的可行度,因此先把第一篇(本篇)文章發上來。啥都沒有發上來幹啥捏?有時候容易半途而廢,先發上來,斷本身後路!必定得寫,否則就是說話不算話了。git

100婚

一、咱們要作什麼?

這個文章,咱們要從0開始封裝一個面向OC對象的數據庫,想了解怎麼作的,能夠一塊兒陪伴一下,全部的流程細節我都會寫在文章內,由於我也第一次搞這個東西,有興趣的話我們能夠一塊兒討論和提高。github

二、作成什麼樣子?

固然是指望作到簡()單()操做數據庫,不須要背語句,也不須要解析模型,相似realm這種騷操做[realm addObject:obj];obj就給你存到數據庫了。 sql

額
固然,咱們不能和行業航母去比較,可是封裝出來的東西必定要簡單適用,安全可靠。咱們封裝的數據庫, 操做必須簡單,功能必須全面(增刪查改同樣不能少,數據庫遷移,數據庫字段更名也得支持,多線程安全也得考慮,最重要的是經常使用的數據類型咱們都得支持)。

三、簡單介紹

(原本說好的只發文章斷本身後路,可是仍是先實現一部分功能,省得顯得太空洞了) 本篇主要實現的功能有:數據庫

  • 一、整個庫的結構設計;
  • 二、打開並建立數據庫、關閉數據庫;
  • 三、根據模型對象,建立對應的數據庫表格; 在作功能以前,先簡單的介紹一下數據庫相關的東西:
    數據庫.png
    以上的圖表示,名稱爲CWDB的數據庫,裏面有一張Student53class的表,表有的字段爲age,stuId,score,height,name。也能夠解釋爲CWDB這個數據庫裏面存了53班每一個學生的年齡,學號,成績,身高,名字,惋惜的是53班目前沒有一個學生=。= 一個數據庫裏面能夠有多張名字不重複的表,一個表裏面能夠有多條主鍵不一致的數據,每條數據指定一個字段爲主鍵當惟一標識。 查看數據庫這裏我用了一個破解版的工具,如今提供給你們: 連接: https://pan.baidu.com/s/1eSIpTBW 密碼: 4rjm

四、開始動手

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

一、庫的結構設計

結構.png

二、打開並建立數據庫、關閉數據庫

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下,由於是測試階段,因此路徑設置在桌面上。須要自行修改路徑bash

+ (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、進行單元測試 選擇以下圖建立一個單元測試的類ide

unitTest.png
而後寫上咱們的測試代碼
test.png
點擊箭頭所指向的框框,則只能測試本函數,經過第43行的斷言來判斷result是否爲YES,是YES則測試經過,而後框框內會變成一個綠色的勾,測試不經過則會變成紅色的叉,而後咱們看看咱們對應的位置有沒有成功建立一個數據庫,最終咱們發現測試經過。

三、根據模型對象,建立對應的數據庫表格;

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(能夠爲空)傳過來,咱們就會建立對應的數據庫並打開,解析模型,建立對應的表格,關閉數據庫。

五、本篇結束

在此,咱們經過調用sqlite的API,經過runtime,將建立數據庫表格的操做用很是簡潔的API開放出來,目前仍是很成功的,在下一篇文章,咱們會實現數據庫插入、查詢、更新操做。。在更後面的文章,咱們會實現刪除、存儲模型內嵌套OC對象,以及數組內嵌套自定義模型,以及多線程安全等的處理。。

每一章的代碼我會上傳到github上。。並打tag做爲一個節點。歡迎你們下載並查找漏洞,由於。我也是第一次封裝。一塊兒學習,一塊兒進步。

github地址 tag爲1.0.0,你能夠在下圖的位置找到他,並下載下來。

image.png

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

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

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

最後再爲你們推薦一個0耦合的側滑框架。 一行代碼集成超低耦合的側滑功能

相關文章
相關標籤/搜索