永続ストレージにリレーショナル データベースを使用するカスタムNSIncrementalStoreサブクラスを実装しています。私がまだ苦労していることの 1 つは、楽観的ロックのサポートです。
(以下の私の質問にこの長い説明を飛ばしてください)
Core Data の SQLite 増分ストアが生成する SQL ログを調べることで、この問題にどのようにアプローチするかを分析し、次の結論に達しました。
データベース内の各エンティティ テーブルには、このエンティティ (行) の特定のインスタンスが変更された回数を 1 (最初の挿入) から始まるZ_OPT列があります。
管理オブジェクトが変更されるたびに、対応するデータベース行のZ_OPT値が増加します。
ストアは、 NSIncrementalStoreNodeインスタンスのキャッシュ ( Core Data ドキュメントでは行キャッシュと呼ばれます) を維持します。それぞれのインスタンスは、管理対象オブジェクトの行に対する前のSELECTまたはUPDATE SQL クエリによって返されたZ_OPT値と等しいバージョンプロパティを持ちます。
管理対象オブジェクトが NSManagedObjectContext から返されると(たとえば、NSFetchRequestを実行することによって)、MOC はこのバージョン番号を含むこのオブジェクトのスナップショットを作成します。
オブジェクトが変更または削除されると、Core Data は、キャッシュされた行とオブジェクトのスナップショットのバージョンを比較して、コンテキスト外で変更または削除されていないことを確認します。これはすべて、オブジェクトが属するコンテキストで-save:が呼び出されたときに発生します。バージョンが異なる場合、マージ競合が検出され、設定されたマージ ポリシーに基づいて処理されます。
MOC が保存されると、変更/削除されたオブジェクトごとに-newValuesForObjectWithID:withContext:error:メソッドが呼び出され、バージョン番号とともにNSIncrementalStoreNodeが返されます。次に、このバージョンがスナップショットのバージョンと比較され、それらが異なる場合、保存は適切なマージ競合で失敗します (少なくともデフォルトのマージ ポリシーでは)。
-newValuesForObjectWithID:withContext:error:オブジェクトが同じストア インスタンスを使用して他のコンテキストで同時に変更された場合、行キャッシュを最初にチェックするため、この単純な使用例は私のストアで適切に機能します。この場合、競合を検出するのに十分な、より高いバージョン番号を持つ更新された行がキャッシュに含まれます。
しかし、同じデータベース ファイルを使用する他のアプリケーションまたは他のストア インスタンスによって、基になるデータベースがストア外で変更されたことをどのように検出できますか? これはめったにない特殊なケースであることはわかっていますが、Core Data はそれを適切に処理しており、私も同じようにしたいと考えています。
Core Data のストアは、次のような SQL クエリを使用して、オブジェクトの行を更新/削除します。
UPDATE ZFOO SET Z_OPT=Y, (...) WHERE (...) AND Z_OPT=X
DELETE FROM ZFOO WHERE (...) AND Z_OPT=X
X - (キャッシュから) ストアに最後に認識されたバージョン番号
Y - 新しいバージョン番号
このようなクエリが失敗した場合 (影響を受ける行がない場合)、行はストアのキャッシュで更新され、そのバージョンが以前にキャッシュされたものと比較されます。
私の質問は次のとおりです。カスタムNSIncrementalStoreは、一部の更新/削除/ロックされたオブジェクトで楽観的ロックの失敗が発生したことを Core Data にどのように通知できますか? NSSaveChangesRequestを処理するときに-executeRequest:withContext:error:メソッドが渡されたことを認識できるのはストアだけです。
基礎となるデータベースがストアの下で変更されない場合、Core Data が-newValuesForObjectWithID:withContext:error:を変更/削除/ロックされたオブジェクトごとに呼び出してから、ストアで変更の保存要求を実行するため、競合が検出されます。NSIncrementalStoreが保存要求の処理を開始した後に楽観的ロックの失敗が発生したことを Core Data に通知する方法を見つけることができませんでした。それを行うための文書化されていない方法はありますか?その場合、Core Data はいくつかの例外をスローするように見えますが、これは魔法のように失敗した保存要求に変換され、NSError がすべての競合を一覧表示します。-executeRequest:withContext:error:から nil を返すことによってのみ、部分的に模倣することができます。自分でエラーメッセージを作成します。このシナリオでも、標準の Core Data 競合処理メカニズムを使用する方法が必要だと思います。