web-dev-qa-db-fra.com

Pratique exemplaire du contexte de base des données de base

J'ai une grosse tâche d'importation à faire avec les données de base.
Disons que mon modèle de données principal ressemble à ceci:

Car
----
identifier 
type

Je récupère une liste d'informations sur la voiture JSON sur mon serveur, puis je souhaite la synchroniser avec mes données de base Car objet, ce qui signifie:
Si c'est une nouvelle voiture -> créer un nouvel objet Core Data Car à partir de la nouvelle information.
Si la voiture existe déjà -> mettez à jour l’objet Core Data Car.

Donc, je veux faire cette importation en arrière-plan sans bloquer l'interface utilisateur et pendant que l'utilisation fait défiler une vue de table cars qui présente toutes les voitures.

Actuellement, je fais quelque chose comme ça:

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

Mais je ne suis pas vraiment sûr de faire la bonne chose ici, par exemple:

Puis-je utiliser setParentContext?
J'ai vu quelques exemples qui l'utilisent comme ceci, mais j'ai vu d'autres exemples qui n'appellent pas setParentContext, mais ils font quelque chose comme ceci:

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

Une autre chose dont je ne suis pas sûr est de savoir quand appeler save sur le contexte principal. Dans mon exemple, j'appelle simplement save à la fin de l'importation, mais j'ai vu des exemples qui utilisent:

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

Comme je l'ai déjà mentionné, je souhaite que l'utilisateur puisse interagir avec les données lors de la mise à jour. Que se passe-t-il si l'utilisateur modifie un type de voiture pendant que l'importation modifie la même voiture, la façon dont je l'ai écrite est-elle sûre?

PDATE:

Grâce à la grande explication de @TheBasicMind, j'essaie d'implémenter l'option A, mon code ressemble donc à:

Voici la configuration Core Data dans 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;
}  

Et voici à quoi ressemble ma méthode d'importation:

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

Et j'ai aussi l'observateur suivant:

[[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

Il s'agit d'un sujet extrêmement déroutant pour les personnes qui abordent Core Data pour la première fois. Je ne dis pas cela à la légère, mais avec l'expérience, je suis confiant de dire que la documentation Apple est quelque peu trompeuse à ce sujet (elle est en fait cohérente si vous la lisez très attentivement, mais n'illustrez pas de manière adéquate pourquoi la fusion de données reste dans de nombreux cas une meilleure solution que de s'appuyer sur les contextes parent/enfant et de simplement sauvegarder d'un enfant au parent).

La documentation donne l'impression forte que les contextes parent/enfant sont le nouveau moyen privilégié de traitement en arrière-plan. Toutefois, Apple néglige de souligner certaines mises en garde. Premièrement, sachez que tout ce que vous récupérez dans votre contexte enfant est d'abord extrait de son parent. Par conséquent, il est préférable de limiter tout enfant du contexte principal en cours d'exécution. sur le fil principal pour traiter (éditer) des données qui ont déjà été présentées dans l'interface utilisateur sur le fil principal.Si vous l'utilisez pour des tâches de synchronisation générales, il est probable que vous souhaiterez traiter des données qui vont bien au-delà des limites de Même si vous utilisez NSPrivateQueueConcurrencyType, pour le contexte de modification enfant, vous risquez de faire glisser une grande quantité de données dans le contexte principal, ce qui peut entraîner de mauvaises performances et un blocage. le contexte principal est un enfant du contexte que vous utilisez pour la synchronisation, car il ne sera pas informé des mises à jour de synchronisation à moins que vous ne le fassiez manuellement, et vous exécuterez des tâches potentiellement longues sur un contexte vous devrez peut-être répondre aux sauvegardes lancées en cascade à partir du contexte d'édition qui est un enfant de votre contexte principal, via le contact principal et jusqu'au magasin de données. Vous devrez soit fusionner manuellement les données et éventuellement suivre ce qui doit être invalidé dans le contexte principal et re-synchronisé. Pas le modèle le plus facile.

Ce que la documentation Apple n'indique pas clairement), c'est que vous aurez probablement besoin d'un hybride des techniques décrites dans les pages décrivant la "vieille" méthode de confinement de threads, et le nouveau parent. -Les contextes d'enfants font les choses.

Votre meilleur pari est probablement (et je donne ici une solution générique, la meilleure solution peut dépendre de vos exigences détaillées), d’avoir un contexte de sauvegarde NSPrivateQueueConcurrencyType en tant que parent supérieur, qui enregistre directement dans le magasin de données. [Edit: vous ne ferez pas beaucoup directement sur ce contexte], puis donnez à ce contexte de sauvegarde au moins deux enfants directs. Un contexte principal NSMainQueueConcurrencyType que vous utilisez pour l’UI [Edit: il est préférable d’être discipliné et d’éviter toute édition des données sur ce contexte], l’autre un NSPrivateQueueConcurrencyType, que vous utilisez pour éditer les données par l’utilisateur l’option A dans le diagramme ci-joint) vos tâches de synchronisation.

