1. ホーム
  2. ios

[解決済み] コアデータの背景とベストプラクティス

2023-03-28 08:37:26

質問

コアデータで大規模なインポートタスクを行う必要があります。

私のコアデータモデルが次のようなものだとします。

Car
----
identifier 
type

サーバーから車情報のJSONリストを取得し、コアデータと同期させたい。 Car オブジェクトと同期させたい、つまり

新車の場合 -> 新しいCore Dataを作成します。 Car オブジェクトを作成します。

車が既に存在する場合 -> Core Dataを更新します。 Car オブジェクトを更新します。

そこで、UIをブロックすることなくバックグラウンドでこのインポートを行い、使用中にすべての車を提示する車のテーブルビューをスクロールさせたいと考えています。

現在、私はこのようなことを行っています。

// create background context
NSManagedObjectContext *bgContext = [[NSManagedObjectContext alloc]initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[bgContext setParentContext:self.mainContext];

[bgContext performBlock:^{
    NSArray *newCarsInfo = [self fetchNewCarInfoFromServer]; 

    // import the new data to Core Data...
    // I'm trying to do an efficient import here,
    // with few fetches as I can, and in batches
    for (... num of batches ...) {

        // do batch import...

        // save bg context in the end of each batch
        [bgContext save:&error];
    }

    // when all import batches are over I call save on the main context

    // save
    NSError *error = nil;
    [self.mainContext save:&error];
}];

でも、例えばここで正しいことをやっているのかどうか、よくわからないんです。

を使うのはいいのでしょうか? setParentContext ?

このように使っている例も見ましたが、他の例では setParentContext を呼び出さない例もありますが、その場合はこのようになります。

NSManagedObjectContext *bgContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
bgContext.persistentStoreCoordinator = self.mainContext.persistentStoreCoordinator;  
bgContext.undoManager = nil;

私の例では、インポートの最後に保存を呼び出すだけですが、私はそれを使用する例を見ました。

[[NSNotificationCenter defaultCenter] addObserverForName:NSManagedObjectContextDidSaveNotification object:nil queue:nil usingBlock:^(NSNotification* note) {
    NSManagedObjectContext *moc = self.managedObjectContext;
    if (note.object != moc) {
        [moc performBlock:^(){
            [moc mergeChangesFromContextDidSaveNotification:note];
        }];
    }
}];  

前にも書きましたが、更新中にユーザーがデータを操作できるようにしたいので、インポートが同じ車を変更している間にユーザーが車種を変更した場合、私の書き方で大丈夫でしょうか?

UPDATEです。

TheBasicMindの素晴らしい説明のおかげで、私はオプションAを実装しようとしているので、私のコードは次のようになります。

これはAppDelegateのCore Dataの設定です。

AppDelegate.m  

#pragma mark - Core Data stack

- (void)saveContext {
    NSError *error = nil;
    NSManagedObjectContext *managedObjectContext = self.managedObjectContext;
    if (managedObjectContext != nil) {
        if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error]) {
            DDLogError(@"Unresolved error %@, %@", error, [error userInfo]);
            abort();
        }
    }
}  

// main
- (NSManagedObjectContext *)managedObjectContext {
    if (_managedObjectContext != nil) {
        return _managedObjectContext;
    }

    _managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
    _managedObjectContext.parentContext = [self saveManagedObjectContext];

    return _managedObjectContext;
}

// save context, parent of main context
- (NSManagedObjectContext *)saveManagedObjectContext {
    if (_writerManagedObjectContext != nil) {
        return _writerManagedObjectContext;
    }

    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (coordinator != nil) {
        _writerManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
        [_writerManagedObjectContext setPersistentStoreCoordinator:coordinator];
    }
    return _writerManagedObjectContext;
}  

そして、私のimportメソッドは今こんな感じになっています。

