Initial commit

This commit is contained in:
lianxiang
2018-08-22 11:15:52 +08:00
parent d98e20881f
commit 249bf6864e
491 changed files with 68665 additions and 7 deletions
+19
View File
@@ -0,0 +1,19 @@
Copyright (c) 2012-2014 YTKKeyValueStore https://github.com/yuantiku
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+132
View File
@@ -0,0 +1,132 @@
## 前言
还记得大学刚学数据库那会儿,天真地以为世界上所有的存储都需要用数据库来做。后来毕业后,正值NOSQL流行,那时我在网易参与了网易微博的开发,我们当时使用了有道自己做的“BigTable”— OMAP来存储微博数据,那个时候才发现,其实Key-Value这种简单的存储也能搞定微博这类不太简单的存储逻辑。
相比MYSQL,当数据量上千万后,NOSQL的优势体现出来了:对于海量数据,NOSQL在存取速度上没有任何影响,另外,天生的多备份和分布式,也说数据安全和扩容变得异常容易。
## iOS端的尝试
后来我从后台转做iOS端的开发,我就尝试了在iOS端直接使用Key-Value式的存储。经过在粉笔网、猿题库、小猿搜题三个客户端中的尝试后,我发现Key-Value式的存储不但完全能够满足大多数移动端开发的需求,而且非常适合移动端采用。主要原因是:移动端存储的数据量不会很大:
* 如果是单机的应用(例如效率工具Clear),用户自己一个人创建的数据最多也就上万条。
* 如果是有服务端的应用(例如网易新闻,微博),那移动端通常不会保存全量的数据,每次会从服务器上获取数据,本地只是做一些内容的缓存而已,所以也不会有很大的数据量。
如果数据量不大的话,那么在iOS端使用最简单直接的Key-Value存储就能带来开发上的效率优势。它能保证:
1. Model层的代码编写简单,易于测试。
2. 由于Value是JSON格式,所以在做Model字段更改时,易于扩展和兼容。
## 实现方案
在存储引擎上,2年前我直接选择了Sqlite当做存储引擎,相当于每个数据库表只有Key,Value两个字段。后来,随着LevelDB的流行,业界也有一些应用采用了LevelDB来做iOS端的Key-Value存储引擎,例如开源的[ViewFinder](https://github.com/viewfinderco/viewfinder)。
因为LevelDB本身并不是为移动端设计的,我担心它过于占用内存,我自己也没有看到业界有在移动端针对LevelDB做很详细的测试,连LevelDB的iOS端移植都不是官方做的。加上我自己写的基于Sqlite的Key-Value存储用着也没有什么问题,所以我也就一直没有更换成LevelDB。
## 开源
经过两年的使用和测试,我认为它非常好用,而且代码也非常简单,只有不到400行。所以现在开源分享给大家,这个项目叫`YTKKeyValueStore`,项目在[这里](https://github.com/yuantiku/YTKKeyValueStore)。以下是一个简单的使用示例:
```
YTKKeyValueStore *store = [[YTKKeyValueStore alloc] initDBWithName:@"test.db"];
NSString *tableName = @"user_table";
[store createTableWithName:tableName];
// 保存
NSString *key = @"1";
NSDictionary *user = @{@"id": @1, @"name": @"tangqiao", @"age": @30};
[store putObject:user withId:key intoTable:tableName];
// 查询
NSDictionary *queryUser = [store getObjectById:key fromTable:tableName];
NSLog(@"query data result: %@", queryUser);
```
## 集成说明
使用本项目,你需要将开源代码中的`YTKKeyValueStore.h``YTKKeyValueStore.m`添加到你的工程中,并且在工程设置的`Link Binary With Libraries`中,增加`libsqlite3.dylib`,如下图所示:
![](http://blog.devtang.com/images/key-value-store-setup.jpg)
由于时间关系,当前还未提供Cocoapods方式集成。
## 使用说明
所有的接口都封装在`YTKKeyValueStore`类中。以下是一些常用方法说明。
### 打开(或创建)数据库
通过`initDBWithName`方法,即可在程序的`Document`目录打开指定的数据库文件。如果该文件不存在,则会创建一个新的数据库。
```
// 打开名为test.db的数据库,如果该文件不存在,则创新一个新的。
YTKKeyValueStore *store = [[YTKKeyValueStore alloc] initDBWithName:@"test.db"];
```
### 创建数据库表
通过`createTableWithName`方法,我们可以在打开的数据库中创建表,如果表名已经存在,则会忽略该操作。如下所示:
```
YTKKeyValueStore *store = [[YTKKeyValueStore alloc] initDBWithName:@"test.db"];
NSString *tableName = @"user_table";
// 创建名为user_table的表,如果已存在,则忽略该操作
[store createTableWithName:tableName];
```
### 读写数据
`YTKKeyValueStore`类提供key-value的存储接口,存入的所有数据需要提供key以及其对应的value,读取的时候需要提供key来获得相应的value。
`YTKKeyValueStore`类支持的value类型包括:NSString, NSNumber, NSDictionary和NSArray,为此提供了以下接口:
```
- (void)putString:(NSString *)string withId:(NSString *)stringId intoTable:(NSString *)tableName;
- (void)putNumber:(NSNumber *)number withId:(NSString *)numberId intoTable:(NSString *)tableName;
- (void)putObject:(id)object withId:(NSString *)objectId intoTable:(NSString *)tableName;
```
与此对应,有以下value为NSString, NSNumber, NSDictionary和NSArray的读取接口:
```
- (NSString *)getStringById:(NSString *)stringId fromTable:(NSString *)tableName;
- (NSNumber *)getNumberById:(NSString *)numberId fromTable:(NSString *)tableName;
- (id)getObjectById:(NSString *)objectId fromTable:(NSString *)tableName;
```
### 删除数据接口
`YTKKeyValueStore`提供了以下接口用于删除数据。
```
// 清除数据表中所有数据
- (void)clearTable:(NSString *)tableName;
// 删除指定key的数据
- (void)deleteObjectById:(NSString *)objectId fromTable:(NSString *)tableName;
// 批量删除一组key数组的数据
- (void)deleteObjectsByIdArray:(NSArray *)objectIdArray fromTable:(NSString *)tableName;
// 批量删除所有带指定前缀的数据
- (void)deleteObjectsByIdPrefix:(NSString *)objectIdPrefix fromTable:(NSString *)tableName;
```
### 更多接口
`YTKKeyValueStore`还提供了以下接口来获取表示内部存储的key-value对象。
```
// 获得指定key的数据
- (YTKKeyValueItem *)getYTKKeyValueItemById:(NSString *)objectId fromTable:(NSString *)tableName;
// 获得所有数据
- (NSArray *)getAllItemsFromTable:(NSString *)tableName;
```
由于`YTKKeyValueItem`类带有`createdTime`字段,可以获得该条数据的插入(或更新)时间,以便上层做复杂的处理(例如用来做缓存过期逻辑)。
## 协议
YTKKeyValueStore 被许可在 MIT 协议下使用。查阅 LICENSE 文件来获得更多信息。
## 其它
两年前写过不少测试用例,后来给弄丢了,所以现在开项项目中还没有测试用例。由于时间关系,更详细的使用说明稍后会更新到项目中。
@@ -0,0 +1,57 @@
//
// YTKKeyValueStore.h
// Ape
//
// Created by TangQiao on 12-11-6.
// Copyright (c) 2012年 TangQiao. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface YTKKeyValueItem : NSObject
@property (strong, nonatomic) NSString *itemId;
@property (strong, nonatomic) id itemObject;
@property (strong, nonatomic) NSDate *createdTime;
@end
@interface YTKKeyValueStore : NSObject
- (id)initDBWithName:(NSString *)dbName;
- (id)initWithDBWithPath:(NSString *)dbPath;
- (void)createTableWithName:(NSString *)tableName;
- (void)clearTable:(NSString *)tableName;
- (void)close;
///************************ Put&Get methods *****************************************
- (void)putObject:(id)object withId:(NSString *)objectId intoTable:(NSString *)tableName;
- (id)getObjectById:(NSString *)objectId fromTable:(NSString *)tableName;
- (YTKKeyValueItem *)getYTKKeyValueItemById:(NSString *)objectId fromTable:(NSString *)tableName;
- (void)putString:(NSString *)string withId:(NSString *)stringId intoTable:(NSString *)tableName;
- (NSString *)getStringById:(NSString *)stringId fromTable:(NSString *)tableName;
- (void)putNumber:(NSNumber *)number withId:(NSString *)numberId intoTable:(NSString *)tableName;
- (NSNumber *)getNumberById:(NSString *)numberId fromTable:(NSString *)tableName;
- (NSArray *)getAllItemsFromTable:(NSString *)tableName;
- (void)deleteObjectById:(NSString *)objectId fromTable:(NSString *)tableName;
- (void)deleteObjectsByIdArray:(NSArray *)objectIdArray fromTable:(NSString *)tableName;
- (void)deleteObjectsByIdPrefix:(NSString *)objectIdPrefix fromTable:(NSString *)tableName;
@end
+316
View File
@@ -0,0 +1,316 @@
//
// YTKKeyValueStore.m
// Ape
//
// Created by TangQiao on 12-11-6.
// Copyright (c) 2012年 TangQiao. All rights reserved.
//
#import "YTKKeyValueStore.h"
#import "FMDatabase.h"
#import "FMDatabaseQueue.h"
#ifdef DEBUG
#define debugLog(...) NSLog(__VA_ARGS__)
#define debugMethod() NSLog(@"%s", __func__)
#define debugError() NSLog(@"Error at %s Line:%d", __func__, __LINE__)
#else
#define debugLog(...)
#define debugMethod()
#define debugError()
#endif
#define PATH_OF_DOCUMENT [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]
@implementation YTKKeyValueItem
- (NSString *)description {
return [NSString stringWithFormat:@"id=%@, value=%@, timeStamp=%@", _itemId, _itemObject, _createdTime];
}
@end
@interface YTKKeyValueStore()
@property (strong, nonatomic) FMDatabaseQueue * dbQueue;
@end
@implementation YTKKeyValueStore
static NSString *const DEFAULT_DB_NAME = @"database.sqlite";
static NSString *const CREATE_TABLE_SQL =
@"CREATE TABLE IF NOT EXISTS %@ ( \
id TEXT NOT NULL, \
json TEXT NOT NULL, \
createdTime TEXT NOT NULL, \
PRIMARY KEY(id)) \
";
static NSString *const UPDATE_ITEM_SQL = @"REPLACE INTO %@ (id, json, createdTime) values (?, ?, ?)";
static NSString *const QUERY_ITEM_SQL = @"SELECT json, createdTime from %@ where id = ? Limit 1";
static NSString *const SELECT_ALL_SQL = @"SELECT * from %@";
static NSString *const CLEAR_ALL_SQL = @"DELETE from %@";
static NSString *const DELETE_ITEM_SQL = @"DELETE from %@ where id = ?";
static NSString *const DELETE_ITEMS_SQL = @"DELETE from %@ where id in ( %@ )";
static NSString *const DELETE_ITEMS_WITH_PREFIX_SQL = @"DELETE from %@ where id like ? ";
+ (BOOL)checkTableName:(NSString *)tableName {
if (tableName == nil || tableName.length == 0 || [tableName rangeOfString:@" "].location != NSNotFound) {
debugLog(@"ERROR, table name: %@ format error.", tableName);
return NO;
}
return YES;
}
- (id)init {
return [self initDBWithName:DEFAULT_DB_NAME];
}
- (id)initDBWithName:(NSString *)dbName {
self = [super init];
if (self) {
NSString * dbPath = [PATH_OF_DOCUMENT stringByAppendingPathComponent:dbName];
debugLog(@"dbPath = %@", dbPath);
if (_dbQueue) {
[self close];
}
_dbQueue = [FMDatabaseQueue databaseQueueWithPath:dbPath];
}
return self;
}
- (id)initWithDBWithPath:(NSString *)dbPath {
self = [super init];
if (self) {
debugLog(@"dbPath = %@", dbPath);
if (_dbQueue) {
[self close];
}
_dbQueue = [FMDatabaseQueue databaseQueueWithPath:dbPath];
}
return self;
}
- (void)createTableWithName:(NSString *)tableName {
if ([YTKKeyValueStore checkTableName:tableName] == NO) {
return;
}
NSString * sql = [NSString stringWithFormat:CREATE_TABLE_SQL, tableName];
__block BOOL result;
[_dbQueue inDatabase:^(FMDatabase *db) {
result = [db executeUpdate:sql];
}];
if (!result) {
debugLog(@"ERROR, failed to create table: %@", tableName);
}
}
- (void)clearTable:(NSString *)tableName {
if ([YTKKeyValueStore checkTableName:tableName] == NO) {
return;
}
NSString * sql = [NSString stringWithFormat:CLEAR_ALL_SQL, tableName];
__block BOOL result;
[_dbQueue inDatabase:^(FMDatabase *db) {
result = [db executeUpdate:sql];
}];
if (!result) {
debugLog(@"ERROR, failed to clear table: %@", tableName);
}
}
- (void)putObject:(id)object withId:(NSString *)objectId intoTable:(NSString *)tableName {
if ([YTKKeyValueStore checkTableName:tableName] == NO) {
return;
}
NSError * error;
NSData * data = [NSJSONSerialization dataWithJSONObject:object options:0 error:&error];
if (error) {
debugLog(@"ERROR, faild to get json data");
return;
}
NSString * jsonString = [[NSString alloc] initWithData:data encoding:(NSUTF8StringEncoding)];
NSDate * createdTime = [NSDate date];
NSString * sql = [NSString stringWithFormat:UPDATE_ITEM_SQL, tableName];
__block BOOL result;
[_dbQueue inDatabase:^(FMDatabase *db) {
result = [db executeUpdate:sql, objectId, jsonString, createdTime];
}];
if (!result) {
debugLog(@"ERROR, failed to insert/replace into table: %@", tableName);
}
}
- (id)getObjectById:(NSString *)objectId fromTable:(NSString *)tableName {
YTKKeyValueItem * item = [self getYTKKeyValueItemById:objectId fromTable:tableName];
if (item) {
return item.itemObject;
} else {
return nil;
}
}
- (YTKKeyValueItem *)getYTKKeyValueItemById:(NSString *)objectId fromTable:(NSString *)tableName {
if ([YTKKeyValueStore checkTableName:tableName] == NO) {
return nil;
}
NSString * sql = [NSString stringWithFormat:QUERY_ITEM_SQL, tableName];
__block NSString * json = nil;
__block NSDate * createdTime = nil;
[_dbQueue inDatabase:^(FMDatabase *db) {
FMResultSet * rs = [db executeQuery:sql, objectId];
if ([rs next]) {
json = [rs stringForColumn:@"json"];
createdTime = [rs dateForColumn:@"createdTime"];
}
[rs close];
}];
if (json) {
NSError * error;
id result = [NSJSONSerialization JSONObjectWithData:[json dataUsingEncoding:NSUTF8StringEncoding]
options:(NSJSONReadingAllowFragments) error:&error];
if (error) {
debugLog(@"ERROR, faild to prase to json");
return nil;
}
YTKKeyValueItem * item = [[YTKKeyValueItem alloc] init];
item.itemId = objectId;
item.itemObject = result;
item.createdTime = createdTime;
return item;
} else {
return nil;
}
}
- (void)putString:(NSString *)string withId:(NSString *)stringId intoTable:(NSString *)tableName {
if (string == nil) {
debugLog(@"error, string is nil");
return;
}
[self putObject:@[string] withId:stringId intoTable:tableName];
}
- (NSString *)getStringById:(NSString *)stringId fromTable:(NSString *)tableName {
NSArray * array = [self getObjectById:stringId fromTable:tableName];
if (array && [array isKindOfClass:[NSArray class]]) {
return array[0];
}
return nil;
}
- (void)putNumber:(NSNumber *)number withId:(NSString *)numberId intoTable:(NSString *)tableName {
if (number == nil) {
debugLog(@"error, number is nil");
return;
}
[self putObject:@[number] withId:numberId intoTable:tableName];
}
- (NSNumber *)getNumberById:(NSString *)numberId fromTable:(NSString *)tableName {
NSArray * array = [self getObjectById:numberId fromTable:tableName];
if (array && [array isKindOfClass:[NSArray class]]) {
return array[0];
}
return nil;
}
- (NSArray *)getAllItemsFromTable:(NSString *)tableName {
if ([YTKKeyValueStore checkTableName:tableName] == NO) {
return nil;
}
NSString * sql = [NSString stringWithFormat:SELECT_ALL_SQL, tableName];
__block NSMutableArray * result = [NSMutableArray array];
[_dbQueue inDatabase:^(FMDatabase *db) {
FMResultSet * rs = [db executeQuery:sql];
while ([rs next]) {
YTKKeyValueItem * item = [[YTKKeyValueItem alloc] init];
item.itemId = [rs stringForColumn:@"id"];
item.itemObject = [rs stringForColumn:@"json"];
item.createdTime = [rs dateForColumn:@"createdTime"];
[result addObject:item];
}
[rs close];
}];
// parse json string to object
NSError * error;
for (YTKKeyValueItem * item in result) {
error = nil;
id object = [NSJSONSerialization JSONObjectWithData:[item.itemObject dataUsingEncoding:NSUTF8StringEncoding]
options:(NSJSONReadingAllowFragments) error:&error];
if (error) {
debugLog(@"ERROR, faild to prase to json.");
} else {
item.itemObject = object;
}
}
return result;
}
- (void)deleteObjectById:(NSString *)objectId fromTable:(NSString *)tableName {
if ([YTKKeyValueStore checkTableName:tableName] == NO) {
return;
}
NSString * sql = [NSString stringWithFormat:DELETE_ITEM_SQL, tableName];
__block BOOL result;
[_dbQueue inDatabase:^(FMDatabase *db) {
result = [db executeUpdate:sql, objectId];
}];
if (!result) {
debugLog(@"ERROR, failed to delete item from table: %@", tableName);
}
}
- (void)deleteObjectsByIdArray:(NSArray *)objectIdArray fromTable:(NSString *)tableName {
if ([YTKKeyValueStore checkTableName:tableName] == NO) {
return;
}
NSMutableString *stringBuilder = [NSMutableString string];
for (id objectId in objectIdArray) {
NSString *item = [NSString stringWithFormat:@" '%@' ", objectId];
if (stringBuilder.length == 0) {
[stringBuilder appendString:item];
} else {
[stringBuilder appendString:@","];
[stringBuilder appendString:item];
}
}
NSString *sql = [NSString stringWithFormat:DELETE_ITEMS_SQL, tableName, stringBuilder];
__block BOOL result;
[_dbQueue inDatabase:^(FMDatabase *db) {
result = [db executeUpdate:sql];
}];
if (!result) {
debugLog(@"ERROR, failed to delete items by ids from table: %@", tableName);
}
}
- (void)deleteObjectsByIdPrefix:(NSString *)objectIdPrefix fromTable:(NSString *)tableName {
if ([YTKKeyValueStore checkTableName:tableName] == NO) {
return;
}
NSString *sql = [NSString stringWithFormat:DELETE_ITEMS_WITH_PREFIX_SQL, tableName];
NSString *prefixArgument = [NSString stringWithFormat:@"%@%%", objectIdPrefix];
__block BOOL result;
[_dbQueue inDatabase:^(FMDatabase *db) {
result = [db executeUpdate:sql, prefixArgument];
}];
if (!result) {
debugLog(@"ERROR, failed to delete items by id prefix from table: %@", tableName);
}
}
- (void)close {
[_dbQueue close];
_dbQueue = nil;
}
@end