近期又學習了一下資料庫的東西,決定花時間封裝一個資料庫,看了一些原始碼,感覺有一定的可行度,所以先把第一篇(本篇)文章發上來。啥都沒有發上來幹啥捏?有時候容易半途而廢,先發上來,斷自己後路!一定得寫,不然就是說話不算話了。
![100婚](https://i.iter01.com/images/424b20fbf4e923b9689bbcf5a990310bdea7f2466dd2bdfdc08a012059407720.jpg)
1、我們要做什麼?
這個文章,我們要從0開始封裝一個面向OC物件的資料庫,想了解怎麼做的,可以一起陪伴一下,所有的流程細節我都會寫在文章內,因為我也第一次搞這個東西,有興趣的話我們們可以一起討論和提升。
2、做成什麼樣子?
當然是期望做到簡(無)單(腦)運算元據庫,不需要背語句,也不需要解析模型,類似realm這種騷操作[realm addObject:obj];obj就給你存到資料庫了。
![額](https://i.iter01.com/images/8c61f103e1bdd31e87a7868112e87c3c23f4b75ddba32a1fd3cc1623d6f18a46.jpg)
3、簡單介紹
(本來說好的只發文章斷自己後路,但是還是先實現一部分功能,免得顯得太空洞了) 本篇主要實現的功能有:
- 1、整個庫的結構設計;
- 2、開啟並建立資料庫、關閉資料庫;
- 3、根據模型物件,建立對應的資料庫表格;
在做功能之前,先簡單的介紹一下資料庫相關的東西:
以上的圖表示,名稱為CWDB的資料庫,裡面有一張Student53class的表,表有的欄位為age,stuId,score,height,name。也可以解釋為CWDB這個資料庫裡面存了53班每個學生的年齡,學號,成績,身高,名字,可惜的是53班目前沒有一個學生=。= 一個資料庫裡面可以有多張名字不重複的表,一個表裡面可以有多條主鍵不一致的資料,每條資料指定一個欄位為主鍵當唯一標識。 檢視資料庫這裡我用了一個破解版的工具,現在提供給大家: 連結: https://pan.baidu.com/s/1eSIpTBW 密碼: 4rjm
4、開始動手
首先建立一個工程,並向工程中拖入libsqlite3.0.tbd這個庫
1、庫的結構設計
![結構.png](https://i.iter01.com/images/8e237221ce9727f4d24c3bd11a4ac6215527be3a1590a6293243ea764e6c308d.jpg)
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](https://i.iter01.com/images/47e026808c0f8edcc7931a5d053980eff49f51296bd13a0940fc41123f0255ee.jpg)
![test.png](https://i.iter01.com/images/3a7e1f4316dc2ad63c3153e181e7f51c602653d2aae74a3ab393be83d840cb72.jpg)
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](https://i.iter01.com/images/77ad6082ba2ee96613f75c7ab1da7e88688851ef7e78433e6a9fa909da45d1f1.jpg)
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](https://i.iter01.com/images/2f8e81608e9338562b5ae553d81bce3349030bc0f9622686d3076839be11a749.png)
使用者如果要建立一個表,只需要呼叫這個方法
+ (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](https://i.iter01.com/images/2cf00c110bfda4cc5ad3d82e629bef8a39b5e9e32b51c07890d68ff9c93a5241.png)
最後覺得有用的同學,希望能給本文點個喜歡,給github點個star以資鼓勵,謝謝大家。
PS: 因為我也是一邊封裝,一邊寫文章。效率可能比較低,問題也會有,歡迎大家向我拋issue,有更好的思路也歡迎大家留言!
目前第二篇文章已經出爐,地址:從0開始弄一個面向OC資料庫(二)
最後再為大家推薦一個0耦合的側滑框架。 一行程式碼整合超低耦合的側滑功能