iOSアプリを作ってみる (10) - CoreDataでランダム検索

今、AppStoreに出しているこのアプリですが、現バージョンでは、アプリから直接SQLiteを呼び出しています。恥ずかしながら、開発開始当初CoreDataのことをよく理解できていなかったので、SQLiteを直接呼ぶしかないだろうと思い込んでいたのが最大の原因だったりします。

Pinyin Trainer

Pinyin Trainer

  • Kentaro Miyagawa
  • 無料

確かにSQLiteを直接呼ぶのは、多少SQLをかじってきた自分にとっても扱いやすいモノでした。SQL文を用意してそれを投げると結果が返っている。SQL文をある程度自由に発行できるので、TableのJoinだったりランダム検索だったり、ある意味「好き放題」できる訳ですから。

ただ、CoreDataのドキュメントに目を通すなどしてきて、やはり「王道」にも対応しておくべきだという思いがわき上がり、次のリリースからはCoreDataに切り替えることにして、あらかた開発を終えたところです。と言うことで、気がついた事をメモしておきます。

Joinができない

CoreData Programming Guideにも謳われている通り、テーブルのJoinが出来ません。このアプリの中でも、「単語のマスタ」と学習記録が残る「学習統計」という2つのテーブルをJoinさせて、クイズとして出題する単語を選択しており、CoreData化に際して対応が必要となりました。普通に考えれば、2回に分けてフェッチするしかないところですが、今回の場合は両者のテーブルエントリの関係が1対1だったので、2つのテーブルを統合してしまいました。

もともとテーブルを分けた経緯というのが、

  • マスタデータはsqliteの拡張子でdocumentフォルダに置きたくない(そのまま転用されてしまうから)
  • 一方で学習統計は更新可能な領域に置かなくてはならない

という問題を解消するのが目的でした。ただ「更新可能かつユーザがアクセスできない領域」については目処が付きました。となるとアプリの中でマルチユーザ対応でもしない限りは、両者のテーブルを分ける意味合いが薄らいでしまったのです。ならば、くっつけちゃうかという結論に至りました。

ランダム検索

Appではクイズ形式を取っている以上、単語が同じ順番で表示されるのでは意味がありません。SQLiteでは、ランダム検索があらかじめ実装されているので、このあたりのロジックを考える必要はなかったのですが、CoreDataにはこのオプションはありません。Stackoverflow.comなどの同様の議論が上がっていました。そこでは、先に選択したものをNSMutableArrayに入れて、そこから乱数などでエントリを拾うという方法が紹介されていたのですが、サイズの小さいテーブルならまだしも、単語データのような大きくなるであろうテーブルのエントリを全部読むのはメモリ消費の観点からも得策とは思えませんでした。

そこで考えたのが以下の方法です。

// Prepare for selection
NSFetchRequest *request = [[NSFetchRequest alloc]init];
NSEntityDescription *entity = [NSEntityDescription entityForName:@"CWordsStats" inManagedObjectContext:coreDataMgr.managedObjectContext];
[request setEntity:entity];
if (exclutionList != nil) {
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"NOT (wordID in %@)", exclutionList];
    [request setPredicate:predicate];
}
[request setFetchOffset:(arc4random()% cwordsEntries)];
[request setFetchLimit:1];

あらかじめテーブルの件数を数えておき(cwordsEntries)、setFetchOffsetに乱数を持たせました。例えば、該当するエントリが100件あって、乱数によってsetFetchOffsetの値が80となった場合、該当レコードの最初の80件は無視されて、その次のレコード、つまり81件目のレコードが1件(setFetchLimit:1なので)だけ選択されることになります。乱数なので取得されるレコードは毎回変わるわけで、これを欲しい件数になるまでFor文なりWhile文なりで繰り返せば良いわけです。理屈の上では、毎回1レコードだけ取得されるのでメモリの消費が極端に増えることはありません。

ひとまず、この方法で問題なく動いており、メモリ消費も許容範囲内のようなのでこれで行く事にしました。