【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");
    }