- (void)import {
    NSManagedObjectContext *saveObjectContext = [AppDelegate saveManagedObjectContext];

    // create background context
    NSManagedObjectContext *bgContext = [[NSManagedObjectContext alloc]initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    bgContext.parentContext = saveObjectContext;

    [bgContext performBlock:^{
        NSArray *newCarsInfo = [self fetchNewCarInfoFromServer];

        // import the new data to Core Data...
        // I'm trying to do an efficient import here,
        // with few fetches as I can, and in batches
        for (... num of batches ...) {

            // do batch import...

            // save bg context in the end of each batch
            [bgContext save:&error];
        }

        // no call here for main save...
        // instead use NSManagedObjectContextDidSaveNotification to merge changes
    }];
}  

あと、以下のようなオブザーバがあります。

[[NSNotificationCenter defaultCenter] addObserverForName:NSManagedObjectContextDidSaveNotification object:nil queue:nil usingBlock:^(NSNotification* note) {

    NSManagedObjectContext *mainContext = self.managedObjectContext;
    NSManagedObjectContext *otherMoc = note.object;

    if (otherMoc.persistentStoreCoordinator == mainContext.persistentStoreCoordinator) {
        if (otherMoc != mainContext) {
            [mainContext performBlock:^(){
                [mainContext mergeChangesFromContextDidSaveNotification:note];
            }];
        }
    }
}];

どのように解決するのですか?

これは、初めて Core Data にアプローチする人々にとって非常に混乱するトピックです。軽々しく言うことではありませんが、経験上、Apple のドキュメントがこの件に関してやや誤解を招くと自信を持って言えます (非常に注意深く読めば実際には一貫していますが、データをマージすることが、多くの場合、親/子コンテキストに依存して単に子から親に保存するより良いソリューションであり続ける理由は、十分に説明されていません)。

ドキュメントでは、親/子コンテキストがバックグラウンド処理を行うための新しい好ましい方法であるという強い印象を与えています。しかし、Apple は、いくつかの強い注意点を強調することを怠っています。まず、子コンテキストに取り込むすべてのものは、最初に親コンテキストを通して取り込まれることを認識してください。したがって、メインスレッドで実行されているメインコンテキストの子は、メインスレッド上のUIにすでに表示されているデータの処理(編集)に限定するのが最善です。一般的な同期タスクに使用する場合は、現在 UI に表示されている範囲をはるかに超えるデータを処理する必要がある可能性があります。子編集コンテキストにNSPrivateQueueConcurrencyTypeを使用する場合でも、メインコンテキストに大量のデータをドラッグする可能性があり、パフォーマンスの低下やブロッキングにつながる可能性があります。同期に使用するコンテキストの子コンテキストをメインコンテキストにしないほうがよいでしょう。同期更新の通知を手動で行わない限り、メインコンテキストには通知されません。その場合、手動でデータをマージするか、メインコンテキストで無効化する必要があるものを追跡して再同期する必要があります。最も簡単なパターンではありません。

Apple のドキュメントが明確にしていないのは、スレッド閉じ込めの方法と新しい親子コンテキストの方法を説明するページで説明されているテクニックのハイブリッドが必要になる可能性が高いということです。

最良の方法は、おそらく (ここで一般的な解決策を述べているので、最良の解決策は詳細な要件に依存するかもしれません)、データストアに直接保存する NSPrivateQueueConcurrencyType 保存コンテキストを最上位の親として持つことでしょう。[編集:このコンテキストで直接行うことはあまりないでしょう]、次にその保存コンテキストに少なくとも2つの直接の子を与えます。もう一つは NSPrivateQueueConcurrencyType で、ユーザーがデータを編集したり、(添付の図のオプション A で)同期タスクを実行するのに使用します。

次に、同期コンテキストによって生成された NSManagedObjectContextDidSave 通知のターゲットをメインコンテキストにし、通知の .userInfo 辞書をメインコンテキストの mergeChangesFromContextDidSaveNotification: に送ります。

次に考えるべきことは、ユーザー編集コンテキスト(ユーザーによって行われた編集がインターフェースに反映されるコンテキスト)をどこに置くかということです。ユーザーのアクションが常に少量の提示データの編集に限定される場合、NSPrivateQueueConcurrencyType を使用してこれを再びメイン コンテキストの子にすることが最善の策であり、管理が最も簡単です (save は、メイン コンテキストに直接編集を保存し、NSFetchedResultsController がある場合、適切なデリゲート メソッドが自動的に呼ばれるので UI で更新を処理できます controll:didChangeObject:atIndexPath:forChangeType:newIndexPath:) 。(これもオプションA)。

一方、ユーザーのアクションによって大量のデータが処理される可能性がある場合、メインコンテキストと同期コンテキストの別のピアにすることを検討するとよいでしょう。 メイン , 同期 (プライベートキュータイプ) と 編集 (プライベート・キュー・タイプ)の2つです。この配置を図ではオプションBとして示しました。

同期コンテキストと同様に、データが保存されたとき(またはより詳細な粒度が必要な場合は、データが更新されたとき)、データをマージするアクションを取る(通常は mergeChangesFromContextDidSaveNotification: を使用) [Edit: main context to receive notifications] が必要でしょう。この配置では、メインコンテキストが save: メソッドを呼び出す必要がないことに注意してください。

親子関係を理解するために、選択肢Aを取り上げます。親子アプローチとは、編集コンテキストがNSManagedObjectを取得すると、それらはまず保存コンテキストに、次にメインコンテキストに、最後に編集コンテキストにコピーされる(登録される)、という単純なものです。あなたはそれらに変更を加えることができます。そして、編集コンテキストで save: を呼び出すと、その変更が保存されます。 がメインコンテキストに保存されます。 . ディスクに書き出される前に、メインコンテキストで save: を呼び出し、次に save: コンテキストで save: を呼び出さなければならないでしょう。

子コンテキストから親コンテキストに保存する場合、様々な NSManagedObject の変更と保存の通知が発生します。例えば、フェッチリザルトコントローラを使用してUI用のデータを管理している場合、そのデリゲートメソッドが呼び出されるので、適宜UIを更新することができます。

いくつかの結果。編集コンテキストでオブジェクトとNSManagedObject Aを取得し、それを変更し、保存すると、その変更がメインコンテキストに返されます。この場合、変更されたオブジェクトは、メインと編集の両方のコンテキストに対して登録されることになります。このままではまずいことになりますが、メインコンテキストでもう一度オブジェクトを変更すると、 エディットコンテキストに保存されているオブジェクトとは別のオブジェクトになります。編集コンテキストに保存されているオブジェクトにさらに変更を加えようとすると、その変更はメインコンテキストのオブジェクトと同期しなくなり、編集コンテキストを保存しようとするとエラーが発生します。

このため、オプションAのような配置では、オブジェクトの取得、変更、保存、編集コンテキストのリセット(例:[editContext reset])をランループの単一反復で(または[editContext performBlock:]に渡された任意のブロック内で)試みるのがよいパターンです。また、このようなことは絶対にしないようにしましょう。 <項目 任意 の編集をメインコンテキストで行います。 また、繰り返しになりますが、main の処理はすべてメインスレッドで行われるため、編集コンテキストにたくさんのオブジェクトをフェッチすると、メインコンテキストはフェッチ処理を行うことになり、その結果 をメインスレッド でフェッチ処理を行うことになります。処理されるデータが多い場合、UI が応答しなくなることがあります。たとえば、管理対象オブジェクトの大きなストアがあり、それらをすべて編集するようなUIオプションがある場合です。このような場合、オプションAのようにアプリを構成するのは得策ではありません。

何千ものオブジェクトを処理しないのであれば、オプション A で完全に十分かもしれません。

ところで、どのオプションを選択するかについて、あまり心配する必要はありません。A から始めて、必要なら B に変更するのがよいかもしれません。そのような変更は思ったより簡単で、通常、予想より少ない結果で済みます。