iOSアプリを作ってみる(3) : SQLiteと接続してデータの取得をする

先日、勢いで作ってしまった概念図。もうちょっとまじめに作れば良かったのですが、新たに作り直すのも面倒なので、そのまま使ってしまいましょう。

f:id:deutschina:20130904154103j:plain

今回は、赤い線でくくってある部分、SQLiteに接続してデータを受け取る部分についてです。これもまだ手探りの部分があるので、分かる人が見れば「あちゃー」といいう部分も多々にしてあるはずですが、海外勤務が長いので空気を読まないことには慣れています。

iOSでデータを扱う場合、SQLiteを使うというのは特段珍しいことではありません。なのでググるとチュートリアルがゴロゴロしています。自分もそれらを参考にしていますし、いくつかある中で自分のやりたいことに一番近いモノを参考にすれば良いのではないかと思います。ちなみに、個人的にはこのおねいさんのBlogの記事は丁寧で分かりやすいと思いました。

http://iosmadesimple.blogspot.sg/2013/05/sqlite-tutorial-part-1.html

Model用Classの作成

さて、前の記事にも書いたようにSQLiteへのアクセスそのものだけではなくて、データ構造(Model)用のClassを準備する必要があります。なのでまずはModel用のClassを作ってしまいましょう。1つの例として、単語の詳細を格納するためのClassのコーディングです。

KPTWordDetail.h

#import <Foundation/Foundation.h>

@interface KPTWordDetail : NSObject {
}

@property (nonatomic, assign)NSInteger wordID;
@property (nonatomic, retain)NSString *word;
@property (nonatomic, retain)NSString *pinyin;
@property (nonatomic, retain)NSString *pos;
@property (nonatomic, retain)NSString *meaningC;
@property (nonatomic, retain)NSString *meaningM;

@end

KPTWordDetail.m

#import "KPTWordDetail.h"

@implementation KPTWordDetail
@synthesize wordID;
@synthesize word;
@synthesize pinyin;
@synthesize pos;
@synthesize meaningC;
@synthesize meaningM;
@end

以上。。。

データを一時格納するための骨組みだけのClassなので、参照するSQLiteのテーブルの項目に合わせて@propertyと@synthesizeを書くだけ。Methodすら定義してません。実際に使うときは、インスタンス化して各要素に値を受け渡して、複数エントリを取得する場合はそれ用のNSMutableArrayのインスタンスにClassごと放り込みます。アクセスするテーブルが複数ある場合は、このようなClassを必要な数だけ用意しておきます。

SQLiteへの接続用のClass

次にSQLite用に接続するための準備が必要になります。具体的には、SQLiteのファイルを作成中のAPPにドラッグして参照できるようにしておくこと。もう1つは、"libsqlite3.0.dylib"をFrameworkとして使えるようにすること。このファイルは、XCodeSDKがインストールされていればすでにマシンの中にあるはずです。

実際のコーディングイメージはこんな感じ。

KPTDBAccess.h

#import <sqlite3.h>

@interface KPTDBAccess : NSObject {
    sqlite3 *db;
    NSMutableArray *theList;
}

@property (strong, nonatomic)NSMutableArray *theList;

// Preparation for Database
-(void)prepareDatabase;

// Select
-(void)getWordDetail:(NSInteger)wordID;
-(void)getStatsRecord:(NSInteger)wordID;
(中略)
-(void)getWIDListRandom:(NSInteger)limitNum;
(中略)

@end

最初のimportで、sqlite3.hを指定しないことには始まりません。あとは実際にDBにアクセスする方法に合わせてMethodを用意してやります。上のコードでは結構省いていますが、実際には7-8つのMethodを定義しています。

実装ファイルはこんな感じ。

KPTDBAccess.mの前半部分

#import "KPTDBAccess.h"
#import "KPTWordDetail.h"
#import "KPTStatusDetail.h"
#import "KPTWIDList.h"
#import "KPTDummyOptions.h"
#import "KPTAppSetting.h"

@implementation KPTDBAccess
@synthesize theList;

