CodaDataで"NSMergeConflict for NSManagedObject"エラーが発生する

CoreDataのNSManagedObjectContextインスタンスsave:を実行した時にNSErrorが以下のエラーを返す。

NSError: code=133020, domain=NSCocoaErrorDomain, userInfo=
{
    conflictList=
    (
        NSMergeConflict (0x28164b080) for NSManagedObject (0x282e0ba70) with objectID '0xd068d480c425f056 <x-coredata://26EDCB60-2D7B-472B-863A-C88D60F76034/****>' with oldVersion = 5 and newVersion = 7 and old object snapshot = {
    ...
} and new cached row = {
    ...
};

エラーの原因

CodaDataでは以下のような状況でNSMergeConfilictが発生する。

  1. スレッド1のNSManagedObjectContextNSManagedObjectインスタンスのデータを取得する
  2. スレッド2のNSManagedObjectContextでスレッド1と同じNSManagedObjectインスタンスのデータを取得する
  3. スレッド1でNSManagedObjectインスタンスのプロパティを更新し、saveを呼び出す
  4. スレッド2でNSManagedObjectインスタンスのプロパティを更新し、saveを呼び出す。この時、スレッド1が更新したプロパティと同じプロパティを更新していると、データの競合が発生するためNSErrorがNSMergeConfilictを返す。

*スレッド1とスレッド2のNSManagedObjectContext が同じインスタンスの場合は上記のエラーは発生しない。ただし、NSManagedObjectContextはスレッドセーフではないので、そのような使い方をするとメモリアクセス違反が発生してアプリがクラッシュする場合がある。

CoreDataでは上記の状況が発生した場合、NSManagedObjectContextmergePolicyに従って競合したデータのマージ処理が行われる。初期状態ではmergePolicyNSErrorMergePolicyになっているため、NSErrorでエラーが返る。

簡単な解決方法

NSManagedObjectContextmergePolicyNSErrorMergePolicy以外に変更するとエラーは発生せず、以下のルールに従ってデータの更新が行われる。

mergePolicyの値 データの競合が発生した場合に行われる処理
NSErrorMergePolicy NSErrorがエラーを返す
NSMergeByPropertyObjectTrumpMergePolicy 競合したプロパティには更新しようとしたデータの値(上記の例の場合はスレッド2のデータの値)が使用される。それ以外のプロパティは両方の変更が適用される。
NSMergeByPropertyStoreTrumpMergePolicy 競合したプロパティには保存済みのデータの値(上記の例の場合はスレッド1のデータの値)が使用される。それ以外のプロパティは両方の変更が適用される。
NSOverwriteMergePolicy 更新しようとしたデータで上書きする。上記の例の場合はスレッド2のデータで更新され、スレッド1が更新したデータは失われる。
NSRollbackMergePolicy 更新しようとしたデータは破棄される。上記の例の場合はスレッド1のデータが残り、スレッド2が更新しようとしたデータは失われる。

理想的な解決方法

mergePolicyの変更によってエラーが発生することはなくなるが、データの更新が一部失われることには変わりがない。理想的な解決方法としては以下の記事が紹介するようにデータの競合が発生しないように処理を書き換えることが望ましい。

以下の記事ではデータの更新を一つのNSOperationQueue内でのみ実行することによってデータの競合を防ぐ方法が紹介されている。

stackoverflow.com