【iOS/Objective-c】SQLiteクライアント
実装してみた
珍しく2クラス晒す。
わたくしはオープンソース屋ではないですし、ただの職業プログラマなので、必要な機能しか実装しておりません
いい加減githubのアカウント作り直そうかな。
libsqlite3.dylib追加してね
SQLiteEntityBase.h
@interface SQLiteEntityBase : NSObject + (NSDictionary *)propertyList; + (NSString *)primaryKeyName; + (NSArray *)autoIncrementNames; @end
SQLiteEntityBase.m
#import "SQLiteEntityBase.h" #import <objc/message.h> @implementation SQLiteEntityBase + (NSArray *)availableClassNames { return @[ @"NSString", @"NSDate", ]; } + (NSDictionary *)propertyList { NSMutableDictionary *ret = [NSMutableDictionary dictionary]; unsigned int cnt = 0; objc_property_t *properties = class_copyPropertyList(self, &cnt); for (int i = 0; i < cnt; i++) { NSString *propertyName = [NSString stringWithUTF8String:property_getName(properties[i])]; NSString *typeName = [self getAvailableTypeNameOfPropery:property_getAttributes(properties[i])]; if (typeName == nil) @throw @"invalid property type"; [ret setValue:typeName forKey:propertyName]; } free(properties); return ret; } + (NSString *)getAvailableTypeNameOfPropery:(const char *)attributes { NSString * const attributeStr = [NSString stringWithCString:attributes encoding:NSUTF8StringEncoding]; NSRange const typeRangeStart = [attributeStr rangeOfString:@"T@\""]; if (typeRangeStart.location != NSNotFound) { NSString * const typeStringWithQuote = [attributeStr substringFromIndex:typeRangeStart.location + typeRangeStart.length]; NSRange const typeRangeEnd = [typeStringWithQuote rangeOfString:@"\""]; if (typeRangeEnd.location != NSNotFound) { NSString * const typeString = [typeStringWithQuote substringToIndex:typeRangeEnd.location]; BOOL available = NO; for (NSString *availableType in [self availableClassNames]) { if ([typeString isEqualToString:availableType]) { available = YES; break; } } return available ? typeString : nil; } } NSArray * attr = [[NSString stringWithUTF8String:attributes] componentsSeparatedByString:@","]; NSString * typeAttribute = [attr objectAtIndex:0]; NSString * propertyType = [typeAttribute substringFromIndex:1]; const char * rawPropertyType = [propertyType UTF8String]; if (strcmp(rawPropertyType, @encode(NSInteger)) == 0) return @"NSInteger"; else if (strcmp(rawPropertyType, @encode(double)) == 0) return @"double"; else if (strcmp(rawPropertyType, @encode(BOOL)) == 0) return @"BOOL"; return nil; } + (NSString *)primaryKeyName { // need override return nil; } + (NSArray *)autoIncrementNames { // need override return nil; } @end
SQLiteClient.h
#import <Foundation/Foundation.h> #import "SQLiteEntityBase.h" @interface SQLiteClient : NSObject - (id)initWithEntityClass:(Class)clazz; - (void)createTableIfNotExists; - (void)dropTableIfExists; - (void)insertByEntity:(SQLiteEntityBase *)entity; - (NSArray *)selectWithWhereStmt:(NSString *)whereStmt; @end
SQLiteClient.m
#import "SQLiteClient.h" #import <sqlite3.h> @interface SQLiteClient () @property (nonatomic) Class entityClass; @property (nonatomic) NSString *filePath; @property (nonatomic, readonly) const char *cFilePath; @end @implementation SQLiteClient #pragma mark - Property - (const char *)cFilePath { return [self cString:self.filePath]; } #pragma mark - Initialize - (id)init { [self doesNotRecognizeSelector:_cmd]; return nil; } - (id)initWithEntityClass:(Class)clazz { self = [super init]; if (self) { if (![clazz isSubclassOfClass:[SQLiteEntityBase class]]) @throw @"invalid entity class"; self.entityClass = clazz; NSString *docsPath = NSSearchPathForDirectoriesInDomains (NSDocumentDirectory, NSUserDomainMask, YES)[0]; self.filePath = [docsPath stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.db", NSStringFromClass(clazz)]]; } return self; } #pragma mark - Public - (void)createTableIfNotExists { sqlite3 *db = nil; NSInteger result = sqlite3_open_v2(self.cFilePath, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nil); if (result != SQLITE_OK) { sqlite3_close(db); @throw @"failed open db"; } NSMutableString *query = [NSMutableString stringWithFormat:@"CREATE TABLE IF NOT EXISTS %@ (", NSStringFromClass(self.entityClass)]; NSDictionary *propertyList = [self.entityClass propertyList]; for (NSString *propertyName in propertyList.allKeys) { [query appendString:propertyName]; if ([propertyName isEqualToString:[self.entityClass primaryKeyName]]) { NSString *typeName = propertyList[propertyName]; if (![typeName isEqualToString:@"NSInteger"]) @throw @"PK should be NSInteger"; [query appendString:@" INTEGER PRIMARY KEY"]; } for (NSString *autoIncrement in [self.entityClass autoIncrementNames]) { if ([propertyName isEqualToString:autoIncrement]) { NSString *typeName = propertyList[propertyName]; if (![typeName isEqualToString:@"NSInteger"]) @throw @"AUTOINCREMENT should be NSInteger"; [query appendString:@" AUTOINCREMENT"]; break; } } if (propertyName != propertyList.allKeys.lastObject) [query appendString:@", "]; } [query appendString:@");"]; char *errMsg; result = sqlite3_exec(db, [self cString:query], nil, nil, &errMsg); sqlite3_close(db); if (result != SQLITE_OK) @throw [NSString stringWithFormat:@"failed create table:%@", [NSString stringWithUTF8String:errMsg]]; } - (void)dropTableIfExists { sqlite3 *db = nil; NSInteger result = sqlite3_open_v2(self.cFilePath, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nil); if (result != SQLITE_OK) { sqlite3_close(db); @throw @"failed open db"; } NSString *query = [NSString stringWithFormat:@"DROP TABLE IF EXISTS %@;", NSStringFromClass(self.entityClass)]; char *errMsg; result = sqlite3_exec(db, [query cStringUsingEncoding:NSUTF8StringEncoding], nil, nil, &errMsg); sqlite3_close(db); if (result != SQLITE_OK) @throw [NSString stringWithFormat:@"failed drop table:%@", [NSString stringWithUTF8String:errMsg]]; } - (void)insertByEntity:(SQLiteEntityBase *)entity { sqlite3 *db = nil; NSInteger result = sqlite3_open_v2(self.cFilePath, &db, SQLITE_OPEN_READWRITE, nil); if (result != SQLITE_OK) { sqlite3_close(db); @throw @"failed open db"; } NSDictionary *propertyList = [[entity class] propertyList]; NSMutableString *query = [NSMutableString stringWithFormat:@"INSERT INTO %@ (%@) VALUES(", NSStringFromClass([entity class]), [propertyList.allKeys componentsJoinedByString:@","]]; for (NSString *propertyName in propertyList.allKeys) { NSString *typeName = propertyList[propertyName]; if ([typeName isEqualToString:@"NSString"]) [query appendFormat:@"\"%@\"", [entity valueForKey:propertyName]]; else if ([typeName isEqualToString:@"NSDate"]) { NSDate *date = [entity valueForKey:propertyName]; [query appendFormat:@"\"%f\"", [date timeIntervalSinceDate:[NSDate dateWithTimeIntervalSince1970:0]]]; } else [query appendFormat:@"\"%@\"", [entity valueForKey:propertyName]]; if (propertyName != propertyList.allKeys.lastObject) [query appendString:@", "]; } [query appendString:@");"]; char *errMsg; result = sqlite3_exec(db, [self cString:query], nil, nil, &errMsg); sqlite3_close(db); if (result != SQLITE_OK) @throw [NSString stringWithFormat:@"failed insert:%@", [NSString stringWithUTF8String:errMsg]]; } - (NSArray *)selectWithWhereStmt:(NSString *)whereStmt { sqlite3 *db = nil; NSInteger result = sqlite3_open_v2(self.cFilePath, &db, SQLITE_OPEN_READONLY, nil); if (result != SQLITE_OK) { sqlite3_close(db); @throw @"failed open db"; } NSMutableString *query = [NSMutableString stringWithFormat:@"SELECT * FROM %@", NSStringFromClass(self.entityClass)]; if (whereStmt.length > 0) [query appendString:whereStmt]; [query appendString:@";"]; sqlite3_stmt *stmt = nil; result = sqlite3_prepare_v2(db, [self cString:query], -1, &stmt, nil); if (result != SQLITE_OK) { sqlite3_close(db); @throw @"failed prepare db"; } NSDictionary *propertyList = [self.entityClass propertyList]; NSMutableArray *ret = [NSMutableArray array]; while (sqlite3_step(stmt) == SQLITE_ROW) { id instance = [self.entityClass new]; for (int i = 0; i < propertyList.allKeys.count; i++) { NSString *typeName = propertyList[propertyList.allKeys[i]]; if ([typeName isEqualToString:@"NSString"]) [instance setValue:[NSString stringWithUTF8String:(const char *)sqlite3_column_text(stmt, i)] forKey:propertyList.allKeys[i]]; else if ([typeName isEqualToString:@"NSDate"]) [instance setValue:[NSDate dateWithTimeIntervalSince1970:sqlite3_column_double(stmt, i)] forKey:propertyList.allKeys[i]]; else if ([typeName isEqualToString:@"NSInteger"]) [instance setValue:[NSNumber numberWithInteger:sqlite3_column_int(stmt, i)] forKey:propertyList.allKeys[i]]; else if ([typeName isEqualToString:@"double"]) [instance setValue:[NSNumber numberWithDouble:sqlite3_column_double(stmt, i)] forKey:propertyList.allKeys[i]]; else if ([typeName isEqualToString:@"BOOL"]) [instance setValue:[NSNumber numberWithBool:sqlite3_column_int(stmt, i)] forKey:propertyList.allKeys[i]]; } [ret addObject:instance]; } sqlite3_finalize(stmt); return ret; } #pragma mark - Private - (const char *)cString:(NSString *)string { return [string cStringUsingEncoding:NSUTF8StringEncoding]; } @end
つかいかた
こんな感じでEntityを定義して
#import "SQLiteEntityBase.h" @interface SampleEntity : SQLiteEntityBase @property (nonatomic) NSInteger integer; @property (nonatomic) NSString *str; @property (nonatomic) NSDate *date; @property (nonatomic) double doubleValue; @property (nonatomic) BOOL boolean; @end
こう!
SQLiteClient *client = [[SQLiteClient alloc] initWithEntityClass:[SampleEntity class]]; [client createTableIfNotExists]; SampleEntity *entity = [SampleEntity new]; entity.integer = 1; entity.str = @"aiueo"; entity.date = [NSDate date]; entity.doubleValue = 0.1; entity.boolean = YES; [client insertByEntity:entity]; NSArray *ret = [client selectWithWhereStmt:nil]; for (SampleEntity *entity in ret) { NSLog(@"%ld", (long)entity.integer); NSLog(@"%@", entity.str); NSLog(@"%@", entity.date); NSLog(@"%f", entity.doubleValue); NSLog(@"%@", entity.boolean ? @"TRUE" : @"FALSE"); }