web-dev-qa-db-ja.com

コアデータバックグラウンドコンテキストのベストプラクティス

コアデータで行う必要がある大きなインポートタスクがあります。
コアデータモデルは次のようになります。

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;

私が確信していないもう1つのことは、メインコンテキストでいつsaveを呼び出すかです。私の例では、インポートの最後にsaveを呼び出すだけですが、使用する例を見ました:

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

前に述べたように、ユーザーが更新中にデータとやり取りできるようにしたいので、インポートで同じ車を変更しているときに車の種類を変更した場合、私が書いた方法は安全ですか?

更新:

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

これはAppDelegateのコアデータ設定です。

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;
}  

そして、これが私のインポート方法が今どのように見えるかです:

- (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];
            }];
        }
    }
}];
74
Eyal

これは、初めてコアデータにアクセスする人々にとって非常に紛らわしいトピックです。私はこれを軽く言ってはいませんが、経験上、Appleのドキュメントはこの問題について多少誤解を招く可能性があると確信しています(実際に注意深く読んだ場合、一貫性がありますが、多くの場合、データの結合が親/子コンテキストに依存して単純に子から親に保存するよりも優れたソリューションである理由を十分に説明しないでください)。

このドキュメントは、親/子コンテキストがバックグラウンド処理を行うための新しい推奨される方法であることを強く印象付けています。ただし、Appleいくつかの強力な警告を強調することを怠ります。まず、子コンテキストにフェッチするものはすべて親から取得されることに注意してください。したがって、実行中のメインコンテキストの子を制限することをお勧めしますメインスレッドでUIに既に表示されているデータを処理(編集)します。一般的な同期タスクに使用する場合は、データの範囲をはるかに超えるデータを処理する必要があります。 NSPrivateQueueConcurrencyTypeを子編集コンテキストに使用している場合でも、メインコンテキストを介して大量のデータをドラッグする可能性があり、パフォーマンスの低下やブロックにつながる可能性があります。メインコンテキストは、同期に使用するコンテキストの子です。手動で実行する場合を除き、同期の更新は通知されず、コンテキストで潜在的に長時間実行されるタスクを実行するためです。メインコンテキストの子である編集コンテキストから、メインの連絡先、データストアまでのカスケードとして開始された保存に応答する必要がある場合があります。データを手動でマージするか、メインコンテキストで無効にする必要があるものを追跡し、再同期する必要があります。最も簡単なパターンではありません。

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

NSPrivateQueueConcurrencyTypeをコンテキストを最上位の親として保存し、データストアに直接保存するのが最善の策です(そして、ここで一般的なソリューションを提供します。最適なソリューションは詳細な要件に依存する可能性があります)。 [編集:このコンテキストで直接行うことはあまりありません]、その保存コンテキストに少なくとも2つの直接の子を与えます。 UIに使用するNSMainQueueConcurrencyTypeメインコンテキスト[編集:このコンテキストでデータを編集しないでください]添付図のオプションA)同期タスク。

次に、メインコンテキストを同期コンテキストによって生成されたNSManagedObjectContextDidSave通知のターゲットにし、通知.userInfoディクショナリをメインコンテキストのmergeChangesFromContextDidSaveNotification:に送信します。

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

一方、ユーザーアクションにより大量のデータが処理される可能性がある場合は、保存コンテキストに3つの直接の子があるように、メインコンテキストと同期コンテキストの別のピアにすることを検討する必要があります。 メイン同期(プライベートキュータイプ)および編集(プライベートキュータイプ)。この配置を図のオプションBとして示しました。

同期コンテキストと同様に、データの保存時に[編集:通知を受信するメインコンテキストを構成]する必要があります(または、データを更新するときに細分性が必要な場合)。 )。この配置では、メインコンテキストがsave:メソッドを呼び出す必要がないことに注意してください。 enter image description here

親/子関係を理解するには、オプションAを使用します。親子アプローチは、編集コンテキストがNSManagedObjectsをフェッチする場合、保存コンテキスト、メインコンテキスト、最後に編集コンテキストに「コピー」(登録)されることを意味します。それらに変更を加えることができ、それからsaveを呼び出すと:編集コンテキストで、変更が保存されますメインコンテキストにのみ。メインコンテキストでsave:を呼び出してから、ディスクに書き出す前にsaveコンテキストでsave:を呼び出す必要があります。

子から親まで保存すると、さまざまなNSManagedObjectの変更と保存の通知が発生します。たとえば、フェッチ結果コントローラーを使用してUIのデータを管理している場合、デリゲートメソッドが呼び出されるので、必要に応じてUIを更新できます。

