Mejor práctica del contexto de fondo de Core Data

Tengo una gran tarea de importación que debo hacer con los datos centrales.
Digamos que mi modelo de datos básicos se ve así:

Car ---- identifier type 

Busco una lista de información del coche JSON de mi servidor y luego quiero sincronizarla con mi objeto Car núcleo de datos, que significa:
Si se trata de un automóvil nuevo,> crea un objeto nuevo de Datos básicos a partir de la nueva información.
Si el auto ya existe -> actualice el objeto Core Data Car .

Así que quiero hacer esta importación en segundo plano sin bloquear la interfaz de usuario y mientras el uso se desplaza una vista de mesa de coches que presentan todos los coches.

Actualmente estoy haciendo algo como esto:

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

Pero no estoy seguro de estar haciendo lo correcto aquí, por ejemplo:

¿Está bien que use setParentContext ?
Vi algunos ejemplos que lo usan así, pero vi otros ejemplos que no llaman a setParentContext , sino que hacen algo como esto:

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

Otra cosa de la que no estoy seguro es cuándo llamar guardar en el contexto principal. En mi ejemplo, simplemente llamo guardar al final de la importación, pero vi ejemplos que usan:

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

Como mencioné anteriormente, quiero que el usuario pueda interactuar con los datos durante la actualización, entonces, ¿qué sucede si el usuario cambia el tipo de automóvil mientras la importación cambia el mismo automóvil? ¿Es seguro el modo en que lo escribí?

ACTUALIZAR:

Gracias a la gran explicación de @TheBasicMind estoy tratando de implementar la opción A, por lo que mi código se ve algo así como:

Esta es la configuración de Core Data en 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; } 

Y así es como se ve mi método de importación ahora:

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

Y también tengo el siguiente observador:

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

Este es un tema extremadamente confuso para las personas que se acercan a Core Data por primera vez. No lo digo a la ligera, pero con la experiencia, confío en decir que la documentación de Apple es un poco engañosa sobre este asunto (de hecho es consistente si la lees con mucho cuidado, pero no ilustran adecuadamente por qué sigue existiendo la fusión de datos). en muchos casos, es una mejor solución que confiar en los contextos padre / hijo y simplemente guardar de un hijo a los padres).

La documentación brinda la fuerte impresión de que los contextos padre / hijo son la nueva forma preferida de procesar el fondo. Sin embargo, Apple descuida destacar algunas advertencias fuertes. En primer lugar, tenga en cuenta que todo lo que busque en el contexto de su hijo primero se obtiene a través de su padre. Por lo tanto, es mejor limitar cualquier hijo del contexto principal que se ejecuta en el hilo principal a los datos de procesamiento (edición) que ya se han presentado en la interfaz de usuario en el hilo principal. Si lo usa para tareas de sincronización general, es probable que desee procesar datos que se extiendan mucho más allá de los límites de lo que está mostrando actualmente en la interfaz de usuario. Incluso si usa NSPrivateQueueConcurrencyType, para el contexto de edición hijo, posiblemente arrastre una gran cantidad de datos a través del contexto principal y eso puede conducir a un mal rendimiento y locking. Ahora es mejor no convertir el contexto principal en un elemento secundario del contexto que utiliza para la sincronización, ya que no se le notificarán las actualizaciones de sincronización a menos que vaya a hacer eso manualmente, además de que estará ejecutando tareas potencialmente de larga ejecución en una Es posible que tenga que responder a los inicios de las salvaciones iniciadas como una cascada desde el contexto de edición que es un elemento secundario de su contexto principal, a través del contacto principal y hasta el almacén de datos. Tendrá que fusionar manualmente los datos y también posiblemente rastrear lo que debe invalidarse en el contexto principal y volver a sincronizar. No es el patrón más fácil.

Lo que la documentación de Apple no aclara es que lo más probable es que necesite un híbrido de las técnicas descritas en las páginas que describen la “antigua” forma de encuadernación de hilos y las nuevas formas de hacer las cosas en contextos Padre-Hijo.

Su mejor opción es probablemente (y aquí le doy una solución genérica, la mejor solución puede depender de sus requisitos detallados), tener un contexto de guardar NSPrivateQueueConcurrencyType como el padre superior, que guarda directamente en el almacén de datos. [Editar: no harás mucho directamente en este contexto], luego dale a ese contexto de salvar al menos dos hijos directos. Uno su contexto principal NSMainQueueConcurrencyType que utiliza para la interfaz de usuario [Editar: es mejor ser disciplinado y evitar editar los datos en este contexto], el otro es un NSPrivateQueueConcurrencyType, que utiliza para hacer ediciones de los datos y también (en opción A en el diagtwig adjunto) sus tareas de sincronización.

A continuación, convierte el contexto principal en el objective de la notificación NSManagedObjectContextDidSave generada por el contexto de sincronización y envía el diccionario .userInfo de notificaciones al mergeChangesFromContextDidSaveNotification del contexto principal :.

