1鸽照、我們要做什么矮燎?
這個文章赔癌,我們要從0開始封裝一個面向OC對象的數(shù)據(jù)庫灾票,想了解怎么做的刊苍,可以一起陪伴一下班缰,所有的流程細節(jié)我都會寫在文章內(nèi)埠忘,因為我也第一次搞這個東西,有興趣的話咱們可以一起討論和提升名船。
2渠驼、做成什么樣子迷扇?
當然是期望做到簡(無)單(腦)操作數(shù)據(jù)庫蜓席,不需要背語句课锌,也不需要解析模型,類似realm這種騷操作[realm addObject:obj];obj就給你存到數(shù)據(jù)庫了请毛。
當然获印,我們不能和行業(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班每個學生的年齡侣颂,學號憔晒,成績拒担,身高从撼,名字,可惜的是53班目前沒有一個學生=钧栖。=
一個數(shù)據(jù)庫里面可以有多張名字不重復(fù)的表低零,一個表里面可以有多條主鍵不一致的數(shù)據(jù),每條數(shù)據(jù)指定一個字段為主鍵當唯一標識拯杠。
查看數(shù)據(jù)庫這里我用了一個破解版的工具,現(xiàn)在提供給大家:
鏈接: https://pan.baidu.com/s/1eSIpTBW 密碼: 4rjm
3掏婶、開始動手
首先創(chuàng)建一個工程,并向工程中拖入libsqlite3.0.tbd這個庫
1潭陪、庫的結(jié)構(gòu)設(shè)計
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ù)庫的代碼丹壕,當傳uid的時候會以uid命名數(shù)據(jù)庫,如果沒傳將會默認數(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;
}
// 檢測當前連接的數(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、進行單元測試
然后寫上我們的測試代碼
點擊箭頭所指向的框框似扔,則只能測試本函數(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ù)庫以及表格
b凿叠、面向模型來創(chuàng)建數(shù)據(jù)庫表格
做完以上步驟捆蜀,我們成功的通過調(diào)用sqlite3的API準確的創(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; // 學號
@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ù)漫萄,在控制臺得到如下打印:
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è)計一個自動增長的主鍵教硫,另一種是學習realm的方式,通過代理讓用戶為模型返回一個主鍵辆布,這里我們使用后者瞬矩。在CWModelProtocol聲明一個協(xié)議方法,且這個方法是必須實現(xiàn)的
@protocol CWModelProtocol <NSObject>
@required
/**
操作模型必須實現(xiàn)的方法,通過這個方法獲取主鍵信息
@return 主鍵字符串
*/
+ (NSString *)primaryKey;
@end
接下來锋玲,我們封裝創(chuàng)建數(shù)據(jù)庫表格的最終方法
在CWSqliteModelTool內(nèi)景用,封裝一個方法
// uid用來確認哪個數(shù)據(jù)庫,targetId用來區(qū)分數(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; // 學號
@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é)果
在對應(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(可以為空)以及目標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é)點屈张。歡迎大家下載并查找漏洞擒权,因為袱巨。我也是第一次封裝。一起學習碳抄,一起進步愉老。
github地址
tag為1.0.0,你可以在下圖的位置找到他剖效,并下載下來嫉入。
最后覺得有用的同學,希望能給本文點個喜歡璧尸,給github點個star以資鼓勵咒林,謝謝大家。
PS: 因為我也是一邊封裝爷光,一邊寫文章垫竞。效率可能比較低,問題也會有蛀序,歡迎大家向我拋issue欢瞪,有更好的思路也歡迎大家留言!
目前第二篇文章已經(jīng)出爐,地址:從0開始弄一個面向OC數(shù)據(jù)庫(二)
最后再為大家推薦一個0耦合的側(cè)滑框架徐裸。
一行代碼集成超低耦合的側(cè)滑功能