iOSアプリを作ってみる(6) テーブルに書き込めない その2

ファイルをdocumentフォルダに移せば更新できるよという事は分かりました。次に突き当たったのは、それはどこの事かという疑問です。これを調べてみると、NSDocumentDirectryというのを使うと良さそうだと言うことで、こういうロジックを仕組んでみました。(パクってきただけですが)

NSArray *dirPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *docsDir = dirPath[0];
    sdbPath = [[NSString alloc] initWithString: [docsDir stringByAppendingPathComponent:@"PTStats.sqlite"]];
    NSLog(@"sdbPath : %@", sdbPath);

最後にNSLogが控えているので、ある意味「ファイルはここに置きなさい」という啓示がログに降ってくるわけです(笑)。

2013-09-07 11:19:40.127 PinyinTrainer[60567:c07] sdbPath : /Users/xxxxx/Library/Application Support/iPhone Simulator/6.1/Applications/901BB33A-31A8-4F56-A028-E025C662593E/Documents/PTStats.sqlite

さっきのと似ているようでちょっと違うんですよね。これを見れば分かると思います。

f:id:deutschina:20130907172114p:plain

先ほどのDBファイルは.appのパッケージの中に入っていました。しかしDocumentはアプリケーションの外に出ていると言うことは、少し気がかりな点が出てきます。sqliteのファイルをappの外に出しておくと言うことは、単語などのデータベースの生データを見られてしまう可能性があります。Objective-Cソースコードをパクられる事は別に何とも思いませんが、数年かけて溜めたDBが悪意のある人によって、そのままコピーされて再利用されるのは少々癪にに障ります。

じゃ、2つに分ければいいんじゃね?

そこで考えたのは、DBそのものを参照専用としてクローズドにするもの、更新をかけるものに分けるというアイディアです。具体的には

  1. 単語データ本体
  2. 紛らわしい誤回答を生成するロジック

この部分は引き続きResouceファイルとして参照しかさせないようにする。一方で、更新が必要となる

  1. 学習ステータスデータ
  2. 一般設定データ
  3. 障害解析用のログ(今のところ未実装)

これらについては、Documentフォルダに保存するという二段構えにしておきます。ただ更新可能なものについては、ある意味Publicな場所に置くので、仮にファイルが消されてしまった場合の事を想定して慎重にロジックを組む必要があります。

事例を見るとSQLを発行してテーブルを作るところから一から実行するという案も見かけましたが、もう少し簡単にしたいと思いついたのが、参照専用を逆手にする方法です。Documentフォルダに置く予定のファイルをResouceにも置いておくのです。もしApp実行時にDocumentの中にファイルが見つからなかった場合は、Resourceに置いてあった最低限の情報が入ったファイルをコピーしてDocumentの中に置きなおすのです。コピー元は参照専用のエリアにあるので変更が掛かる心配はありません。

で、こんなロジックになった

では、パクられても別に困らない(とさっき言い切ってしまった)ソースコードです。

-(void)prepareDatabase {

// Main DB - Read only
    
    NSFileManager *fileMgr = [NSFileManager defaultManager];
        
    //Main DB - Read only
    dbPath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"ChineseWords.sqlite"];
    NSLog(dbPath);

    BOOL success = [fileMgr fileExistsAtPath:dbPath];
    if(!success) {
        NSLog(@"Main Database file not found '%@'.", dbPath);
    }
    if(!(sqlite3_open([dbPath UTF8String], &db) == SQLITE_OK)) {
        NSLog(@"Cannot open the main database file.");
    }
    NSLog(@"Main database file is available now!");

// Stats DB in document directory - Read/Writable
    NSArray *dirPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *docsDir = dirPath[0];
    sdbPath = [[NSString alloc] initWithString: [docsDir stringByAppendingPathComponent:@"PTStats.sqlite"]];
    NSLog(@"sdbPath : %@", sdbPath);
    
    if ([fileMgr fileExistsAtPath:sdbPath]==NO) {
        NSLog(@"PTStats.sqlite not found. Copy from template");
        NSString *tempPath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"PTStats.sqlite"];
        if ([fileMgr fileExistsAtPath:tempPath]==NO){
            NSLog(@"Template not found");
        } else {
            [fileMgr copyItemAtPath:tempPath toPath:sdbPath error:nil];
            if ([fileMgr fileExistsAtPath:sdbPath]==YES) {
                NSLog(@"PTStats.sqlite ready after copy");
            }
        }
    } else {
        NSLog(@"PTStats.sqlite ready");
    }
    if(!(sqlite3_open([sdbPath UTF8String], &sdb) == SQLITE_OK)) {
        NSLog(@"Cannot open the stats database file.");
    }
    NSLog(@"Stats file is available now!");
    
}

前半がこれまでの参照専用の方、後半がDocumentに置く変更可能な方のファイルです。ファイルがなかったらコピーするというのが、copyItemAtPath:tempPath toPath:sdbPath error:nilという部分ですね。

これを見て分かるように、結果的に2つのSQLite接続がオープンされることになります。これに伴う他へのソースコード変更の影響は、

  1. joinを使っていたSQL発行部分の改造。具体的には2つのSQLに分割。
  2. SQL文発行時の参照先の変更。更新可能なテーブルへのアクセス時には"db"を参照していたものを"sdb"に変更。

これは前者の方がやや難しかったですね。単語のテーブルとステータスのテーブルをLEFT OUTER JOINで検索している部分があったのですが、今は別のデータベースファイルに入っているので、2つのSQLに分割しなくてはなりませんでした。後者は本当に簡単で、SQL文のFromで指定されているテーブルが変更可能な方のDBファイルに入っている場合は、参照先を変更するだけ。といっても、これまで"db"と表現されていた部分を"sdb"に置き換えるだけなので、1分もあれば切り替えが終わってしまいました。

ここまでくれば、あとは細かい挙動の修正などに集中することが出来そうです。

(つづく)