The operation couldn’t be completed. (Cocoa error 133020.) の対処方法

NSURLConnection を使って非同期でサーバ接続させて -connectionDidFinishLoading: が呼ばれたら後処理をする、、、ってセオリー通りな処理を書いたつもりなんだけど、CoreData で保存するとエラーが発生する。
毎回同じデータに対して発生するんなら、実装エラーだろーけど、成功したり失敗したりする。スレッドの実行タイミングが絡んでるよ。orz

実行してる処理はこんな感じ

        NSManagedObjectContext *context =  ...
        File *file = … // try to find NSManagedObject, if not, create new object.
        file.outOfSync = [NSNumber numberWithBool:NO];
        
        // ファイル保存
        NSError *error;
        error = nil;
        [NSManagedObjectContext save:&error];
        if (error) {
            DLog(@"file store failure. fileId:%@", file.fileId);
            DLog(@"userInfo:%@", [error userInfo]);
        }


エラーメッセージで Cocoa Error 133020 ってのが出てて、、、

The operation couldn’t be completed. (Cocoa error 133020.)


userInfo の内容をみると conflictList ってのが表示されてる。

-[FileDownloader initInternal:]_block_invoke_1[104] userInfo:{
    conflictList =     (
        "NSMergeConflict (0xed496c0) for NSManagedObject (0x95abec0) with objectID 
        '0x9571e10 <x-coredata://F7CBD47F-9A3D-49A3-9AAC-261AD79F1583/CSWorkspace/p2>' 
        with oldVersion = 11 and newVersion = 13 and old object snapshot = {
    ...
} and new cached row = {
    ...


とりあえず Cocoa error 133020 の意味は「マージポリシーの失敗:マージできません」ってことらしい。

NSManagedObjectMergeError
 Error code to denote that a merge policy failed—Core Data is unable to complete merging.

Core Data Constants Reference


前に CoreData はスレッド毎に NSManagedObjectContext を作ってあげないとダメというのを経験してて、予防策もちゃんとった(つもり)。どこでコンフリクト原因をつくっちゃったんだ? (T^T)

グーグル先生で検索しまくってもビンゴなヒントが見つからない。初心に帰って CoreData プログラミングガイドを読み返したてたら、ヒントっぽいのが見つかった。:-)

[不整合の検出と楽観的ロック]
Core Dataは、永続ストアからオブジェクトをフェッチすると、その状態のスナップショットを作ります。… このスナップショットが楽観的ロックに関与します。

フレームワークは、保存の際、編集した各オブジェクトのスナップショットトに記録されている値と、その時点で永続ストアに記録されている値を比較します。

値が一致していれば …。一方、値が違っていれば、オブジェクトをフェッチした、あるいは最後に保存した時点以降に、 ストアに変更が施されたことになります。これは楽観的ロックの失敗を意味します。

[不整合の解消]
複数の永続スタックが同じ外部データストアを参照していれば、楽観的ロックが失敗することがあります。… デフォルトの動作は、NSErrorMergePolicyとして定義されています。この方針では、マージの際 に不整合があると、保存できないようになっています。

保存メソッドはこの場合、ユーザ情報 (userInfo)辞書に「@"conflictList"」というキーでエラー情報を格納して戻ります。キーに対応する値は、不整合が生じているレコードの配列です。この配列を調べると、保存しようとしている値と、現時点でストアに格納されている値との差分が分かります。保存するためには、不整合を解消する(オブジェクトを再フェッチしてスナップショットを更新する)か、別の方針を選択しなければなりません。

エラーが発生しうる方針は、NSErrorMergePolicだけです。その他の方針、 すなわちNSMergeByPropertyStoreTrumpMergePolicy、 NSMergeByPropertyObjectTrumpMergePolicy、NSOverwriteMergePolicyの場合、編集したオブジェクトの状態に、ストア内のオブジェクトの状態をそれぞれの方法でマージした上で、保存を続 行できます。NSRollbackMergePolicyの場合は、メモリ上で行った変更を破棄することにより不整合を解消し、永続ストアにおけるオブジェクトの状態を活かします。


説明を読んだ感じ、永続ストア(直前のスナップショット?)とメモリ上のオブジェクト間で楽観的エラーが発生してると思うのが妥当。よく見ると userInfo の出力結果にも conflictList って表示されてるし、対処方法はこの辺の地雷を踏んだわけね。そうすると対処方法としては次の2つかな。

  • (1) オブジェクトを再フェッチしてスナップショットを更新する
  • (2) NSMergePolicy を NSErrorMergePolicy 以外に変更する


マージに失敗してエラーってなってるんで、(2)のマージポリシーの変更するのが良いのかな?


意訳するとこんな感じ。

キー項目 説明
NSErrorMergePolicy マージコンフリクトを起こすと保存に失敗する(デフォルト)
NSMergeByPropertyStoreTrumpMergePolicy 外部変更(external changes、永続ストア側?)に優先権を与えて、永続ストアとメモリ上のバージョン間でマージする
NSMergeByPropertyObjectTrumpMergePolicy メモリ上の変更に優先権を与えて、永続ストアとメモリ上のバージョン間でマージする
NSOverwriteMergePolicy コンフリクトした場合、変更オブジェクトで永続ストアの値(state)を上書きする
NSRollbackMergePolicy コンフリクトした場合、メモリ上の変更内容を破棄する


デフォルトの NSErrorMergePolicy(マージしない)から NSOverwriteMergePolicy あたりが妥当っぽい。こんな感じ(↓)にマージポリシーを変更して、問題回避はできたよ。

ただ、ロードしたオブジェクトが最新版になってるって保証がないとダメ。この辺は追跡調査が必要だなぁ。^^;)

        NSManagedObjectContext *context =  ...

        // ポリシー変更
        NSMergePolicy *originalMergePolicy = context.mergePolicy;
        context.mergePolicy = NSOverwriteMergePolicy;

        File *file = … // try to find NSManagedObject, if not, create new object.
        file.outOfSync = [NSNumber numberWithBool:NO];
        
        // ファイル保存
        NSError *error;
        error = nil;
        [NSManagedObjectContext save:&error];
        if (error) {
            DLog(@"file store failure. fileId:%@", file.fileId);
            DLog(@"userInfo:%@", [error userInfo]);
        }
        
        // ポリシーを戻す
       context.mergePolicy = originalMergePolicy;
動作確認