FATAL ERROR
NSFetchedResultsController を使っているアプリで下記のエラーが出た。
2010-06-25 12:29:11.679 HairConcierge[29825:207] *** Terminating app due to
uncaught exception 'NSInternalInconsistencyException', reason: 'FATAL ERROR:
The persistent cache of section information does not match the current
configuration. You have illegally mutated the NSFetchedResultsController's
fetch request, its predicate, or its sort descriptor without either disabling
caching or using +deleteCacheWithName:'
状況としては、下記のデータがありそれぞれで NSFetchedResultsController を使い UITableViewを表示しているケース。
|[ブログ][コメント]
|
|ーブログA
| |ーーーコメント1
| |ーーーコメント2
| |ーーーコメント3
| | :
|ーブログB
| |ーーーコメント1
| |ーーーコメント2
| |ーーーコメント3
| | :
ブログAのコメント一覧を見た後、ブログBのコメント一覧を見ようとするところで先程のエラーが発生した。
原因は NSFetchedResultsController のキャッシュ。エラーメッセージにも書いてあるようにキャッシュの内容と実体との整合性が取れていないが為に FATAL ERROR となっている( +deleteCacheWithName: を使ってキャッシュを削除しろともある)。
NSFetchedResultsController のキャッシュ
NSFetchedResultsControllerインスタンスの作成コードはこんな感じ。
NSFetchedResultsController *aFetchedResultsController =
[[NSFetchedResultsController alloc]
initWithFetchRequest:fetchRequest
managedObjectContext:coreDataManager.managedObjectContext
sectionNameKeyPath:@"postedYear" cacheName:@"Root"];
NSFetchedResultsController のキャッシュは、インスタンス生成時に名前をつけると作成される。上記の場合、@"Root" と名付けている。これは Xcodeのテンプレートで作成されたものをそのまま使っている。どうもこれが原因らしい。
リファレンス:
NSFetchedResultsController Class Reference - The Cache
リファレンスによるとキャッシュは order と sections で使われるとのこと。データそのもののキャッシュではない(実際 SQLを見てみると、キャッシュの有無にかかわらず発行されている SQLの数に違いはない)。sectionを利用していて、特に件数が多い場合にこのキャッシュが有用だと思われる(例えば各行が日付を持っていて、年単位のsectionを指定している場合など)。
キャッシュの再利用条件としてリファレンスでは次の説明がある。
The controller compares the current entity name, entity version hash, sort descriptors, section key-path, and total object count with those stored in the cache, as well as the modification date of the cached information file and the persistent store file.
これらの諸条件が揃った場合のみキャッシュが再利用される。大抵の場合、レコードに変更が入った場合には上記条件を満たさないことが多いと思われるので、キャッシュの効果が期待できるのは変更が入らない場合がほとんどだと考えて良いと思われる。
エラーの原因
今回 @"Root" という名前でキャッシュを作っていたが、NSFetchedResultsController のインスタンスはブログ毎に作り直していたので検索条件が違っていた。
ブログAのコメント一覧用の NSFetchedResultsController(条件:ブログ==ブログA)
ブログBのコメント一覧用の NSFetchedResultsController(条件:ブログ==ブログB)
コードはこんな感じ。
NSPredicate* predicate =
[NSPredicate predicateWithFormat:@"blog == %@", blog]; // blogは NSManagedObject
[fetchRequest setPredicate:predicate];
NSFetchedResultsController *aFetchedResultsController =
[[NSFetchedResultsController alloc]
initWithFetchRequest:fetchRequest
managedObjectContext:coreDataManager.managedObjectContext
sectionNameKeyPath:@"postedYear" cacheName:@"Root"];
本来であれば別のリストを扱っているのでキャッシュ名をブログ毎に変えるべきだったのに、同じ名前@"Root"を使い回したのが今回のエラーの原因。今回はテストデータを自動生成して使っていた為、件数までも同じでキャッシュの再利用条件が満たされる状況になっていた。その為、NSFetchedResultController はブログAのコメント一覧で作成したキャッシュを、ブログBのコメント一覧で再利用する判断を行った。しかし実際にはブログAのコメント一覧とブログBのコメント一覧の中身がまったくことなる為に整合性確認で失敗し FATAL ERRORとなった(逆に同じ件数でなかったら再利用条件が満たされないのでキャッシュが再作成されてエラーに気がつかなかったのかもしれない)。
イメージ:
1. ブログAのコメント一覧作成 => キャッシュ@"Root"に格納
2. ブログBのコメント一覧作成 <= キャッシュ@"Root"の再利用
3. データの整合性が取れず、FATAL ERROR
解決策
キャッシュの使用をやめるか、キャッシュ名をリスト毎に変えれば良い。
まずキャッシュを使わない設定を行ったところ、このエラーは出なくなった。
NSFetchedResultsController *aFetchedResultsController =
[[NSFetchedResultsController alloc]
initWithFetchRequest:fetchRequest
managedObjectContext:coreDataManager.managedObjectContext
sectionNameKeyPath:@"postedYear" cacheName:nil];
次にキャッシュを使う場合は、条件に応じた名前を用意すればいいから違いとなっている部分すなわちブログの情報を元にキャッシュ名を作ればいい。
(例)blogが NSManagedObject の場合、ユニークな値として objectIDが使える
NString* cacheName = [[[blog objectID] URIRepresentation] description];
NSFetchedResultsController *aFetchedResultsController =
[[NSFetchedResultsController alloc]
initWithFetchRequest:fetchRequest
managedObjectContext:coreDataManager.managedObjectContext
sectionNameKeyPath:@"postedYear" cacheName:cacheName];
ただしこの場合、閲覧したブログの数だけキャッシュが作成されるのでその分ディスク(リファレンスにはそう書いてあった)を消費することになる。
If the controller can’t find an appropriate cache, it calculates the required sections and the order of objects within sections. It then writes this information to disk.