iOSでSQLiteを使う

iOSSQLiteを使う場合はFMDBを使うと便利です。

FMDB
GitHub - ccgus/fmdb: A Cocoa / Objective-C wrapper around SQLite

設定

SQLiteを使うためには、「Link Binary With Libraries」にlibsqlite3.0.dylibを追加する必要があります。

ダウンロードしたFMDBのzipを解凍し、srcの中にあるFMDB関連のh,mファイルをプロジェクトに追加します。
例では、Frameworksグループの中にfmdbグループを作成し、その中に追加しました。


ファイルを追加する時は、Xcodeで右クリックして「Add Files to "XXXXX"」から行ったほうが無難です。
Finderからドラッグ&ドロップでファイルを追加して、ビルド時に次のようなエラーが発生した場合、「Build Phases」-「Compile Sources」にFMDBのmファイルを追加してあげると解決すると思います。


プログラム
私の場合、データベースアクセス処理はデータベースアクセス専用のクラスを作り、staticメソッドで実装しています。


データベースオブジェクトの取得
データベースにアクセスするために、データベースファイルを読み込んでFMDatabaseオブジェクトを作成します。
二種類試してみました。

Xcodeのプロジェクトの中にデータベースファイルを用意しておく場合

// Databaseオブジェクトの取得
+(FMDatabase *)getDB1
{
	NSString *dir = [[NSBundle mainBundle] resourcePath];
	dir = [dir stringByAppendingPathComponent:@"db1.db"];
	
	return [FMDatabase databaseWithPath:dir];
}

SQLiteManagerなどのツールであらかじめデータを入れておき、そのデータをアプリから使いたい場合などに使えます。
ただし、シミュレーターだとこの方式でもデータを更新できますが、実機ではreadonly databaseとなり、データの更新ができないようです。


実行時にFMDBに自動でデータベースファイルを作らせる場合

+(FMDatabase *)getDB2
{
	NSArray*    paths = NSSearchPathForDirectoriesInDomains( NSDocumentDirectory, NSUserDomainMask, YES );
	NSString*   dir   = [paths objectAtIndex:0];
	return [FMDatabase databaseWithPath:[dir stringByAppendingPathComponent:@"db2.db"]];
}

アプリ内でデータを追加していく場合はこちらを使うことになると思います。


マスターデータなどはあらかじめ用意しておき、アプリ内でもデータを追加したい場合などは併用するのでしょうか・・。
アプリ初回起動時に、前者のデータを後者のデータに移す処理をしたりするのかもしれません。


FMDBにデータベースファイルを作らせる場合、必要なテーブルを作る処理も入れておきます。is not existsが使えるので便利ですね。

// テーブルがなければ作成
+(void)initDatabase2
{
	FMDatabase *db = [DatabaseUtil getDB2];
	NSString *sqlA = @"create table if not exists tableA(id integer primary key,name text);";
	NSString *sqlB = @"create table if not exists tableB(id integer primary key,a_id integer,name text);";
	
	[db open];
	[db executeUpdate:sqlA];
	[db executeUpdate:sqlB];
	[db close];
}

データの更新

テーブルの全データを削除し、60,000件のデータを投入する例です。

+(void)insertManyData
{
	FMDatabase *db = [DatabaseUtil getDB2];
	
	NSString *sqlD1 = @"delete from tableA";
	NSString *sqlD2 = @"delete from tableB";
	
	NSString *sqlA = @"insert into tableA(id,name) values(?,?)";
	NSString *sqlB = @"insert into tableB(id,a_id,name) values(?,?,?)";
	
	[db open];
	
	// データ削除
	[db executeUpdate:sqlD1];
	[db executeUpdate:sqlD2];
	
	// データ追加
	for(int i = 0; i < 15000; i++)
	{
		NSString *AName = [NSString stringWithFormat:@"Aデータ%d", i];
		[db executeUpdate:sqlA,[NSNumber numberWithInt:i], AName];
		
		for(int j = 0; j < 3; j++)
		{
			NSString *BName = [NSString stringWithFormat:@"Bデータ%d_%d",i,j];
			[db executeUpdate:sqlB,[NSNumber numberWithInt:(i * 10 + j)],[NSNumber numberWithInt: i],BName];
		}
	}
	
	[db close];
}

データの検索
検索結果をNSDictionaryに格納していますが、検索結果の値がnilの場合は例外が発生します。
nilの場合、NSNullを格納すればいいようです。

・1件のデータを検索

// 1件のデータ検索
+(NSDictionary *)selectData:(NSInteger)id1
{
	FMDatabase *db = [DatabaseUtil getDB2];
	
	NSString *sql = @"select t1.id id1,t1.name name1,count(t2.id) cnt from tableA t1,tableB t2 where t1.id = t2.a_id and t1.id = ? group by t1.id,t1.name";
	
	[db open];
	
	FMResultSet *results = [db executeQuery:sql,[NSNumber numberWithInt:id1]];
	NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
	if([results next])
	{
		[dictionary setValue:[NSNumber numberWithInt:[results intForColumn:@"id1"]] forKey:@"id1"];
		[dictionary setValue:[results stringForColumn:@"name1"] forKey:@"name1"];
		[dictionary setValue:[NSNumber numberWithInt:[results intForColumn:@"cnt"]] forKey:@"cnt"];
	}
	
	[db close];
	
	return [NSDictionary dictionaryWithDictionary:dictionary];
}

60,000件のデータから検索したときのレスポンス

インデックスなし インデックスあり
シミュレーター 14ms 2ms
iPod touch(第4世代) 100ms 9ms
iPad2 60ms 4ms

・データの一覧を検索する

// データ一覧検索
+(NSArray *)selectList
{
	FMDatabase *db = [DatabaseUtil getDB2];
	
	NSString *sql = @"select t1.id id1,t1.name name1,count(t2.id) cnt from tableA t1,tableB t2 where t1.id = t2.a_id group by t1.id,t1.name limit 1000";
	
	[db open];
	
	FMResultSet *results = [db executeQuery:sql];
	NSMutableArray *array = [NSMutableArray array];
	while([results next])
	{
		NSMutableDictionary *dictionary = [[NSMutableDictionary alloc]init ];
		[dictionary setValue:[NSNumber numberWithInt:[results intForColumn:@"id1"]] forKey:@"id1"];
		[dictionary setValue:[results stringForColumn:@"name1"] forKey:@"name1"];
		[dictionary setValue:[NSNumber numberWithInt:[results intForColumn:@"cnt"]] forKey:@"cnt"];
		
		[array addObject:dictionary];
		[dictionary release];
	}
	
	[db close];
	
	return [NSArray arrayWithArray:array];
}

60,000件のデータから検索したときのレスポンス

インデックスなし インデックスあり
シミュレーター 100ms 15ms
iPod touch(第4世代) 1150ms 150ms
iPad2 600ms 65ms

インデックスを適切に設定すれば高速に動作します。

その他

インデックス作成・削除

// インデックス作成
+(void)createIndex
{
	FMDatabase *db = [DatabaseUtil getDB2];
	
	NSString *sql = @"create index if not exists idx2 on tableB(a_id)";
	
	[db open];
	[db executeUpdate:sql];
	[db close];
}

// インデックス削除
+(void)dropIndex
{
	FMDatabase *db = [DatabaseUtil getDB2];
	
	NSString *sql = @"drop index if exists idx2";
	
	[db open];
	[db executeUpdate:sql];
	[db close];
}

参考にさせていただいたサイト
iOS で SQLite - FMDB の使い方 - アカベコマイリ