直で扱うのはすごく面倒なのでFMDBというライブラリを使用する。
■ダウンロード
gitがないと生きていけない体になってしまったので以下のコマンドでcloneする。
git clone https://github.com/ccgus/fmdb.git
必要なファイル
src配下にある以下の6ファイルが必要なのでコピーしてプロジェクトにインポートする。
- FMDatabase.h
- FMDatabase.m
- FMDatabaseAdditions.h
- FMDatabaseAdditions.m
- FMResultSet.h
- FMResultSet.m
■フレームワーク
以下のフレームワークが必要になる。
- libsqlite3.dylib
ちなみにテスト用のターゲットが存在している場合にはそちらにもフレームワークを追加しておく。
■実装
設計思想によるがテーブルごとにクラスを作る。
DB.h
以下のようにハンドラ(的なインスタンス)の取得、テーブルの作成、データの取得・登録・更新・削除用のメソッドを用意する。
#import <Foundation/Foundation.h> @interface HogeDB : NSObject { NSString *dbFileName; } @property (nonatomic, retain) NSString *dbFileName; - (FMDatabase *)getDB; - (void)create; - (NSMutableArray *)getById:(long)identifer; - (bool)registerWithName:(NSString *)name; - (bool)updateById:(long)identifer withName:(NSString *)name; - (bool)deleteById:(long)identifer @end
DB.m
以下のようにして各メソッドを実装する。prepared statementなどはライブラリでカバーされている(日本語可)。
#import "HogeDB.h" #import "FMDatabase.h" #import "FMDatabaseAdditions.h" #define DB_FILE @"info.justoneplanet.iphone.database.sqlite"// データベースファイル名 #define KEY_TABLE_HOGE_VERSION @"key_table_hoge_version" @implementation HogeDB - (id)init { self = [super init]; if (self != nil) { [self setDbFileName:DB_FILE]; } return self; } // テスト用に別DBファイルを指定できるようにする - (id)initWithDBFile:(NSString *)name { self = [super init]; if (self != nil) { [self setDbFileName:name]; } return self; } // データベースハンドラ的なFMDatabaseのインスタンスを取得する - (FMDatabase *)getDB { NSArray *pathes = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *dir = [pathes objectAtIndex:0]; NSString *path = [dir stringByAppendingPathComponent:[self dbFileName]]; FMDatabase *db = [FMDatabase databaseWithPath:path]; if (![db open]) { @throw [NSException exceptionWithName:@"DBOpenException" reason:@"couldn't open specified db dile" userInfo:nil]; } return db; } // テーブルを作成する // NSDefaultのフラグで既にあるかチェック // またバージョン番号をNSDefaultに保存して // 今後のバージョンアップの際のALTER等に備える - (void)create { FMDatabase *db = [self getDB]; NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; int version = [ud integerForKey:KEY_TABLE_HOGE_VERSION]; if(version == 0){ NSDictionary *tables = [NSDictionary dictionaryWithObjectsAndKeys: @"CREATE TABLE `hoge`( `_id` INTEGER PRIMARY KEY AUTOINCREMENT, `id_category` INTEGER, `created` INTEGER, `face` TEXT, `tag` TEXT);", @"hoge", nil]; for (NSString *table in tables) {// CREATE TABLE系を全て実行 if (![db tableExists:table]) {// テーブルが存在していない場合 if (![db executeUpdate:[tables objectForKey:table], nil]) { NSLog(@"ERROR: %d: %@", [db lastErrorCode], [db lastErrorMessage]); } } } [ud setInteger:1 forKey:KEY_TABLE_HOGE_VERSION]; [ud synchronize]; } [db close]; } // IDで取得する - (NSMutableArray *)getById:(long) identifer { FMDatabase *db = [self getDB]; FMResultSet *result = [db executeQuery:@"SELECT * FROM `hoge` WHERE `_id` = ?", [NSNumber numberWithLong:identifer]]; NSMutableArray *ary = [NSMutableArray arrayWithObjects: nil]; while ([result next]) { [ary addObject:[result resultDict]]; } [result close]; [db close]; return ary; } // 登録する - (bool)registerWithName:(NSString *)name { FMDatabase *db = [self getDB]; long long int unixtime = (long long int)[[NSDate date] timeIntervalSince1970]; if([db executeUpdate:@"INSERT INTO `hoge`(`name`, `created`) VALUES(?, ?)", name, [NSNumber numberWithLongLong:unixtime]]){ [db close]; return YES; } else{// 登録に失敗した場合 NSLog(@"ERROR: %d: %@", [db lastErrorCode], [db lastErrorMessage]); [db close]; return NO; } } // idを条件にして更新 - (bool)updateById:(long)identifer withName:(NSString *)name { FMDatabase *db = [self getDB]; if ([db executeUpdate:@"UPDATE `hoge` SET `name` = ? WHERE `_id` = ?", name, [NSNumber numberWithLong:identifer]]) { [db close]; return YES; } else{// 更新に失敗した場合の処理 NSLog(@"ERROR: %d: %@", [db lastErrorCode], [db lastErrorMessage]); [db close]; return NO; } } // idを条件にして削除する - (bool)deleteById:(long) identifer { FMDatabase *db = [self getDB]; if([db executeUpdate:@"DELETE FROM `hoge` WHERE `_id` = ?", [NSNumber numberWithLong:identifer]]){ [db close]; return YES; } else{// 削除に失敗した場合の処理 NSLog(@"ERROR: %d: %@", [db lastErrorCode], [db lastErrorMessage]); [db close]; return NO; } } @end
残念ながらandroidとは違いバージョンアップによるALTERなどは自分でコントロールせねばならない。
■テスト
以下のようにGHUnitを使ってテストケースを記述する。
HogeDBTest.h
実行順序をコントロールするためにメソッド名を連番にした。
#import <GHUnitIOS/GHUnit.h> @class HogeDB; @interface HogeDBTest : GHTestCase { HogeDB *hoge; } @property (nonatomic, retain) HogeDB *hoge; - (void)test1_getById; - (void)test3_registerWithName; - (void)test4_updateById; - (void)test5_deleteById; @end
HogeDBTest.m
#import "HogeDBTest.h" #import "HogeDB.h" #define DB_FILE @"info.justoneplanet.hoge.i.test.sqlite" @implementation HogeDBTest @synthesize hoge; - (void)setUpClass { hoge = [[HogeDB alloc] initWithDBFile:DB_FILE]; [hoge create]; } - (void)tearDownClass { [hoge del]; [hoge release]; } - (void)test1_getById { NSMutableArray *data = [hoge getByIdCategory:1]; GHAssertEqualStrings( [[data objectAtIndex:([data count] - 1)] objectForKey:@"name"], @"Emily", @""); GHAssertEqualStrings( [[data objectAtIndex:0] objectForKey:@"name"], @"John", @""); } - (void)test2_registerWithName { [hoge registerWithName:@"Nick"]; NSMutableArray *data = [hoge getAll]; GHAssertEqualStrings( [[data objectAtIndex:0] objectForKey:@"name"], @"Nick", @""); } - (void)test3_updateById { [hoge updateById:2l withName:@"Mike"]; GHAssertEqualStrings( [[[hoge getById:2l] objectAtIndex:0] objectForKey:@"name"], @"Mike", @""); [hoge updateById:2l withName:@"Ken"];// 元に戻しておく } - (void)test4_deleteById { NSMutableArray *data1 = [hoge getAll]; GHAssertEqualStrings( [[data1 objectAtIndex:0] objectForKey:@"name"], @"Nick", @""); [hoge deleteById:[[[data1 objectAtIndex:0] objectForKey:@"_id"] longValue]]; NSMutableArray *data2 = [hoge getAll]; GHAssertEqualStrings( [[data2 objectAtIndex:0] objectForKey:@"name"], @"John", @""); } @end
ちょっとテストケースが少ないけどこんな感じ。