看了一段時間的MySQL, 正好藉此機會把經常使用於嵌入式和移動設備的SQLite複習了一下. 我結合了公司的項目 以實際的業務需求爲導向, 對sqlite3進行了簡單的封裝, 實現對項目中搜索記錄的管理.git
搜索記錄的保存, 刪除某一條記錄, 刪除全部記錄, 數據所佔內存的大小github
在整個項目中, 可能會存在對個搜索頁面, 並且搜索的內容也不一樣, 就拿咱們項目來講, 有項目列表頁的搜索, 資訊搜索, 這些不一樣類型的搜索是要分開來處理的.sql
在準備的過程當中遇到的第一個問題就是, 項目中用來存儲歷史記錄的數據庫和對應的表, 是要動態調用sql語句建立仍是直接在本地建立好, 而後把文件導入工程中. 數據庫
若是是動態建立, 那麼在項目的總體邏輯中要常常判斷是否已經存在了咱們所須要的庫和表, 我的以爲很煩, 並且一個db文件佔內存也就才幾K, 因此我就選擇了直接建立而後導入工程的方式.數組
我用的sqlite圖形化工具是 DB Browser for SQLite, 操做起來很簡單, 庫和表分分鐘搞定. 緩存
表名search_history
id就不用提了
search_type儲存的是搜索的使用場景, 一個場景對應一個type. 舉個例子, 好比項目搜索相關的都是type1, 資訊搜索的是type2, 這樣就把場景區分開了.
search_content就是保存起來的搜索關鍵字.
search_time是用來記錄數據存儲的時間(直接使用時間戳), 加這個time字段就是排序用的, 由於咱們在展現搜索記錄是, 默認的邏輯是最新搜索的在最前面, 並且若是一個你曾經搜索過的記錄, 若是從新搜索了一次, 那也要更新這個記錄對應的時間.app
其實就是這兩個類, 先忽略那個Error文件吧, 我感受本身處理error作的不太好, 裏面也就沒寫啥, 只有兩個字段. 重點是SearchDBHelper這個類.框架
下面是.h文件中的代碼, 我只拋出了須要的這幾個方法函數
#import <Foundation/Foundation.h> @class BaseSQLError; NS_ASSUME_NONNULL_BEGIN // 歷史記錄類型 // 根據項目中的實際使用場景來定義,每一個場景定義一個type. // 好比說項目搜索和商品搜索頁面都要分別記錄搜索歷史, 那就建立兩個類型, 一個對應項目列表,一個對應商品列表, 以此類推 extern NSString * const SEARCH_TYPE_1; extern NSString * const SEARCH_TYPE_2; @interface SearchDBHelper : NSObject /** 建立SearchDBHelper實例,設置變量的默認 @param searchType 歷史記錄類型 @return 實例 */ + (instancetype)initWithSearchType:(NSString *)searchType; /** 設置最大存儲數量和類型 @param max 最大儲存數量 */ - (void)setMax:(NSInteger)max; /** 獲取當前分類下全部的歷史記錄 @return 全部歷史記錄 */ - (NSArray<NSString *> *)getAllSearchData; /** 插入數據 @param keyword 要要入到表中的搜索記錄 @param complete 完成的回調 */ - (void)insertData:(NSString *)keyword callBack:(void (^)(BaseSQLError * _Nullable error))complete; /** 清除一條搜索記錄 @param keyword 要清除的搜索關鍵字 @param complete 完成回調 */ - (void)clearSingleSearchData:(NSString *)keyword callBack:(void (^)( BaseSQLError * _Nullable error))complete; /** 刪除當前類型的全部搜索記錄 */ - (void)clearAllSearchDataFromCurrentType:(void (^)(BaseSQLError * _Nullable error))complete; /** 獲取該數據庫佔內存大小 @return 數據庫大小:單位k */ + (NSString *)getSizeFromDataBase; /** 刪除全部的搜索歷史記錄(清空記錄緩存) */ + (void)clearAllSearchData:(void (^)(BaseSQLError * _Nullable error))complete; NS_ASSUME_NONNULL_END @end
針對這個.h文件中的代碼, 我說幾個點工具
方法參數的爲空問題
如今看到的.h 文件中的方法 實際上是我修改以後的, 在最開始的設計中, 幾乎每個方法都要傳進去searchType, 可是呢, 我又想要限制傳的searchType不能爲空, 因此就想到了限制符.
蘋果咋子Xcode6.3引入OC的新特性, Nullability和Annotations, 核心呢就是兩個修飾符, __nonnull和__nullable, __nonnull表示修飾的對象不能夠爲NULL或nil, __nullable表示能夠爲空. 有了這種修飾符, 在使用方法時若是傳nil或NULL, 編譯器就會報警. 在 Xcode7 中,爲了不與第三方庫潛在的衝突,蘋果把 __nonnull / __nullable 改爲 _Nonnull / _Nullable。再加上蘋果一樣支持了沒有下劃線的寫法 nonnull/nullable,很亂, 但本質上是同樣的, 效果也同樣, 就是修飾符放的位置不太同樣, 具體的使用方法就不囉嗦, 從工程裏隨便找個原生的類點進去看看就一目瞭然了.
下面是蘋果的建議 :
- 對於屬性、方法返回值、方法參數的修飾,使用:nonnull/nullable;
- 對於 C 函數的參數、Block 的參數、Block 返回值的修飾, 使用:_Nonnull/_Nullable,建議棄用
__nonnull / __nullable
可是像我這樣一個.h文件, 若是加太多的nonnull的話, 首先是很繁瑣, 在一個確實也不太好看, 爲了解決這個問題, Foundation框架給出了這對宏, NS_ASSUME_NONNULL_BEGIN和 NS_ASSUME_NONNULL_END. 包含在這對宏裏的對象默認添加nonnull修改符, 而後有極個別的能夠爲空的(好比block)單獨添加添加能夠爲空的修飾符
(void (^)(BaseSQLError * _Nullable error))complete
這個是爲了統必定義searchType, 而後在外部使用這個helper類的時候, 要傳searchType時能夠直接使用定義好的SEARCH_TYPE_1此類的變量名, 不至於都是直接使用@"xxx"字符串形式, 容易出錯.
其實大部分的功能看 .m 中的代碼都是能夠看明白的, 我這裏稍微簡單的說一下一些思路問題吧.
就拿insert這個方法來講, 這個方法也是比較麻煩的一個了吧,下面是方法的實現代碼
- (void)insertData:(NSString *)keyword callBack:(void (^)(BaseSQLError *_Nullable error))complete { // 判斷是否已經存儲過此keyword 已存儲:update 未存儲:insert if ([self selectKeyWordIsExit:keyword] == ExitKeyWord) { [self updateSearchKeyword:keyword callBack:complete]; } else if ([self selectKeyWordIsExit:keyword] == NotExitKeyWord){ [self insertSearchKeyword:keyword callBack:complete]; } else if ([self selectKeyWordIsExit:keyword] == ExitUnknown){ BaseSQLError * sqlErr = [BaseSQLError new]; sqlErr.errInfo = @"插入數據庫失敗"; sqlite3_close(_sql3); if(complete){ complete(sqlErr); } return; } }
能夠看出來, 在插入一個數據以前 , 要先要先判斷這個內容是否已經存在了, 也就是selectKeyWordIsExit:這個方法, 他返回的是一個枚舉值(存在, 不存在, 未知), 未知就是表明在操做數據庫是出錯.
若是已經存在了, 那就調用update的方法, 來更新這個search_content對應的search_time. 若是不存在, 在進行下一步的判斷.
那就是判斷當前的這個search_type下儲存的數據條數,是否已經到了最大值了, 這個最大值咱們能夠經過調用方法設置, 若是不設置默認我給的是10條. 就拿max爲10來講, 若是尚未到達10條, 如今能夠調用insert的方法了, 若是已經有10條了, 那就得把時間最先的那一條刪掉, 而後再insert這條新的.
數據庫操做無非就是增刪改查, 增刪改這個三類操做比較簡單, 由於沒有返回的數據, 可是查詢呢 , 有一種數量的查詢(查詢某個searchTpye下已經儲存的數據的條數), 這種只是放回了一個int的值. 在一種就是查詢數據了(好比說當前searchType下全部的儲存的歷史距離), 那這一種就要返回一個數組NSArray <NSString *>*. 這兩種查詢的sql實現不太同樣, 我在代碼裏都有標識了. 你們能夠具體看看代碼 .
這個類的封裝邏輯就是這個樣子, 具體的sqlite語句的用法, 你們能夠看看代碼. 我這裏把整個.m代碼也放出來, 你們能夠這樣看, 也能夠去github上面下載代碼看一下, 並且我在github的代碼中還作了一個小demo來演示用法.
// // SearchDBHelper.m // LearnSQLite // // Created by Sunxb on 2018/4/26. // Copyright © 2018年 Sunxb. All rights reserved. // #import "SearchDBHelper.h" #import <sqlite3.h> #import "BaseSQLError.h" // 表名 static NSString * const TABLE_NAME = @"search_history"; // 類型名 NSString * const SEARCH_TYPE_1 = @"type1"; NSString * const SEARCH_TYPE_2 = @"type2"; // 是否已經存在某條記錄 typedef NS_ENUM(NSInteger, DBExitKeyWord) { ExitKeyWord = 0, NotExitKeyWord, ExitUnknown }; @interface SearchDBHelper() @property (nonatomic,strong) NSString * searchType; @property (nonatomic,assign) NSInteger maxNum; @property (nonatomic,assign) sqlite3 *sql3; @end @implementation SearchDBHelper + (instancetype)initWithSearchType:(NSString *)searchType { SearchDBHelper * helper = [[SearchDBHelper alloc] init]; helper.searchType = searchType; return helper; } - (instancetype)init { if (self = [super init]){ self.maxNum = 10; self.searchType = @""; } return self; } - (void)setMax:(NSInteger)max { self.maxNum = max; } - (NSArray<NSString *> *)getAllSearchData { return [self runSelectListSQLWithType:self.searchType]; } - (void)insertData:(NSString *)keyword callBack:(void (^)(BaseSQLError *_Nullable error))complete { // 判斷是否已經存儲過此keyword 已存儲:update 未存儲:insert if ([self selectKeyWordIsExit:keyword] == ExitKeyWord) { [self updateSearchKeyword:keyword callBack:complete]; } else if ([self selectKeyWordIsExit:keyword] == NotExitKeyWord){ [self insertSearchKeyword:keyword callBack:complete]; } else if ([self selectKeyWordIsExit:keyword] == ExitUnknown){ BaseSQLError * sqlErr = [BaseSQLError new]; sqlErr.errInfo = @"插入數據庫失敗"; sqlite3_close(_sql3); if(complete){ complete(sqlErr); } return; } } - (void)clearSingleSearchData:(NSString *)keyword callBack:(void (^)(BaseSQLError *_Nullable error))complete { NSString * deleteSingle = [NSString stringWithFormat:@"delete from search_history where search_content=\"%@\" and search_type=\"%@\";",keyword,self.searchType]; [self runSQL:deleteSingle callBack:complete]; } - (void)clearAllSearchDataFromCurrentType:(void (^)(BaseSQLError *_Nullable error))complete { NSString * deleteSingle = [NSString stringWithFormat:@"delete from search_history where search_type=\"%@\";",self.searchType]; [self runSQL:deleteSingle callBack:complete]; } + (NSString *)getSizeFromDataBase { NSString * dbPath = [self getDBPath]; NSLog(@"=== %@",dbPath); NSFileManager * fileManager = [NSFileManager defaultManager]; if (dbPath.length > 0) { long long dbSize = [fileManager attributesOfItemAtPath:dbPath error:nil].fileSize; return [NSString stringWithFormat:@"%gk",dbSize/1024.0]; } return @"0k"; } + (void)clearAllSearchData:(void (^)(BaseSQLError *))complete { SearchDBHelper * helper = [[SearchDBHelper alloc] init]; NSString * deleteAll = [NSString stringWithFormat:@"delete from search_history"]; [helper runSQL:deleteAll callBack:complete]; } #pragma mark - private // 獲取數據庫文件在工程中的路徑 + (NSString *)getDBPath { NSString *filePath = [[NSBundle mainBundle] pathForResource:@"mydb" ofType:@"db"]; NSFileManager * manager = [NSFileManager defaultManager]; if ([manager fileExistsAtPath:filePath]) { return filePath; } return @""; } // 是否打開數據庫 - (BOOL)openDataBase { NSString * dbPath = [SearchDBHelper getDBPath]; if(dbPath.length > 0) { if (sqlite3_open([dbPath UTF8String], &_sql3) != SQLITE_OK) { sqlite3_close(_sql3); _sql3 = nil; return NO; } else { return YES; } } return NO; } // 運行普通無返回sql語句 : insert || update - (void)runSQL:(NSString *)sql callBack:(void (^)(BaseSQLError *))complete { BaseSQLError * sqlErr = [BaseSQLError new]; if (![self openDataBase]) { sqlErr.errInfo = @"打開數據庫失敗"; if(complete){ complete(sqlErr); } return; } char * err; int code = sqlite3_exec(_sql3, [sql UTF8String], NULL, NULL, &err); if (code != SQLITE_OK) { BaseSQLError * sqlErr = [BaseSQLError new]; sqlErr.errInfo = [NSString stringWithCString:err encoding:NSUTF8StringEncoding]; sqlErr.errCode = code; complete(sqlErr); } else { complete(nil); } sqlite3_close(_sql3); _sql3 = nil; } // TODO: 運行查詢性質的sql語句 - 返回數組 (好比查詢全部的數據) - (NSArray *)runSelectListSQLWithType:(NSString *)searchType { if (![self openDataBase]) { BaseSQLError * sqlErr = [BaseSQLError new]; sqlErr.errInfo = @"打開數據庫失敗"; return nil; } NSString * sql = [NSString stringWithFormat:@"select search_content from search_history where search_type=\"%@\" order by search_time desc;",searchType]; sqlite3_stmt * statement; if (sqlite3_prepare_v2(_sql3, [sql UTF8String], -1, &statement, NULL)==SQLITE_OK){ NSMutableArray * historyArr = [NSMutableArray new]; while (sqlite3_step(statement) == SQLITE_ROW) { NSString * str = [NSString stringWithUTF8String:(const char *)sqlite3_column_text(statement, 0)]; [historyArr addObject:str]; } sqlite3_finalize(statement); statement = nil; return historyArr; } sqlite3_close(_sql3); return nil; } /** 運行查詢性質的sql語句 - 返回數量 (好比查詢符合某條件的結果數) @param dict 須要查詢的條件(key value形式) @param complete 完成回調 @return 查詢的數值 (查詢過程出錯返回 -1) */ - (NSInteger)runCountSelectSQL:(NSDictionary *)dict callBack:(void (^)(BaseSQLError *))complete { if (![self openDataBase]) { BaseSQLError * sqlErr = [BaseSQLError new]; sqlErr.errInfo = @"打開數據庫失敗"; if(complete){ complete(sqlErr); } return -1; } NSMutableString * searchRequire = [NSMutableString new]; NSArray * keysArr = dict.allKeys; for (int i = 0; i < keysArr.count; i ++){ NSString * key = keysArr[i]; if (i == 0){ [searchRequire appendFormat:@"%@=\"%@\"",key,[dict objectForKey:key]]; } else { [searchRequire appendFormat:@"and %@=\"%@\"",key,[dict objectForKey:key]]; } } NSString * selSql = [NSString stringWithFormat:@"select count(*) from search_history where %@;",searchRequire]; sqlite3_stmt * statement; //預處理 if(sqlite3_prepare_v2(_sql3, [selSql UTF8String], -1, &statement, NULL)==SQLITE_OK){ sqlite3_step(statement); NSInteger result = sqlite3_column_int(statement, 0); sqlite3_finalize(statement); statement = nil; return result; } return -1; } // 查詢某記錄是否已經存在 - (DBExitKeyWord)selectKeyWordIsExit:(NSString *)keyword { NSInteger count = [self runCountSelectSQL:@{@"search_content":keyword,@"search_type":self.searchType} callBack:nil]; if(count > 0) { return ExitKeyWord; } else if (count == 0){ return NotExitKeyWord; } else { return ExitUnknown; } } //插入數據 - (void)insertSearchKeyword:(NSString *)keyword callBack:(void (^)(BaseSQLError *))complete { // TODO: 是否已經超過了最大存儲數量 NSInteger alreadCount = [self runCountSelectSQL:@{@"search_type":self.searchType} callBack:complete]; if(alreadCount >= 0) { if (alreadCount >= self.maxNum) { // 先刪除時間最先的記錄,再插入 [self deleteEarliestSearchKeywordWithType:self.searchType callBack:^(BaseSQLError *err) { if (err==nil) { [self sqlInsert:keyword callBack:complete]; } else { if(complete){ complete(err); } } }]; } else { // 直接插入 [self sqlInsert:keyword callBack:complete]; } } else { BaseSQLError * sqlErr = [BaseSQLError new]; sqlErr.errInfo = @"插入數據失敗"; if(complete){ complete(sqlErr); } } } - (void)sqlInsert:(NSString *)keyword callBack:(void (^)(BaseSQLError *))complete{ NSString * keyStr = @"search_type,search_content,search_time"; NSString * valuesStr = [NSString stringWithFormat:@"\"%@\",\"%@\",\"%@\"",self.searchType,keyword,[self getCurrentTimestamp]]; NSString * insertSql = [NSString stringWithFormat:@"insert into search_history (%@) values (%@);",keyStr,valuesStr]; [self runSQL:insertSql callBack:complete]; } //更新數據 - (void)updateSearchKeyword:(NSString *)keyword callBack:(void (^)(BaseSQLError *))complete { NSString * updateSql = [NSString stringWithFormat:@"update search_history set search_time=\"%@\" where search_content=\"%@\";",[self getCurrentTimestamp],keyword]; [self runSQL:updateSql callBack:complete]; } // 刪除當前分類下, 時間最先的記錄 - (void)deleteEarliestSearchKeywordWithType:(NSString *)searchType callBack:(void (^)(BaseSQLError *))complete{ NSString * deleteStr = @"delete from search_history where id in \ (select id from search_history where search_type=\"%@\" order by search_time limit 0,1)"; NSString * deleteSql = [NSString stringWithFormat:deleteStr,searchType]; [self runSQL:deleteSql callBack:complete]; } // 查詢當前類型下已經存儲的歷史記錄的條數 - (NSInteger)getHistoryCountInDB { return [self getHistoryCountInDBWithSearchType:self.searchType]; } - (NSInteger)getHistoryCountInDBWithSearchType:(NSString *)type { NSInteger count = [self runCountSelectSQL:@{@"search_type":type} callBack:nil]; count = (count>0?count:0); return count; } // 獲取當前時間戳 - (NSString*)getCurrentTimestamp { NSDate * data = [NSDate dateWithTimeIntervalSinceNow:0]; NSTimeInterval a = [data timeIntervalSince1970]; NSString * timeString = [NSString stringWithFormat:@"%0.f", a]; return timeString; } @end
特此感謝: https://my.oschina.net/u/2340... 給我巨大的靈感.