Vous définissez ensuite le contexte principal comme cible de la notification NSManagedObjectContextDidSave générée par le contexte de synchronisation et envoyez le dictionnaire de notifications .userInfo au fichier mergeChangesFromContextDidSaveNotification :. du contexte principal.

La prochaine question à prendre en compte est l'endroit où vous placez le contexte d'édition de l'utilisateur (contexte dans lequel les modifications effectuées par l'utilisateur sont reflétées dans l'interface). Si les actions de l'utilisateur se limitent toujours aux modifications apportées à de petites quantités de données présentées, il est préférable d'en faire un enfant du contexte principal à l'aide de NSPrivateQueueConcurrencyType. Cette sauvegarde est alors la plus facile à gérer (l'enregistrement sauvegarde les modifications directement dans le contexte principal). vous avez un NSFetchedResultsController, la méthode de délégation appropriée sera appelée automatiquement afin que votre interface utilisateur puisse traiter le contrôleur de mises à jour: didChangeObject: atIndexPath: forChangeType: newIndexPath :) (là encore, il s'agit de l'option A).

Si, par ailleurs, les actions de l'utilisateur peuvent entraîner le traitement de grandes quantités de données, vous pouvez envisager de le transformer en un autre homologue du contexte principal et du contexte de synchronisation, de sorte que le contexte de sauvegarde ait trois enfants directs. main, sync (type de file d'attente privée) et edit (type de file d'attente privée). J'ai montré cette disposition en tant qu'option B sur le diagramme.

De la même manière que le contexte de synchronisation, vous devrez [Éditer: configurer le contexte principal pour recevoir des notifications] lorsque les données sont enregistrées (ou si vous avez besoin de plus de précision, lorsque les données sont mises à jour) et entreprendre une action pour fusionner les données (généralement à l'aide de mergeChangesFromContextDidSaveNotification: ). Notez qu'avec cet arrangement, il n'est pas nécessaire que le contexte principal appelle la méthode save:. enter image description here

Pour comprendre les relations parent/enfant, prenez l’option A: l’approche parent-enfant signifie simplement que si le contexte d’édition récupère NSManagedObjects, ils seront "copiés dans" (enregistrés avec) d’abord dans le contexte de sauvegarde, puis dans le contexte principal, puis enfin dans le contexte d’édition. Vous pourrez y apporter des modifications, puis lorsque vous appelez save: dans le contexte de modification, les modifications seront enregistrées niquement dans le contexte principal. Vous devrez appeler save: sur le contexte principal puis appeler save: sur le contexte de sauvegarde avant qu'ils ne soient écrits sur le disque.

Lorsque vous enregistrez depuis un enfant, jusqu’à un parent, les différentes notifications de modification et d’enregistrement de NSManagedObject sont déclenchées. Ainsi, par exemple, si vous utilisez un contrôleur de résultats d'extraction pour gérer vos données pour votre interface utilisateur, ses méthodes de délégation seront appelées afin que vous puissiez mettre à jour l'interface utilisateur selon les besoins.

Quelques conséquences: Si vous récupérez un objet et NSManagedObject A dans le contexte d'édition, modifiez-le et enregistrez-le afin que les modifications soient renvoyées au contexte principal. Vous avez maintenant l'objet modifié enregistré dans les contextes principal et d'édition. Ce serait un mauvais style de le faire, mais vous pouvez maintenant modifier l'objet à nouveau dans le contexte principal et il sera désormais différent de l'objet car il est stocké dans le contexte d'édition. Si vous essayez ensuite d'apporter d'autres modifications à l'objet tel qu'il est stocké dans le contexte d'édition, vos modifications ne seront pas synchronisées avec l'objet du contexte principal et toute tentative de sauvegarde du contexte d'édition générera une erreur.

Pour cette raison, avec un arrangement comme l’option A, c’est un bon modèle pour essayer de récupérer des objets, de les modifier, de les sauvegarder et de réinitialiser le contexte d’édition (par exemple, [editContext reset] avec une seule itération de la boucle d’exécution (ou dans un délai plus court)). tout bloc donné passé à [editContext performBlock:]). Il est également préférable d’être discipliné et d’éviter de faire des éditions tout sur le contexte principal. De plus, pour réitérer, puisque tout traitement sur principal est le thread principal, si vous récupérez beaucoup d'objets dans le contexte d'édition, le contexte principal effectuera son traitement d'extraction sur le thread principal car ces objets sont copiés de manière itérative de contextes parent à enfant. De nombreuses données sont en cours de traitement, ce qui peut entraîner une absence de réponse de la part de l'interface utilisateur. Par exemple, si vous disposez d'un grand magasin d'objets gérés et que vous disposez d'une option de l'interface utilisateur qui les éditerait tous en même temps. mauvaise idée dans ce cas de configurer votre application comme l'option A. Dans ce cas, l'option B est un meilleur pari.

Si vous ne traitez pas des milliers d'objets, l'option A peut être entièrement suffisante.

BTW ne vous inquiétez pas trop sur l'option que vous sélectionnez. Ce pourrait être une bonne idée de commencer par A et si vous devez passer à B. Il est plus facile que vous ne le pensez de faire un tel changement et a généralement moins de conséquences que vous ne le pensez.

170
TheBasicMind

Premièrement, les contextes parent/enfant ne sont pas destinés au traitement en arrière-plan. Ils sont destinés aux mises à jour atomiques de données connexes pouvant être créées dans plusieurs contrôleurs de vue. Ainsi, si le dernier contrôleur de vue est annulé, le contexte enfant peut être jeté sans incidence négative sur le parent. Ceci est expliqué en détail par Apple au bas de cette réponse en [^ 1]. Maintenant que c'est à l'écart et que vous n'êtes pas tombé dans l'erreur commune, vous pouvez vous concentrer sur la façon dont faire correctement les données de base en arrière-plan.

Créez un nouveau coordinateur de magasin persistant (dont vous n'avez plus besoin sous iOS 10, voir la mise à jour ci-dessous) et un contexte de file d'attente privée. Écoutez la notification de sauvegarde et fusionnez les modifications dans le contexte principal (sur iOS 10, le contexte a une propriété pour le faire automatiquement)

Pour un exemple par Apple, voir "Tremblements de terre: remplissage d’un magasin de données principal à l’aide d’une file d’arrière-plan" https: //developer.Apple.com/library/mac/samplecode/Earthquakes/ Introduction/Intro.html Comme vous pouvez le voir dans l'historique des révisions du 2014-08-19, ils ont ajouté "Un nouvel exemple de code indiquant comment utiliser une seconde pile de données de base pour récupérer des données dans une file d'attente en arrière-plan."

Voici ce bit de 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;
}

Et dans AAPLQuakesViewController.m

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

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

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

Voici la description complète de la conception de l'échantillon:

Tremblements de terre: Utilisation d'un coordinateur de magasin persistant "privé" pour extraire des données en arrière-plan

La plupart des applications qui utilisent Core Data emploient un seul coordinateur de magasin persistant pour faciliter l'accès à un magasin persistant donné. Earthquakes montre comment utiliser un coordinateur de magasin persistant supplémentaire "privé" lors de la création d'objets gérés à l'aide de données extraites d'un serveur distant.

Architecture d'application

L'application utilise deux "piles" de données de base (définies par l'existence d'un coordinateur de magasin persistant). Le premier est la pile "polyvalente" typique; le second est créé par un contrôleur de vue spécifiquement pour extraire les données d'un serveur distant (à partir de iOS 10, un second coordinateur n'est plus nécessaire, voir la mise à jour au bas de la réponse).

Le coordinateur de magasin persistant principal est vendu par un objet singleton "contrôleur de pile" (une instance de CoreDataStackManager). Il incombe à ses clients de créer un contexte d'objet géré à utiliser avec le coordinateur [^ 1]. Le contrôleur de pile transmet également les propriétés du modèle d'objet géré utilisé par l'application et l'emplacement du magasin persistant. Les clients peuvent utiliser ces dernières propriétés pour configurer d'autres coordinateurs de magasin persistant afin qu'ils fonctionnent en parallèle avec le coordinateur principal.

Le contrôleur de vue principal, une instance de QuakesViewController, utilise le coordinateur de stockage persistant du contrôleur de pile pour extraire les récits du magasin persistant à afficher dans une vue sous forme de tableau. La récupération de données du serveur peut être une opération longue nécessitant une interaction importante avec le magasin persistant pour déterminer si les enregistrements récupérés du serveur sont de nouveaux tremblements de terre ou des mises à jour potentielles de tremblements de terre existants. Pour s'assurer que l'application peut rester active pendant cette opération, le contrôleur de vue utilise un deuxième coordinateur pour gérer les interactions avec le magasin persistant. Il configure le coordinateur pour qu'il utilise le même modèle d'objet géré et le même magasin persistant que le coordinateur principal vendu par le contrôleur de pile. Il crée un contexte d'objet géré lié à une file d'attente privée pour extraire des données du magasin et valider les modifications apportées au magasin.

[^ 1]: Ceci prend en charge l'approche "passe le bâton" selon laquelle, en particulier dans les applications iOS, un contexte est transmis d'un contrôleur de vue à un autre. Le contrôleur de vue racine est chargé de créer le contexte initial et de le transmettre aux contrôleurs de vue enfant le cas échéant.

La raison de ce modèle est de garantir que les modifications apportées au graphe d'objet géré sont limitées de manière appropriée. Core Data prend en charge les contextes d'objet géré "imbriqués" permettant une architecture flexible facilitant la prise en charge d'ensembles de modifications indépendants et annulables. Avec un contexte enfant, vous pouvez autoriser l’utilisateur à apporter un ensemble de modifications aux objets gérés, qui peuvent ensuite être validées en bloc pour le parent (et finalement enregistrées dans le magasin) en tant que transaction unique, ou ignorées. Si toutes les parties de l'application récupèrent simplement le même contexte, par exemple, d'un délégué d'application, cela rend ce comportement difficile, voire impossible, à prendre en charge.

Mise à jour: Sous iOS 10 Apple a transféré la synchronisation du niveau de fichier sqlite au coordinateur persistant. Cela signifie que vous pouvez Créez à présent un contexte de file d'attente privée et réutilisez le coordinateur existant utilisé par le contexte principal sans les mêmes problèmes de performances que ceux rencontrés auparavant. Cool!

13
malhal

À propos, ce document de Apple explique ce problème très clairement. Swift version ci-dessus pour les personnes intéressées.

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)")
    }
}

Et encore plus simple si vous utilisez NSPersistentContainer pour iOS 10 et supérieur

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