結果:編集コンテキストでオブジェクトとNSManagedObject Aをフェッチし、それを変更して保存すると、変更がメインコンテキストに返されます。これで、メインコンテキストと編集コンテキストの両方に対して、変更されたオブジェクトが登録されました。そうするのは悪いスタイルですが、メインコンテキストでオブジェクトを再度変更することができ、編集コンテキストに保存されているオブジェクトとは異なるようになります。その後、編集コンテキストに保存されているオブジェクトをさらに変更しようとすると、変更はメインコンテキストのオブジェクトと同期しなくなり、編集コンテキストを保存しようとするとエラーが発生します。

このため、オプションAのような配置では、オブジェクトのフェッチ、変更、保存、および編集コンテキストのリセット(たとえば、[editContext reset]を実行ループの1回の繰り返しで(または[editContext performBlock:])に渡された任意のブロック。統制され、メインコンテキストでanyの編集を行わないようにすることも最適です。メインスレッド、多くのオブジェクトを編集コンテキストにフェッチする場合、メインコンテキストはフェッチ処理を行いますメインスレッド上これらのオブジェクトは親コンテキストから子コンテキストに繰り返しコピーされます。大量のデータが処理されているため、UIが応答しなくなる可能性があるため、たとえば、管理対象オブジェクトの大規模なストアがあり、それらすべてを編集するUIオプションがある場合は、この場合、オプションAのようにアプリを構成するのは悪い考えです。このような場合、オプションBの方が良い方法です。

数千のオブジェクトを処理していない場合は、オプションAで十分です。

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

170
TheBasicMind

まず、親/子コンテキストはバックグラウンド処理用ではありません。これらは、複数のView Controllerで作成される可能性のある関連データのアトミック更新用です。したがって、最後のView Controllerがキャンセルされた場合、親に悪影響を与えることなく子コンテキストを破棄できます。これは、Apple [^ 1]のこの回答の最後にあります。これで問題はなくなり、よくある間違いに陥ることはありません。バックグラウンドコアデータを適切に行うため。

新しい永続ストアコーディネーター(iOS 10では不要になりました。以下の更新を参照)とプライベートキューコンテキストを作成します。保存通知をリッスンし、変更をメインコンテキストにマージします(iOS 10では、コンテキストにこれを自動的に行うプロパティがあります)

Appleによるサンプルについては、「地震:バックグラウンドキューを使用したコアデータストアへの入力」を参照してください https://developer.Apple.com/library/mac/samplecode/Earthquakes/ Introduction/Intro.html 2014-08-19の改訂履歴からわかるように、「2番目のCore Dataスタックを使用してバックグラウンドキューのデータを取得する方法を示す新しいサンプルコード」を追加しました。

AAPLCoreDataStackManager.mからのビットは次のとおりです。

// Creates a new Core Data stack and returns a managed object context associated with a private queue.
- (NSManagedObjectContext *)createPrivateQueueContext:(NSError * __autoreleasing *)error {

    // It uses the same store and model, but a new persistent store coordinator and context.
    NSPersistentStoreCoordinator *localCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[AAPLCoreDataStackManager sharedManager].managedObjectModel];

    if (![localCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil
                                                  URL:[AAPLCoreDataStackManager sharedManager].storeURL
                                              options:nil
                                                error:error]) {
        return nil;
    }

    NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    [context performBlockAndWait:^{
        [context setPersistentStoreCoordinator:localCoordinator];

        // Avoid using default merge policy in multi-threading environment:
        // when we delete (and save) a record in one context,
        // and try to save edits on the same record in the other context before merging the changes,
        // an exception will be thrown because Core Data by default uses NSErrorMergePolicy.
        // Setting a reasonable mergePolicy is a good practice to avoid that kind of exception.
        context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy;

        // In OS X, a context provides an undo manager by default
        // Disable it for performance benefit
        context.undoManager = nil;
    }];
    return context;
}

そして、AAPLQuakesViewController.m

- (void)contextDidSaveNotificationHandler:(NSNotification *)notification {

    if (notification.object != self.managedObjectContext) {

        [self.managedObjectContext performBlock:^{
            [self.managedObjectContext mergeChangesFromContextDidSaveNotification:notification];
        }];
    }
}

サンプルの設計方法の完全な説明は次のとおりです。

地震:「プライベート」永続ストアコーディネーターを使用してバックグラウンドでデータを取得する