-(void)prepareDatabase {
    @try {
		NSFileManager *fileMgr = [NSFileManager defaultManager];
        NSString *dbPath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"ChineseWords.sqlite"];
        BOOL success = [fileMgr fileExistsAtPath:dbPath];
        if(!success) {
            NSLog(@"Database file not found '%@'.", dbPath);
        }
        if(!(sqlite3_open([dbPath UTF8String], &db) == SQLITE_OK)) {
            NSLog(@"Cannot open the file.");
        }
    }

    @catch (NSException *exception) {
        NSLog(@"An exception occured: %@", [exception reason]);
    }

    @finally {
        NSLog(@"Database file is available now!");
    }

}

冒頭のimportのところがてんこ盛りなのは、Model用のClassのヘッダファイルを全て指定しているからです。@synthesizeのところにある"theList"というのが取得したDBからのエントリをまとめて入れておく箱です。この箱の中身だけを次のデータ加工用のClassに受け渡すことにしました。

preparationDatabaseというmethodはSQLiteのファイルをオープンして使えるようにするための前準備です。ここは考えどころで、

①アプリの最初にSQLiteへの接続を確立してその後アプリ終了までオープンなままにする
②接続の度にSQLiteの接続を確立して必要なデータを取得したらクローズを繰り返す

というパターンを考えたのですが、後々メモリの問題などが出てこない限りは前者のケースで行く事にしました。従って、SQLiteへの接続確立だけを行うMethodを外出しにしてあります。

KPTDBAccess.mの後半部分

....
-(void)getWordDetail:(NSInteger)wordID {

    // SQL Query preparation
    NSString* querySQL = [[NSString alloc] initWithFormat:@"SELECT WID, Word, Pinyin, POS, MeaningC, MeaningM FROM CWords WHERE WID = %d", wordID];
    const char *query_stmnt = [querySQL UTF8String];
    
    sqlite3_stmt *sqlStatement;
    if(sqlite3_prepare(db, query_stmnt, -1, &sqlStatement, NULL) != SQLITE_OK){
        NSLog(@"Problem occured during preparing satement");
    } else {
        [theList init];
        [theList removeAllObjects];
        while(sqlite3_step(sqlStatement)==SQLITE_ROW) {
            KPTWordDetail *wordDetail = [[KPTWordDetail alloc] init];
            wordDetail.wordID = sqlite3_column_int(sqlStatement, 0);
            wordDetail.word = [NSString stringWithUTF8String:(char *) sqlite3_column_text(sqlStatement,1)];
            wordDetail.pinyin = [NSString stringWithUTF8String:(char *) sqlite3_column_text(sqlStatement,2)];
            wordDetail.pos = [NSString stringWithUTF8String:(char *) sqlite3_column_text(sqlStatement,3)];
            wordDetail.meaningC = [NSString stringWithUTF8String:(char *) sqlite3_column_text(sqlStatement,4)];
            wordDetail.meaningM = [NSString stringWithUTF8String:(char *) sqlite3_column_text(sqlStatement,5)];
            [theList addObject:wordDetail];
        }
        sqlite3_finalize(sqlStatement);
    }
    
}
....

これが実際にテーブルにアクセスする部分のサンプルです。最初に発行するSQL文のひな形を作っておきます。これは、前の記事で書いたSQLite manager等を使って、実際に必要とするデータが取得できることを確認した上でひな形にしておくと良いと思います。必要な部分は変数にしておいて都度変更できるように。なお、SQLに突っ込む文字列はchar型とのお約束があるようなので、生成したSQL文をこのchar変数に受け渡した上でSQL文を実行します。このあたりの細かい文は1つ1つ意味を考えるよりも、「こんなもんなんだ」とそのまま貼り付けて、必要なところだけ書き換えてあげた方が賢明だし確実です。

SQL文が問題なく発行できた場合は、while(){}の中で処理をしています。ソースコードを読むと分かりますが、取得したカラムの値をwordDetailというClassのpropertyに受け渡しています。IDなどの数字項目の場合はそのまま受け渡せます。ただし、文字型の項目の場合はNSString形式に変換した上で受け渡す必要があります。

最後にtheListというNSMutableArray型の入れ物にaddObjectしています。この例の場合は、ユニークな値を持っているIDだけで検索しているので常に検索結果は1件ですが、複数の場合はこの部分がLoop(実際にはwhile)して複数エントリが追加されるという流れです。

まだUIを作っていないのでさくっとは試せませんが、これらのMethodをどこか適当な場所から呼び出して、デバッグモードで動作を確認してみると実際にSQLiteのファイルにアクセスしてデータが拾えているかどうかを確認できると思います。

(つづく)