La siguiente pregunta a considerar es dónde colocar el contexto de edición del usuario (el contexto donde las modificaciones realizadas por el usuario se reflejan en la interfaz). Si las acciones del usuario siempre se limitan a ediciones en pequeñas cantidades de datos presentados, entonces hacer de este un elemento secundario del contexto principal usando NSPrivateQueueConcurrencyType es su mejor apuesta y la más fácil de administrar (save guardará las ediciones directamente en el contexto principal y si tiene un NSFetchedResultsController, se llamará automáticamente al método delegado apropiado para que su UI pueda procesar el controlador de actualizaciones: didChangeObject: atIndexPath: forChangeType: newIndexPath 🙂 (de nuevo esta es la opción A).

Si, por otro lado, las acciones del usuario pueden dar lugar a que se procesen grandes cantidades de datos, es posible que desee considerar convertirlo en otro interlocutor del contexto principal y el contexto de sincronización, de modo que el contexto de guardar tenga tres hijos directos. main , sync (tipo de cola privada) y edit (tipo de cola privada). He mostrado esta disposición como la opción B en el diagtwig.

De forma similar al contexto de sincronización, necesitará [Editar: configurar el contexto principal para recibir notificaciones] cuando se guardan los datos (o si necesita más granularidad, cuando se actualicen los datos) y tomar medidas para fusionar los datos (normalmente usando mergeChangesFromContextDidSaveNotification: ) Tenga en cuenta que con esta disposición, no hay necesidad de que el contexto principal llame al método save :. enter image description here

Para comprender las relaciones padre / hijo, tome la Opción A: El enfoque padre e hijo simplemente significa que si el contexto de edición obtiene NSManagedObjects, se “copiarán” (registrarán) primero en el contexto guardado, luego en el contexto principal y finalmente en el contexto. Podrá realizar cambios en ellos, luego cuando llame a guardar: en el contexto de edición, los cambios se guardarán solo en el contexto principal . Debería llamar a guardar: en el contexto principal y luego llamar a guardar: en el contexto de guardar antes de que se escriban en el disco.

Cuando guarda de un hijo, hasta un padre, se activan las diferentes notificaciones de cambio y guardado de NSManagedObject. Por ejemplo, si está utilizando un controlador de resultados de búsqueda para administrar sus datos para su UI, se llamarán sus métodos de delegado para que pueda actualizar la interfaz de usuario según corresponda.

Algunas consecuencias: si busca objetos y NSManagedObject A en el contexto de edición, modifíquelos y guárdelos para que las modificaciones se devuelvan al contexto principal. Ahora tiene el objeto modificado registrado contra el contexto principal y el de edición. Sería un mal estilo hacerlo, pero ahora podría modificar el objeto nuevamente en el contexto principal y ahora será diferente del objeto tal como está almacenado en el contexto de edición. Si luego intenta hacer más modificaciones al objeto almacenado en el contexto de edición, sus modificaciones no estarán sincronizadas con el objeto en el contexto principal, y cualquier bash de guardar el contexto de edición generará un error.

Por esta razón, con una disposición como la opción A, es un buen patrón tratar de buscar objetos, modificarlos, guardarlos y restablecer el contexto de edición (por ejemplo, [editContext reset] con cualquier iteración del ciclo de ejecución (o dentro de cualquier bloque dado pasado a [editContext performBlock:]). También es mejor ser disciplinado y evitar hacer ediciones en el contexto principal. Además, para volver a iterar, ya que todo el procesamiento en main es el hilo principal, si lo recuperas muchos objetos para el contexto de edición, el contexto principal hará su procesamiento de búsqueda en el hilo principal, ya que esos objetos se copian iterativamente de contextos padre a hijo. Si se procesan muchos datos, esto puede causar falta de respuesta en la interfaz de usuario. Por lo tanto, si, por ejemplo, tiene una gran cantidad de objetos administrados, y tiene una opción de interfaz de usuario que daría como resultado que todos sean editados. En este caso, sería una mala idea configurar su aplicación como la opción A. tal opción de caso B es una mejor apuesta.

Si no está procesando miles de objetos, entonces la opción A puede ser completamente suficiente.

Por cierto, no te preocupes demasiado por la opción que elijas. Puede ser una buena idea comenzar con A y si necesita cambiar a B. Es más fácil de lo que podría pensar para hacer tal cambio y generalmente tiene menos consecuencias de las que podría esperar.

En primer lugar, el contexto padre / hijo no es para procesamiento en segundo plano. Son para actualizaciones atómicas de datos relacionados que podrían crearse en múltiples controladores de visualización. Por lo tanto, si se cancela el último controlador de vista, el contexto secundario se puede descartar sin efectos adversos para el padre. Esto es explicado completamente por Apple en la parte inferior de esta respuesta en [^ 1]. Ahora que está fuera del camino y no ha caído en el error común, puede enfocarse en cómo hacer correctamente los datos básicos de fondo.

Cree un nuevo coordinador de tienda persistente (ya no es necesario en iOS 10 ver actualización a continuación) y un contexto de cola privada. Escuche la notificación de guardado y combine los cambios en el contexto principal (en iOS 10 el contexto tiene una propiedad para hacer esto automáticamente)

Para ver una muestra de Apple, consulte “Terremotos: rellenando un Almacén de datos básicos utilizando una cola de fondo” https://developer.apple.com/library/mac/samplecode/Earthquakes/Introduction/Intro.html Como puede ver en el historial de revisiones en 2014-08-19 agregaron “Nuevo código de muestra que muestra cómo usar una segunda stack de Datos centrales para obtener datos en una cola de fondo”.

Aquí está ese 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; } 

Y en AAPLQuakesViewController.m

 - (void)contextDidSaveNotificationHandler:(NSNotification *)notification { if (notification.object != self.managedObjectContext) { [self.managedObjectContext performBlock:^{ [self.managedObjectContext mergeChangesFromContextDidSaveNotification:notification]; }]; } } 

Aquí está la descripción completa de cómo se diseña la muestra:

Terremotos: utilizar un coordinador de tienda persistente “privado” para obtener datos en segundo plano

La mayoría de las aplicaciones que usan Core Data emplean un único coordinador de tienda persistente para mediar en el acceso a una tienda persistente determinada. Earthquakes muestra cómo utilizar un coordinador de tienda persistente “privado” adicional al crear objetos administrados utilizando datos recuperados de un servidor remoto.

Arquitectura de aplicaciones

La aplicación utiliza dos “stacks” de datos centrales (como se define por la existencia de un coordinador de tienda persistente). El primero es la stack típica de “propósito general”; el segundo es creado por un controlador de vista específicamente para obtener datos de un servidor remoto (A partir de iOS 10, ya no se necesita un segundo coordinador, consulte la actualización en la parte inferior de la respuesta).

El coordinador de tienda persistente principal se vende mediante un objeto singleton “controlador de stack” (una instancia de CoreDataStackManager). Es responsabilidad de sus clientes crear un contexto de objeto gestionado para trabajar con el coordinador [^ 1]. El controlador de stack también vende propiedades para el modelo de objetos gestionados utilizado por la aplicación y la ubicación de la tienda persistente. Los clientes pueden usar estas últimas propiedades para configurar coordinadores de tienda persistentes adicionales para trabajar en paralelo con el coordinador principal.

El controlador de vista principal, una instancia de QuakesViewController, utiliza el coordinador de tienda persistente del controlador de stack para buscar temblores del almacén persistente para mostrarlos en una vista de tabla. La recuperación de datos del servidor puede ser una operación de larga ejecución que requiere una interacción significativa con la tienda persistente para determinar si los registros recuperados del servidor son nuevos temblores o posibles actualizaciones de terremotos existentes. Para garantizar que la aplicación puede seguir siendo receptiva durante esta operación, el controlador de vista emplea un segundo coordinador para gestionar la interacción con la tienda persistente. Configura al coordinador para usar el mismo modelo de objeto administrado y tienda persistente como el coordinador principal vendido por el controlador de stack. Crea un contexto de objeto gestionado vinculado a una cola privada para recuperar datos de la tienda y confirmar los cambios en la tienda.

[^ 1]: Esto es compatible con el enfoque “pasar el testigo” por el cual, particularmente en las aplicaciones de iOS, se pasa un contexto de un controlador de vista a otro. El controlador de vista raíz es responsable de crear el contexto inicial y pasarlo a los controladores de vista secundarios cuando sea necesario.

El motivo de este patrón es garantizar que los cambios en el gráfico del objeto gestionado estén debidamente restringidos. Core Data admite contextos de objetos gestionados “nesteds” que permiten una architecture flexible que facilita el soporte de conjuntos de cambios independientes y cancelables. Con un contexto secundario, puede permitir que el usuario realice un conjunto de cambios en objetos administrados que luego pueden ser comprometidos al por mayor para el padre (y finalmente guardarse en la tienda) como una sola transacción, o pueden descartarse. Si todas las partes de la aplicación simplemente recuperan el mismo contexto de, por ejemplo, un delegado de aplicación, hace que este comportamiento sea difícil o imposible de admitir.

Actualización: en iOS 10, Apple movió la sincronización desde el nivel de archivo sqlite hasta el coordinador persistente. Esto significa que ahora puede crear un contexto de cola privada y reutilizar el coordinador existente utilizado por el contexto principal sin los mismos problemas de rendimiento que habría tenido que hacer de esa manera antes, ¡genial!

Por cierto este documento de Apple está explicando este problema muy claramente. Swift versión de arriba para cualquier persona interesada

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

Y aún más simple si está usando NSPersistentContainer para iOS 10 y superior

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