Core Dataを使用するほとんどのアプリケーションは、単一の永続ストアコーディネーターを使用して、特定の永続ストアへのアクセスを仲介します。 Earthsは、リモートサーバーから取得したデータを使用して管理対象オブジェクトを作成するときに、追加の「プライベート」永続ストアコーディネーターを使用する方法を示します。

アプリケーションアーキテクチャ

アプリケーションは、2つのコアデータ「スタック」を使用します(永続ストアコーディネーターの存在によって定義されます)。最初は典型的な「汎用」スタックです。 2番目は、特にリモートサーバーからデータを取得するためにView Controllerによって作成されます(iOS 10では、2番目のコーディネーターは不要になりました。回答の最後の更新を参照してください)。

メインの永続ストアコーディネーターは、シングルトン「スタックコントローラー」オブジェクト(CoreDataStackManagerのインスタンス)によって販売されます。コーディネーター[^ 1]と連携する管理対象オブジェクトコンテキストを作成するのは、クライアントの責任です。スタックコントローラーは、アプリケーションが使用する管理対象オブジェクトモデルのプロパティ、および永続ストアの場所も提供します。クライアントは、これらの後者のプロパティを使用して、メインコーディネーターと並行して動作するように追加の永続ストアコーディネーターを設定できます。

メインビューコントローラー、QuakesViewControllerのインスタンスは、スタックコントローラーの永続ストアコーディネーターを使用して永続ストアから地震をフェッチし、テーブルビューに表示します。サーバーからのデータの取得は、サーバーから取得したレコードが新しい地震であるか、既存の地震に対する潜在的な更新であるかを判断するために永続ストアとの重要な対話を必要とする長時間実行される操作です。この操作中にアプリケーションが応答し続けることができるように、View Controllerは2番目のコーディネーターを使用して永続ストアとのやり取りを管理します。スタックコントローラーによって販売されるメインコーディネーターと同じ管理オブジェクトモデルと永続ストアを使用するようにコーディネーターを構成します。プライベートキューにバインドされた管理対象オブジェクトコンテキストを作成して、ストアからデータを取得し、ストアへの変更をコミットします。

[^ 1]:これは、「特にバトンを渡す」アプローチをサポートします。これにより、特にiOSアプリケーションでは、コンテキストが1つのView Controllerから別のView Controllerに渡されます。ルートView Controllerは、初期コンテキストを作成し、必要に応じて子View Controllerに渡します。

このパターンの理由は、管理対象オブジェクトグラフへの変更が適切に制約されるようにするためです。 Core Dataは、「ネストされた」管理オブジェクトコンテキストをサポートしています。これにより、独立したキャンセル可能な変更セットを簡単にサポートできる柔軟なアーキテクチャが可能になります。子コンテキストを使用すると、ユーザーが管理対象オブジェクトに一連の変更を加えて、単一のトランザクションとして親に一括してコミット(および最終的にストアに保存)するか、破棄することができます。アプリケーションのすべての部分が、たとえばアプリケーションデリゲートから同じコンテキストを単に取得する場合、この動作をサポートすることが困難または不可能になります。

更新:iOS 10の場合Apple同期をsqliteファイルレベルから永続コーディネーターに移動しました。プライベートキューコンテキストを作成し、メインコンテキストで使用されている既存のコーディネーターを再利用します。以前はそのようにしていたパフォーマンスの問題はありませんでした。

13
malhal

ちなみに document of Appleはこの問題を非常に明確に説明しています。Swift興味のある方は上のバージョン

let jsonArray = … //JSON data to be imported into Core Data
let moc = … //Our primary context on the main queue

let privateMOC = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
privateMOC.parentContext = moc

privateMOC.performBlock {
    for jsonObject in jsonArray {
        let mo = … //Managed object that matches the incoming JSON structure
        //update MO with data from the dictionary
    }
    do {
        try privateMOC.save()
        moc.performBlockAndWait {
            do {
                try moc.save()
            } catch {
                fatalError("Failure to save context: \(error)")
            }
        }
    } catch {
        fatalError("Failure to save context: \(error)")
    }
}

IOS 10以降で NSPersistentContainer を使用している場合はさらに簡単です

let jsonArray = …
let container = self.persistentContainer
container.performBackgroundTask() { (context) in
    for jsonObject in jsonArray {
        let mo = CarMO(context: context)
        mo.populateFromJSON(jsonObject)
    }
    do {
        try context.save()
    } catch {
        fatalError("Failure to save context: \(error)")
    }
}
4
hariszaman